Excelでのフォワード予測グラフ作成 その2:時間情報を考慮したフォワード予測

こんにちは、Forestです。

長らくお待たせしてしまいましたが、フォワード予測グラフの続きを公開します。

この記事は前回の「Excelでのフォワード予測グラフ作成 その1:損益を再現したフォワード予測」に続き、「時間情報」を考慮したフォワード予測グラフを作る内容です。前回は損益だけを考慮してグラフの横軸を「トレード数」としましたが、今回は「日時」が横軸になります。

損益だけを再現する場合はExcel関数だけでもお試し版を作ることができましたが、時間情報を考慮するとなるとVBAが必要不可欠です。面倒な手順を踏めばExcel関数だけでもできるのかもしれませんが、労力に対して結果が見合わないように思います。そのため、今回は素直にVBAで実装します。

※前回同様、VBAよりRやPython、MATLABなどを使える方はそちらで実現したほうが楽ということはお伝えしておきます。

時間情報を考慮したフォワード予測の手順

時間情報を考慮したフォワード予測は、以下のような順番で行っていきます。

  1. バックテストレポートから損益と時間情報を抽出
  2. 時間情報をフォワード予測に適した形(ポジション基準)に変換
  3. ブートストラップ法で予測範囲を作成
    1. パラメータを読み込む
    2. ランダムにトレードを抽出しつつ日毎に集計して$N$日分の損益系列を作成
    3. $M$回繰り返して$M$本の損益系列を作成
    4. $M$本の損益系列の中央値と信頼区間を計算
    5. シートに中央値と予測区間を挿入
  4. フォワード損益を日毎に集計*
  5. 実フォワードと予測範囲をグラフに表示

前回の損益だけを考慮する場合との最大の違いは、3-2でランダム抽出する際に「日毎で集計」する点、そして「ランダム抽出する個数」が決まっていない点(抽出終了条件は日数条件を満たすこと)です。特に後者が時間情報を考慮したフォワード推移予測を考える上で難しいと感じる点だと思います。今回はこの部分を理解できるように説明をしたつもりです。

いつものように、疑問がある部分は聞いていただければ回答&加筆修正しますので、遠慮なく質問してください。

なお、今回は最終的に以下のようなフォワード予測シートを作成することを目標とします。

*グラフの横軸は時間なのでフォワード損益はポジションごとに表示することもできますが、予測が日毎のため正しく比較できない場合がある点に注意が必要です。(基本は 日毎に集計した損益のばらつき<ポジションごとの損益ばらつき となるため)

参考のExcelファイル

具体的な中身に入る前に参考Excelを貼っておきます。

本Excelの無断での再配布は禁止します。再配布したい場合は事前にTwitterのDMかお問合せフォームから連絡をください。

今回はExcelファイルをGogoJungleの方にも無料商品として登録しました。GogoJungleでダウンロードしておくと、何らかの事情でこのサイトからファイルがダウンロードできなくなっても元ファイルが残るので、一度GogoJungleからもダウンロードしておくと良いと思います。

1. バックテストレポートから損益と時間情報を抽出

最初に計算に必要になる損益の情報と時間情報をバックテストレポートから取り出します。

MT4のレポートではポジションのOpenTime(建玉時刻)とCloseTime(決済時刻)が別の行に存在するので抽出には少し工夫が必要です。また、バックテスト上のドット区切りの時間表示「2021.02.21」はExcelでは時間ではなく文字列と認識されるため修正が必要な点にも注意が必要です。

Excelが認識できる時間形式に修正

まずは、バックテストの時間表示をExcelが認識できる形に修正します。これを行っておかないと、上記手順の2.で時間情報を変換する際にエラーになります前回同様Excelにバックテストレポートを取り込んでください。その後以下を実施します。

  • フィルタがかかっていない状態で取引履歴の時間列を全選択
  • 「Ctrl + H」で置換画面を表示
  • 半角ドット「.」を半角スラッシュ「/」に置換

置換後、時間のセルにカーソルを合わせたときに画面上の数式バーに「2006/8/2 9:00:00」のように秒まで表示されていれば成功です。セルを入力状態にしても確認できます。

Close側の情報抽出

時間を修正できたら損益とCloseTimeを抽出します。このとき、OpenTimeを探す手がかりにするために「注文番号」も同時に抽出しておきます。つまり、バックテストレポートの「時間」「注文番号」「損益」の列を抽出します。

まず、タイトル行にフィルタを適用して「取引種別」列で「s/l」「t/p」「close」を選択します。バックテストによってはバックテスト終了時に決済されたポジションを表す「close at stop」が存在しますが、EA自体で決済されたものではなくEA特性を表すとは言い難いので除外しておくべきです。

目的の行だけを表示できたら、「時間」「注文番号」「損益」の列を選択し、別シートにタイトル行ごとコピーします。今回はOpenTimeを入れるためにA列を空けておきB2セルに貼り付けました。そして、「時間」がCloseTimeだとわかるように列名を変更しておきます。

Open側の情報抽出

OpenTimeを注文番号を手がかりに抽出するために、まず注文番号とOpenTimeの対応表を作成しておきます。

CloseTime同様取引履歴をフィルタで選別してコピーするのではなくわざわざ注文番号を手がかりにする理由は、複数ポジションEAでは決済注文が前後する場合がありOpenとCloseの順序が一致しないためです(下図参照、右列が注文番号)。

注文番号でソートしてから貼り付ける方法もありますが、今回は「close at stop」での決済を除外しているため対応するエントリーを手作業で除外する必要があり手間がかかってしまいます。そのため、今回はExcelのVLOOKUP関数を使って注文番号に対応するOpenTimeを自動的に抽出します。

まず、注文番号とOpenTimeの対応表を作ります。Closeのときと同じように取引履歴の「取引種別」列のフィルタから「buy」と「sell」を選択します。そして「時間」と「注文番号」列を別シートへコピーします。今回はClose側情報を貼り付けたシートのF2, G2に貼り付けることにします。ここで注意して欲しいのは、VLOOKUP関数の性質上検索のキーにする列が最も左側にないといけないため、「注文番号」列はF列に貼り付ける必要がある点です。貼り付けたら、あとから見返したときにわかるように時間の列名を「OpenTime」に変えておきます

これでVLOOKUP関数でOpenTimeを抽出する準備が整いました。

注文番号を手がかりにしたOpenTime抽出

ではVLOOKUP関数を使って注文番号を手がかりにOpenTimeを抽出し、A列へ挿入していきましょう。

Close側の注文番号はC列に、先ほど作成したOpen側の注文番号とOpenTimeの対応表はF3~GXX(XXはトレード数次第)にあります。VLOOKUP関数は VLOOKUP(検索値, 範囲, 列番号, 検索方法) と指定すると以下のように動作します。

  • 「範囲」の一番左の列と「検索値」が一致する行を検索
  • 一致した行の「列番号」番目の列の値を取得
  • 「検索方法」がFALSEなら検索時に完全一致する場合だけ値を返し、TRUEなら検索時に一致するものがない場合は最も近い値を返す(デフォルトはTRUE)

つまり、「検索値」をClose側の注文番号「範囲」を対応表「列番号」を2「検索方法」をFALSE とすればClose側の注文番号を手がかりにOpenTimeを取得できます。

例えば、A3セルに注文番号1のOpenTimeを入れたい場合、A3に以下の式を入力します。検索値や範囲にはコピーしたときに列や行がズレないようにオートインクリメント無効化のドルマーク(\$)を忘れずにつけましょう。あとは取引数分だけA列にコピーしていけば各トレードのOpenTimeが抽出できます。

=VLOOKUP($C3, $F$3:$G$2007, 2, FALSE)

2. 時間情報をフォワード予測に適した形(ポジション基準)に変換

OpenTimeとCloseTimeは抽出できましたが、フォワード予測に使うにはもうひと手間必要です。ここではそのための変換方法と理由を説明します。

下図はバックテスト上の時間表記を表した図です。この図ではt番目トレードのOpenTimeをOpenTime[t]CloseTimeをCloseTime[t]と表記しています。横軸を時間の流れとすればOpenTime[t]やCloseTime[t]が表すのはその瞬間の時刻を示す青の縦線です。人が見る分にはこの表記で良いのですが、フォワード推移予測のためには建玉と決済の間、トレード間の時間関係が必要なのでOpenTime[t]やCloseTime[t]での表記は望ましくありません

バックテストでの時間情報

そこで、以下のように建玉・決済間、トレード間の時間関係を表すことにします。

  • 建玉・決済間:保有時間 HoldPeriod[t] = CloseTime[t] – OpenTime[t]
    • 1つのポジション内での時間(建玉~決済)を表すために使用
  • トレード間:前ポジション保有からの経過時間 TradeInterval[t] = OpenTime[t] – OpenTime[t-1]
    • 2つのトレード間をつなぐために使用

この変換では1つ目のトレードのTradeIntervalは計算できない点に注意してください。時間情報を考慮する場合、フォワード予測では1つ目のトレードは使わず2つ目以降のトレードの情報だけを使います。TradeIntervalの計算方法を変えれば最後のトレードを使わないようにもできますが、「直近のトレードのほうが重要度が高い」という考えのもと今回の方法を採用しています。

下図はこの変換を行った際の時間表記のイメージです。トレード間はTradeIntervalで、建玉と決済はHoldPeriodで繋がっています。つまり、開始日を決めたうえで、前回の損益のようにこれらの情報をランダムに抽出して繋いでいけば、時間情報を考慮したフォワード予測が行えます

時間情報を考慮するための変換

トレード間の関係は一つ前のポジションの決済からの経過時間(OpenTime[t] – CloseTime[t-1])と表すこともできますが、複数ポジションの場合にマイナスの値になり得るのでプログラム上での処理を考えると扱いづらいです。今回はOpenTimeの間隔でトレード間の時間情報を考慮することにします。

さて考え方がわかったところで、実際にExcelで計算していきます。といっても特に難しいところはなく、時間同士を引き算するだけで良いです。上記HoldPeriodとTradeIntervalの式の通りに計算式を作ります。I列にHoldPeriod、J列にTradeIntervalを入れる場合は以下のようになります。

=$B3-$A3
=$A4-$A3

もしHoldPeriodTradeIntervalが上記画像のように時間表示にならない場合はセルの書式を変えれば正しく表示できます。セルを右クリック「セルの書式設定」 「ユーザ定義」 と開き、種類欄に 「[h]:mm:ss」と入力します。「[h]」にカッコをつけず「h」とした場合、24時間より長い時間は24で割った余り(28なら4、50なら2)で表示されてしまうので気をつけてください。計算自体は正しく行えているので表示上の問題だけなのですが、目視チェックのためにも直しておきましょう。

最後に、前回の記事と同様に、損益の値を1ロットあたりの損益に変換しておきましょう。これはK列に入れました。

ここまでで、時間情報を考慮してフォワード推移予測を行うための下準備が整いました。あとはVBAを使ってフォワード予測を使うコードを作成していきます。

3. ブートストラップ法で予測範囲を作成

3-1. パラメータの読み込み

まずは予測範囲を作成するために必要なパラメータを読み込みます。

やり方は前回と変わりませんが、今回は取引数ではなく時間要素を考慮する必要があるため「開始日」「生成日数」のパラメータが新設されています。代わりに「生成する取引数」のパラメータがなくなりました。これは、前述の通り「指定した日数条件を満たすまで取引を生成し続ける」必要があるため、取引数では正しく日数条件を満たせないことが理由です。

もう少し具体例を挙げておきます。
例えば2021/1/1 00:00 からヨーイドンで 100トレードを生成し、それを1000試行繰り返したとします。バックテストのTradeIntervalの平均値は6時間HoldPeriodの平均値は2時間だとすると、1000試行の最後のトレードのCloseTimeの中央値は概ね 6時間×100+2時間=602時間後、つまり2021/1/26 02:00程度になります。ただ基本的にTradeIntervalにもHoldPeriodにもばらつきがありますから、すべての試行で同じ値にはならず時間が前後します。ある試行では短い期間が多く出現して 2021/1/15 10:00 となるかもしれませんし、別の試行では反対に 2021/2/10 20:00 となるかもしれません。「日数」の条件を「取引数」指定で満たすためにはばらつきも考慮せねばならず、中々難易度が高いのです。

さて、実際の読み込みですが、ここは前回とやることは変わりません。「開始日」は日付なのでDate型として読み込む点だけ注意が必要です。

    ' パラメータとして、生成開始日、生成する日数、系列数、信頼区間幅、フォワードロットを読み込む
    Dim startDate As Date
    Dim numOfGeneratingDates As Long
    Dim numOfSeries As Long
    Dim confidence As Double
    Dim forwardLots As Double
    startDate = sheet.Cells(3, 7)   ' G3
    numOfGeneratingDates = sheet.Cells(3, 8) ' H3
    numOfSeries = sheet.Cells(3, 9) ' I3
    confidence = sheet.Cells(3, 10) ' J3
    forwardLots = sheet.Cells(3, 11) ' K3

以下に読み込むパラメータをまとめます。

パラメータ変数名データ型
生成開始日startDateDate
生成する日数numOfGeneratingDatesLong
生成する系列数numOfSeriesLong
信頼区間の幅confidenceDouble
フォワードロット数forwardLotsDouble

3-2. ランダムに損益を抽出しつつ日毎に集計して$N$日分の損益系列を作成

この部分が前回の損益だけを考慮する場合と異なり、また一番の山でもあります。ここさえクリアすればあとは前回とほとんど同じなので、フォワード推移予測はできたも同然です。

3-2-1. バックテスト情報を読み込む

前回は損益だけの1列を使いましたが、今回はHoldPeriod,TradeInterval, 損益の3列を使います。1トレード目はTradeIntervalが存在しないので今回は使わず、A3からバックテストトレード数分だけの範囲をRangeオブジェクトとして読み込んでおきます。

    ' バックテストの範囲を取得する:1トレード目は使わないので2トレード目以降
    Dim rangeBacktestTrades As Range
    Set rangeBacktestTrades = sheet.Range(Cells(3, 1), Cells(numOfBacktestTrades + 1, 3)) ' A3~バックテストの数

3-2-2. ランダムに1つトレードを取り出す

この部分は前回とほぼ同じなのですが、取り出す値が1行分の3つに増えている点で異なります。今回はすべて実数(Date型の中身は実数)なので、Double型の配列に入れて関数から返します

' バックテストからランダムに1つトレードを取得する関数
' Parameters
'   :trades: バックテスト損益を表すRangeオブジェクト
' Return
'   (Double(3)): 抽出したHoldPeriod, TradeInterval, 損益を含む1行3列のDouble型配列
Function getBacktestRange(trades As Range) As Double()
    ' お手軽にWorksheetFunctionのRANDBETWEENを使う
    Dim r As Long: r = WorksheetFunction.RandBetween(1, trades.Rows.count)
    Dim row(3) As Double
    row(1) = trades.Cells(r, 1)
    row(2) = trades.Cells(r, 2)
    row(3) = trades.Cells(r, 3)
    getBacktestRange = row
End Function

3-2-3. 取引を日毎に集計して$N$日分の損益系列を作成

さて、ここが一番の山です。ここを越えれば、次の3-3, 3-4, 3-5は前回と全く同じ(変数名が違うだけ)なのでほぼでき上がりです。

まず以下に作成する関数の全文を貼ります。

' 指定日数分のトレードを生成して損益系列を作成する関数
' Parameters
'   :trades (Range): バックテストのHoldPeriod, TradeInterval, 損益を含むRangeオブジェクト
'   :numOfGeneratingDates (Long): 生成する日数
' Return
'   (Double(numOfGeneratingDates+1)): 抽出した損益の配列:要素数=numOfGeneratingDates+1で、i=0は損益0
Function makeProfitSeries(trades As Range, numOfGeneratingDates As Long) As Double()
    ' 日数+1日分の配列を確保してゼロに初期化
    Dim i As Long
    Dim dailyProfit() As Double     ' 日毎の損益
    Dim profitSeries() As Double    ' 累積損益
    ReDim profitSeries(numOfGeneratingDates)
    ReDim dailyProfit(numOfGeneratingDates)
    For i = 0 To numOfGeneratingDates
        profitSeries(i) = 0.0
        dailyProfit(i) = 0.0
    Next i
    ' TradeIntervalの累積がnumOfGeneratingDatesを越えるまで生成し続ける
    ' あわせて決済時刻にごとに損益を集計していく
    Dim closeTime As Double
    Dim tradeRange() As Double
    ReDim tradeRange(3)
    Dim cumsumTradeInterval As Double: cumsumTradeInterval = 0.0
    Do While cumsumTradeInterval < numOfGeneratingDates
        ' バックテストから1行取得
        tradeRange = getBacktestRange(trades)
        ' TradeIntervalを加算
        cumsumTradeInterval = cumsumTradeInterval + tradeRange(2)
        ' OpenTimeが生成日数を超えた時点で終了
        If cumsumTradeInterval >= numOfGeneratingDates Then
            Exit Do
        End If
        ' 決済時間を計算:累積TradeInterval + HoldPeriod
        closeTime = cumsumTradeInterval + tradeRange(1)
        ' 決済時間が指定時間を越えていない場合だけ損益に加算
        ' この時点で終了させないのは、当日中に新たなトレード発生する可能性があるため
        If closeTime < numOfGeneratingDates Then
            ' 決済時間に該当する配列に損益を加える
            ' 0<=closeTime<1のときは開始0日目なので開始日:profitSeries(1)に加える
            ' i<=closeTime<i+1のときは開始i日目なのでprofitSeries(i+1)に加える
            dailyProfit(Int(closeTime) + 1) = dailyProfit(Int(closeTime) + 1) + tradeRange(3)
        End If
    Loop
    ' 日毎の損益を累積損益に変換
    For i = 1 To numOfGeneratingDates
        profitSeries(i) = profitSeries(i-1) + dailyProfit(i)
    Next i
    ' 累積損益を返す
    makeProfitSeries = profitSeries
End Function

最初のブロックは初期化です。ここはスルーして、キモになる日毎の集計の部分を重点的に解説します。以下の部分になります。

ここで、closeTime変数は「今生成したトレードの決済時刻」を一時的に保存する変数、tradeRange配列は先ほどのgetBacktestRange関数の戻り値を受け取る配列、cumsumTradeInterval変数はこれまでに生成したトレードのTradeIntervalの合計、つまり最新ポジションのOpenTimeに対応する数値です。

    ' TradeIntervalの累積がnumOfGeneratingDatesを越えるまで生成し続ける
    ' あわせて決済時刻にごとに損益を集計していく
    Dim closeTime As Double
    Dim tradeRange() As Double
    ReDim tradeRange(3)
    Dim cumsumTradeInterval As Double: cumsumTradeInterval = 0.0
    Do While cumsumTradeInterval < numOfGeneratingDates
        ' バックテストから1行取得
        tradeRange = getBacktestRange(trades)
        ' TradeIntervalを加算
        cumsumTradeInterval = cumsumTradeInterval + tradeRange(2)
        ' OpenTimeが生成日数を超えた時点で終了
        If cumsumTradeInterval >= numOfGeneratingDates Then
            Exit Do
        End If
        ' 決済時間を計算:累積TradeInterval + HoldPeriod
        closeTime = cumsumTradeInterval + tradeRange(1)
        ' 決済時間が指定時間を越えていない場合だけ損益に加算
        ' この時点で終了させないのは、当日中に新たなトレード発生する可能性があるため
        If closeTime < numOfGeneratingDates Then
            ' 決済時間に該当する配列に損益を加える
            ' 0<=closeTime<1のときは開始0日目なので開始日:profitSeries(1)に加える
            ' i<=closeTime<i+1のときは開始i日目なのでprofitSeries(i+1)に加える
            dailyProfit(Int(closeTime) + 1) = dailyProfit(Int(closeTime) + 1) + tradeRange(3)
        End If
    Loop

まず、この部分。ここでは「指定日数の取引を生成」する終了条件を設定しています。実のところDo Whileループの中で終了条件をチェックしているのでこの条件はいらないのですが、まあ気にせず入れておきます。

Do While cumsumTradeInterval < numOfGeneratingDates

次はこの部分です。
ここはコメントの通り、バックテストから1トレード分の情報(HoldPeriod, TradeInterval, 損益)を取得し、cumsumTradeIntervalに取得したTradeIntervalを足していきます。cumsumTradeIntervalは最新ポジションのOpenTimeが「開始から数えて何日経過したか」を表す変数ですから、この値が「生成する日数」を表すnumOfGeneratingDates以上になった時点で終了条件を満たします。こうなれば Exit Do でループを抜けて生成完了とします。

        ' バックテストから1行取得
        tradeRange = getBacktestRange(trades)
        ' TradeIntervalを加算
        cumsumTradeInterval = cumsumTradeInterval + tradeRange(2)
        ' OpenTimeが生成日数を超えた時点で終了
        If cumsumTradeInterval >= numOfGeneratingDates Then
            Exit Do
        End If

次が最も重要な部分です。
まず、OpenTime相当のcumsumTradeIntervalに生成したトレードのHoldPeriodを足してcloseTime相当の値を作成します。その後、closeTimeが「生成する日数」を越えていなければ損益を集計します。

次の集計の部分がミソで、「VBAでは日付は1日を1.0とする実数」で表現されていることを利用します。生成初日(つまりcloseTimeが0.***の値)なら配列dailyProfitの1番目に、生成2日目(closeTimeが1.***)に損益を足したいです。このためには、closeTimeの整数部分に1を足せば良いので、dailyProfitのInt(closeTime)+1番目の要素に生成したトレードの損益を足しますこれをループ終了まで繰り返せば、日毎に集計した損益が計算できます

        ' 決済時間を計算:累積TradeInterval + HoldPeriod
        closeTime = cumsumTradeInterval + tradeRange(1)
        ' 決済時間が指定時間を越えていない場合だけ損益に加算
        ' この時点で終了させないのは、当日中に新たなトレード発生する可能性があるため
        If closeTime < numOfGeneratingDates Then
            ' 決済時間に該当する配列に損益を加える
            ' 0<=closeTime<1のときは開始0日目なので開始日:profitSeries(1)に加える
            ' i<=closeTime<i+1のときは開始i日目なのでprofitSeries(i+1)に加える
            dailyProfit(Int(closeTime) + 1) = dailyProfit(Int(closeTime) + 1) + tradeRange(3)
        End If

最後に、日毎の損益を累積損益に変換して関数から返します。これで日毎に集計した損益系列を作成できました。

    ' 日毎の損益を累積損益に変換
    For i = 1 To numOfGeneratingDates
        profitSeries(i) = profitSeries(i-1) + dailyProfit(i)
    Next i
    ' 累積損益を返す
    makeProfitSeries = profitSeries

3-3. $M$回繰り返して$M$本の損益系列を作成

この部分は前回と全く同じです。一応今回の「生成する日数」という意味にあわせて変数名だけnumOfGeneratingDatesに変えています。

' 抽出日数と系列数を指定してリサンプリングデータを作成する関数
' Parameters
'   :profits (Range): バックテスト損益を表すRangeオブジェクト
'   :numOfGeneratingDates (Long): 生成する日数
'   :numOfSeries (Long): 生成する系列数
' Return
'   (Double(numOfGeneratingDates+1,numOfSeries)): 抽出した損益の配列。各系列(0, i)の値は0
Function makeMultiProfitSeries(profits As Range, numOfGeneratingDates As Long, numOfSeries As Long) As Double()
    ' 日数 x 系列数 の2次元配列を定義
    Dim multiProfitSeries() As Double
    ReDim multiProfitSeries(numOfGeneratingDates, numOfSeries-1)

    ' 1系列の配列を定義
    Dim profitSeries() As Double
    ReDim profitSeries(numOfGeneratingDates)

    Dim iseries, itrade As Long
    For iseries = 0 To numOfSeries - 1
        ' 1系列分の損益系列を生成
        profitSeries = makeProfitSeries(profits, numOfGeneratingDates)
        ' 2次元配列へコピー
        For itrade = 0 To numOfGeneratingDates
            multiProfitSeries(itrade, iseries) = profitSeries(itrade)
        Next itrade
    Next iseries

    makeMultiProfitSeries = multiProfitSeries
End Function

3-4. $M$本の損益系列の中央値と信頼区間を計算

こちらも前回と全く同じですが、変数名だけ変更しています。

' 生成した系列から中央値と信頼区間を求める関数
' Parameters
'   :series (Dobule(numOfGeneratingDates+1,numOfSeries)): 生成した損益系列
'   :numOfGeneratingDates (Long): 生成した日数
'   :numOfSeries (Long): 生成した系列数
'   :confidence (Double): 信頼区間の幅(0~1で指定)
' Return
'   (Double(numOfGeneratingDates+1,3)): 信頼区間下限(0)、中央値(1)、信頼区間上限(2)
Function calcMedianAndConfidenceInterval(series() As Double, numOfGeneratingDates As Long, numOfSeries As Long, confidence As Double) As Double()
    ' トレード数 x 信頼区間下限・中央値・信頼区間上限 の2次元配列を定義
    Dim medianAndConfidence() As Double
    ReDim medianAndConfidence(numOfGeneratingDates, 2)
    ' 配列の(0, :)にはゼロを入れておく
    medianAndConfidence(0, 0) = 0.0
    medianAndConfidence(0, 1) = 0.0
    medianAndConfidence(0, 2) = 0.0

    ' 計算のための一時保存用配列を定義
    Dim tmp() As Double
    ReDim tmp(numOfSeries - 1)

    Dim iseries, itrade As Long
    For itrade = 1 To numOfGeneratingDates
        ' 生成した損益系列のトレードNoごとに、中央値と信頼区間を計算
        For iseries = 0 To numOfSeries - 1
            tmp(iseries) = series(itrade, iseries)
            ' 信頼区間下限
            medianAndConfidence(itrade, 0) = WorksheetFunction.Percentile(tmp, (1.0 - confidence) / 2.0)
            ' 中央値
            medianAndConfidence(itrade, 1) = WorksheetFunction.Percentile(tmp, 0.5)
            ' 信頼区間上限
            medianAndConfidence(itrade, 2) = WorksheetFunction.Percentile(tmp, 1.0 - (1.0 - confidence) / 2.0)
        Next iseries
    Next itrade

    calcMedianAndConfidenceInterval = medianAndConfidence
End Function

3-5. 中央値と信頼区間をシートに挿入

ここもほぼ前回通りです。一つだけ変わっているのは、横軸用の値が連番ではなく日時になる点です。

' 中央値と信頼区間をセルに挿入する関数
' Parameters
'   :series (Dobule(numOfGeneratingDates+1,3)): 中央値と信頼区間
'   :allRange (Range): 挿入先の範囲
'   :numOfGeneratingDates (Long): 生成した日数
'   :startDate (Date): 生成開始日
Sub insertMedianAndConfidenceToCells(medianAndConfidence() As Double, allRange As Range, numOfGeneratingDates As Long, startDate As Date)
    ' 範囲全体のセルの内容を削除
    allRange.Clear
    ' 範囲全体のうち、挿入する範囲だけを背景色指定、数値指定、小数点以下桁数指定
    Dim insertRange As Range
    Set insertRange = allRange.Range(Cells(1, 1), Cells(numOfGeneratingDates + 1, 4)) ' トレード数+1なのは最初のゼロを含むため
    ' 背景色指定:#E2EFDA
    insertRange.Interior.Color = &HE2EFDA
    ' 書式指定:日付はYYYY/MM/DD, 中央値・信頼区間は0.00_ ;[赤]-0.00
    insertRange.Range(Cells(1, 2), Cells(numOfGeneratingDates + 1, 4)).NumberFormatLocal = "0.00_ ;[赤]-0.00"
    insertRange.Range(Cells(1, 1), Cells(numOfGeneratingDates + 1, 1)).NumberFormatLocal = "yyyy/mm/dd"
    ' 罫線を引く
    insertRange.Borders.LineStyle = xlContinuous
    ' 中央値、信頼区間を挿入
    insertRange.Range(Cells(1, 2), Cells(numOfGeneratingDates + 1, 4)).Value = medianAndConfidence
    ' startDateから時刻を消して00:00にする。時刻は小数点以下の数で表現されるのでIntを取れば00:00になる
    startDate = Int(startDate)
    ' 日付を挿入:トレードごとの損益をプロットする場合も考えて、時刻は23:59:59に設定
    Dim i As Long
    For i = 0 To numOfGeneratingDates
        insertRange.Cells(i + 1, 1) = startDate - 1 + i + (1.0 - 1.0 / 86400.0)
    Next i
End Sub

横軸用の日付を作っている部分は以下です。
今回は一応、フォワード損益を日毎に集計せずポジションごとにプロットする場合にも対応するため、表示用の日付は23時59分59秒にしておきます。
まずInt(startDate)として生成開始日の時間・分・秒を消しておきます。その後、生成日分とその1日前の日付を列に入れていきます。その際に23:59:59になるように、1.0-(1.0/86400.0) を足しておきます。86400は1日の秒数で、24*60*60=86400で計算できます。

    ' startDateから時刻を消して00:00にする。時刻は小数点以下の数で表現されるのでIntを取れば00:00になる
    startDate = Int(startDate)
    ' 日付を挿入:トレードごとの損益をプロットする場合も考えて、時刻は23:59:59に設定
    Dim i As Long
    For i = 0 To numOfGeneratingDates
        insertRange.Cells(i + 1, 1) = startDate - 1 + i + (1.0 - 1.0 / 86400.0)
    Next i

さて、これでバックテストからのフォワード予測が完成しました。ここまでお疲れさまでした。

ただ今回はフォワードの方も日毎に集計しないといけないので、もう少しやることがあります。そちらは難しくないので、あと少しだけお付き合いください。

4. フォワード損益を日毎に集計

ほとんど場合フォワード損益のデータは「トレードごと」に取得されますから、これまでに計算した中央値や信頼区間と重ねて表示するためにはフォワード損益も「日毎」に集計が必要です。正確に言えば中央値・信頼区間もフォワード損益もどちらも横軸が「時間」なのでトレードごとの損益でも表示できます。しかし中央値・信頼区間は「日毎」で集計した際の値なので、「トレードごと」の損益とは正しく比較できません。そのため、フォワード損益も「日毎」に集計して基準を合わせます。

ただ厄介なことにmyfxbookなどから出力する時刻の情報はExcelで日付と認識できない形式になっています。myfxbookの例を題材に日付の修正をしたうえで、VBAで日毎に集計する方法を説明します。

4-1. 時刻情報の修正

以下はmyfxbookから出力されるトレード結果CSVです。決済時刻を表す列はD列の “Close Date” です。このClose Dateをよく見ると、日付の表記が MM/DD/YYYY (MM, DDは2桁の月, 日、YYYYは4桁の年) となっています。この形式の文字列はExcelでは自動的に日付と認識してくれませんExcelの機能をうまく使って日付に変換していきます。

まずはClose Dateの列をコピーし、新しいブックやシートに貼り付けます。今回はフォワード予測Excelファイルに「フォワード」シートを作成して作業することにします。

Close Dateを貼り付けたら、リボンの「データ」→「データツール」→「区切り位置」を選択します。Excelのバージョンによってレイアウトが異なるので、ご自身のExcelにあわせて読み替えてください。

「スペースによって右または左に揃えられた固定長フィールドのデータ」を選択します。

Excelの使用上どうも日付と時間を分けないと認識できないようなので、日付と時間の間に区切りを設定します。おそらく、元々設定されているはずです。

「データのプレビュー」の部分で日付の列が選択されていることを確認し、「列のデータ形式」で「日付」を選び、プルダウンから「MDY」を選択します。時間の列はそのままで認識できるので特に操作は不要です。

うまく行けば、A列に日付、B列に時間が入ります。あとはC列に”=A2+B2″のように式を入れてこの2列を足せば、日付と時間を正しく認識した列を作成できます

最後にmyfxbookのCSVからProfit列をコピーすればフォワードのCloseTimeと損益を準備できます。

4-2. フォワード損益の日毎の集計

日毎の集計をExcelの標準機能で行うのは大変なので、素直にVBAで書きます。VBA上の日時表現は1日を1.0とした実数なので、開始日=最も小さい日時の整数部分 ~ 終了日=最も大きい日時の整数部分 に対応する日毎の配列を用意し、CloseTimeに対応する配列の要素に損益を加算すれば集計できます。
例えば開始日=2000・終了日=3000だとすると、日数は2001日になります。2001個の要素を持つ配列を用意し、CloseTimeの整数部分が2000なら1番目、2001なら2番目の要素となるように、CloseTimeの整数部分-2000+1番目の配列要素に損益を足していけば、日毎に集計した損益になります

最後に累積損益に変換すれば完成です。

まず、VBA作成時にシートの違いを意識しなくて良いように、CloseTimeとProfitをフォワード予測を行うシートにコピーします。今回はO,P列にコピーしました。あわせて、バックテスト損益と同様にデータ数をカウントしておきます。

次にVBAでフォワードを日毎に集計していきます。

最初にフォワード損益を読み込みます。これはバックテストと同じ方法なのでコードのみ貼り付けます。

    ''' フォワードの集計
    ' フォワードのトレード数を読み込む
    Dim numOfForwardTrades As Long
    numOfForwardTrades = sheet.Cells(2, 17) ' Q2固定

    ' フォワード損益の範囲を読み込む
    Dim rangeForwardTrades As Range
    Set rangeForwardTrades = sheet.Range(Cells(2, 15), Cells(numOfForwardTrades + 1, 16)) ' O2~フォワードトレード数

次に読み込んだフォワード損益を日毎に集計します。Excelは使い慣れておらず良い方法が思いつかないので、ここでは愚直に「日付に対応する配列を用意してCloseTimeに対応する要素に損益を足していく」ことにします。

作成する関数は aggregateForwardProfit とし、引数として先ほど読み込んだフォワード損益の範囲(Rangeオブジェクト)を受け取ります。この関数の処理が終わると、フォワード日数分の要素数を持つ累積損益の配列を返します。

' フォワード損益を日毎に集計する関数
' Parameters
'   :trades (Range): フォワードのCloseTimeと損益を含むRangeオブジェクト
' Return
'   (Double()): 日毎の累積損益の配列:要素数=フォワード日数
Function aggregateForwardProfit(trades As Range) As Double()
    ' フォワードの初決済と最終決済の日付を取得
    Dim firstDate, lastDate As Date
    firstDate = Application.WorksheetFunction.Min(trades.Columns(1))
    lastDate = Application.WorksheetFunction.Max(trades.Columns(1))

    ' トレード日数を計算:最終日も日数に含むので1を足す
    Dim numOfTradeDates As Long
    numOfTradeDates = Int(lastDate) - Int(firstDate) + 1

    ' 集計用の配列を定義
    Dim dailyProfit() As Double     ' 日毎の損益
    Dim profitSeries() As Double    ' 累積損益
    ReDim dailyProfit(numOfTradeDates)
    ReDim profitSeries(numOfTradeDates)
    Dim i As Long
    For i = 0 To numOfTradeDates
        profitSeries(i) = 0.0
        dailyProfit(i) = 0.0
    Next i

    ' CloseTimeをもとに日毎の損益を集計
    Dim idx As Long
    For i = 1 To trades.Rows.count
        idx = Int(trades.Cells(i, 1)) - Int(firstDate) + 1
        dailyProfit(idx) = dailyProfit(idx) + trades.Cells(i, 2)
    Next i

    ' 日毎の損益を累積損益に変換
    For i = 1 To numOfTradeDates
        profitSeries(i) = profitSeries(i-1) + dailyProfit(i)
    Next i
    ' 累積損益を返す
    aggregateForwardProfit = profitSeries
End Function

順に説明していきます。
今回は、フォワード日数分の要素を持つ配列を用意しておき、ポジションを順に見ていきCloseTimeに対応する日の配列要素に損益を足していく方針を取ります。まず「フォワード日数」を求めないと始まらないので、以下で求めています。

Excelにおける日時は1日を1.0とした実数で表現されていますから、O列の最小値(Min)と最大値(Max)が最初と最後のトレード日時です。ここはワークシート関数のMinとMaxで、フォワード損益範囲の1列目(Columns(1))の最大値と最小値を求めています。
ほしいのは日数で時間の部分(小数点以下)はいらないので、Int関数で整数にしてから差を取って1を足し、Long型のnumOfTradeDates変数に入れておきます。

    ' フォワードの初決済と最終決済の日付を取得
    Dim firstDate, lastDate As Date
    firstDate = Application.WorksheetFunction.Min(trades.Columns(1))
    lastDate = Application.WorksheetFunction.Max(trades.Columns(1))

    ' トレード日数を計算:最終日も日数に含むので1を足す
    Dim numOfTradeDates As Long
    numOfTradeDates = Int(lastDate) - Int(firstDate) + 1

次に日毎に損益を集計する配列(dailyProfit)と、それを累積に変換するための配列(profitSeries)を定義しています。配列長は未定なので、可変長配列で宣言してからReDimで配列長をあとから設定します。

    ' 集計用の配列を定義
    Dim dailyProfit() As Double     ' 日毎の損益
    Dim profitSeries() As Double    ' 累積損益
    ReDim dailyProfit(numOfTradeDates)
    ReDim profitSeries(numOfTradeDates)

配列の宣言ができたら、損益を集計していきます。フォワードの日毎集計はここが要所です。
ポジションのCloseTimeがfirstDateと同日の場合は配列の1番目、firstDateの翌日なら2番目に加算したいです。i番目のトレードのCloseTimeの日付(整数)部分からfirstDateの日付(整数)部分を引くと同日で0になりますから、1を足せば欲しい値になります。これを配列の位置を示すインデックスとしてidx変数に入れておき、dailyProfitに損益を集計します。

    ' CloseTimeをもとに日毎の損益を集計
    Dim idx As Long
    For i = 1 To trades.Rows.count
        idx = Int(trades.Cells(i, 1)) - Int(firstDate) + 1
        dailyProfit(idx) = dailyProfit(idx) + trades.Cells(i, 2)
    Next i

最後に、日毎の損益を累積損益に変換します。ここは特に説明はいらないと思います。

    ' 日毎の損益を累積損益に変換
    For i = 1 To numOfTradeDates
        profitSeries(i) = profitSeries(i-1) + dailyProfit(i)
    Next i

これで日毎の損益に変換できました。
あとはシートに貼り付けていきます。

日毎のフォワード損益はJ・K列の29行目以降に入れることにしましょう。フォワード予測のときと同じように、挿入範囲を読み込んでおいて、関数に渡して値をセルに入れていきます。この部分はバックテストとほぼ同じなので載せるだけにしておきます。

    ' フォワードをセルに挿入
    ' フォワード挿入範囲を定義:J29~K50028
    Dim insertForwardRange As Range
    Set insertForwardRange = sheet.Range(Cells(minRow, 10), Cells(maxRow, 11))
    Call insertForwardProfit(dailyForwardProfit, insertForwardRange, Application.WorksheetFunction.Min(rangeForwardTrades.Columns(1)))
' フォワード集計結果をセルに挿入する関数
' Parameters
'   :series (Dobule(フォワード日数,2)): フォワード
'   :allRange (Range): 挿入先の範囲
'   :startDate (Date): フォワード開始日
Sub insertForwardProfit(forwardProfit() As Double, allRange As Range, startDate As Date)
    ' 範囲全体のセルの内容を削除
    allRange.Clear
    ' 範囲全体のうち、挿入する範囲だけを背景色指定、数値指定、小数点以下桁数指定
    Dim insertRange As Range
    Set insertRange = allRange.Range(Cells(1, 1), Cells(UBound(forwardProfit) + 1, 2)) ' トレード数+1なのは最初のゼロを含むため
    ' 背景色指定:#E2EFDA
    insertRange.Interior.Color = &HFCE4D6
    ' 書式指定:日付はYYYY/MM/DD, 中央値・信頼区間は0.00_ ;[赤]-0.00
    insertRange.Range(Cells(1, 2), Cells(UBound(forwardProfit) + 1, 2)).NumberFormatLocal = "0.00_ ;[赤]-0.00"
    insertRange.Range(Cells(1, 1), Cells(UBound(forwardProfit) + 1, 1)).NumberFormatLocal = "yyyy/mm/dd"
    ' 罫線を引く
    insertRange.Borders.LineStyle = xlContinuous
    ' フォワード損益を挿入
    insertRange.Range(Cells(1, 2), Cells(UBound(forwardProfit) + 1, 2)).Value = forwardProfit
    ' startDateから時刻を消して00:00にする。時刻は小数点以下の数で表現されるのでIntを取れば00:00になる
    startDate = Int(startDate)
    ' 最初の行は損益ゼロを入れる
    insertRange.Cells(1, 1) = startDate - 1 + (1.0 - 1.0 / 86400.0)
    insertRange.Cells(1, 2) = 0.0
    ' 日付を挿入:トレードごとの損益をプロットする場合も考えて、時刻は23:59:59に設定
    Dim i As Long
    For i = 2 To UBound(forwardProfit) + 1
        insertRange.Cells(i, 1) = startDate + (i - 2) + (1.0 - 1.0 / 86400.0)
        insertRange.Cells(i, 2) = forwardProfit(i - 1)
    Next i
End Sub

さいごに

以上で時間情報を考慮したフォワード予測をVBAで実現できました。「損益」に加えて今回は「時間」も考慮する必要があり、難易度が一気に上がっていたと思います。わかりにくい部分はDMやお問合せフォームから気軽に聞いてください。

前回と今回で「損益だけ」「損益と時間」を考慮したフォワード予測を作成し、「時間」の有無による期待値と実フォワードの関係の変化を検討できるようになりました。

ポジション数の軸で見れば順調、つまりRFやPFなどの統計値は順調だとしても、時間の要素を含めると信頼区間下限を下回っているということもあり得ます。ぜひ、様々なEAで2種類のフォワード予測を比較してみてください。

ここまで読んでくださりありがとうございました。

Forest