MQL5でモンテカルロ・シミュレーションを実行する関数

MQL5でモンテカルロ・シミュレーションを実行する関数を作った。

モンテカルロ・シミュレーションと一口に言ってもやり方はいろいろある。ここでは

  • バックテストした後に各トレードの損益を1年当たりのトレード数だけ重複ありで抽出し、リターン、ドローダウン、リターン・ドローダウン比を計算する。
  • そして、これを2500回繰り返し、リターン、ドローダウン、リターン・ドローダウン比それぞれの中央値を出力する。
  • また、2500回中で黒字だった回数、破産した回数を計算して黒字確率、破産確率も出力する。
  • 黒字とは最終損益がプラスであったこと、破産とは最初に入金として設定した初期証拠金が最小ロット数を注文するに必要な金額を下回ったことを指す。
  • 戻り値はリターン・ドローダウン比の中央値である。

といった内容である。これはケビン・J・ダービー著「システムトレード 検証と実践」で紹介していたやり方を参考にした。

環境

  • OANDA MetaTrader 5: 5.00 build 4040
  • MetaEditor: 5.00 build 4040

MQL5でモンテカルロ・シミュレーションを実行する関数

//+------------------------------------------------------------------+
//| モンテカルロ・シミュレーション(2023/11/26動作確認)                             |
//+------------------------------------------------------------------+
double MonteCarlo()
 {
  int trades=0; // 取引数
  datetime start_time=0; // 開始時間
  datetime end_time=0; // 終了時間
  double profit[100000]; // 損益
  HistorySelect(0,TimeCurrent());
  //--- リスト内番号0は初期証拠金なので除外
  for(int i=1;i<HistoryDealsTotal();i++)
    {
    ulong deal_ticket=HistoryDealGetTicket(i); // 約定のチケット
    double deal_profit=HistoryDealGetDouble(deal_ticket,DEAL_PROFIT); // 約定の損益
    datetime deal_time=(datetime)HistoryDealGetInteger(deal_ticket,DEAL_TIME); // 約定の時間
    // 損益を含まないデータを除外。損益0のケースはまれなので無視
    if(deal_profit!=0)
      {
      trades+=1;
      profit[trades-1]=deal_profit;
      }
    //--- 最初の取引の決済時間を開始時間と見なす
    if(i==1)
      start_time=deal_time;
    //--- 最後の取引の決済時間を終了時間と見なす
    if(i==HistoryDealsTotal()-1)
      end_time=deal_time;
    //---
    }
  //---
  double years=double(end_time-start_time)/60/60/24/365; // バックテスト年数
  double initial_deposit=TesterStatistics(STAT_INITIAL_DEPOSIT); // 初期証拠金
  //--- 最小証拠金を計算する(バックテスト終了日の終値+スプレッドが基準になる)
  double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); // ASK(BIDを基準にするより最小証拠金がわずかに大きくなる、つまり破産確率がわずかに高くなる)
  double volume_min=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); // 最小取引数量  
  double contract_size=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_CONTRACT_SIZE); // 契約サイズ(通常は10万通貨)
  double leverage=(double)AccountInfoInteger(ACCOUNT_LEVERAGE); // レバレッジ(アカウント上限のレバレッジ。通常は25倍)
  double min_equity=ask*volume_min*contract_size/leverage; // 最小証拠金
  //---
  double ret[2500]; // リターン(%)
  double dd[2500]; // ドローダウン(%)
  double ret_dd[2500]; // リターン・ドローダウン比(%)
  double surpluse[2500]; // 黒字(黒字なら1、黒字でなければ0)
  double ruin[2500]; // 破産(破産したら1、破産しなければ0)
  for(int i=0;i<2500;i++)
    {
    double cum_profit=0; // 累積損益
    double max_profit=0; // 最大損益
    double max_dd=0; // 最大ドローダウン
    double max_ddp=0; // 最大ドローダウン(%)
    //--- 黒字と破産は0で初期化
    surpluse[i]=0;
    ruin[i]=0;
    //--- 1年当たりの取引数の数だけ損益を重複ありでランダム抽出し、各パフォーマンスを計算する
    MathSrand(i);
    for(int j=0;j<MathRound(trades/years);j++)
      {
      //--- 乱数は0以上の整数なので、取引数で割った余りは0から取引数-1の間の乱数になる
      cum_profit+=profit[MathRand()%trades];
      //--- 最大損益を計算する
      if(max_profit<cum_profit)
        max_profit=cum_profit;
      //--- 最大ドローダウンを計算する
      if(max_dd<max_profit-cum_profit)
        max_dd=max_profit-cum_profit;
      //--- 最大ドローダウン(%)を計算する
      if(max_ddp<max_dd/(initial_deposit+max_profit)*100)
        max_ddp=max_dd/(initial_deposit+max_profit)*100;
      //--- 初期証拠金+累積損益が一時でも最小証拠金を下回ったら破産と見なして1を入力する
      if(initial_deposit+cum_profit<min_equity)
        ruin[i]=1;
      //---
      }
    //--- 最終的な累積損益が0より大きければ黒字と見なして1を入力する
    if(cum_profit>0)
      surpluse[i]=1;
    //---
    ret[i]=cum_profit/initial_deposit*100;
    dd[i]=max_ddp;
    if(dd[i]!=0)
      ret_dd[i]=ret[i]/dd[i];
    else
      ret_dd[i]=0;
    }
  //--- 昇順(小→大)に並べ替える
  ArraySort(ret);
  ArraySort(dd);
  ArraySort(ret_dd);
  //--- 破産した回数と黒字だった回数を計算する
  double ruins=0; // 破産した回数
  double surpluses=0; // 黒字だった回数
  for(int i=0;i<2500;i++)
    {
    ruins+=ruin[i];
    surpluses+=surpluse[i];
    }
  //--- 結果を出力する
  Print("★★★モンテカルロ・シミュレーション★★★");
  Print("初期証拠金=",DoubleToString(initial_deposit,0),"円");
  Print("リターンの中央値=",DoubleToString((ret[1249]+ret[1250])/2,2),"%");
  Print("ドローダウンの中央値=",DoubleToString((dd[1249]+dd[1250])/2,2),"%");
  Print("リターン・ドローダウン比の中央値=",DoubleToString((ret_dd[1249]+ret_dd[1250])/2,2));
  Print("黒字確率=",DoubleToString(surpluses/2500*100,2),"%");
  Print("破産確率=",DoubleToString(ruins/2500*100,2),"%");
  //--- リターン・ドローダウン比の中央値を戻り値とする
  double res=(ret_dd[1249]+ret_dd[1250])/2; // 戻り値
  //---
  return(res);
 }

使用例

OnTester()関数内でこの関数を利用する。

double OnTester()
 {
  return(MonteCarlo()*10000);
 }

なぜ戻り値を1万倍しているかというと、「カスタム最大」で最適化すると「結果」では小数が省略されるため、小数部分が分からなくなるからである。

あるトレード戦略でバックテストすると以下のように出力された。

★★★モンテカルロ・シミュレーション★★★
初期証拠金=1000000円
リターンの中央値=3.68%
ドローダウンの中央値=1.25%
リターン・ドローダウン比の中央値=2.87
黒字確率=94.72%
破産確率=0.00%

ケビン・J・ダービー氏によると、リターン・ドローダウン比の中央値が2未満のトレード戦略は使い物にならないとのことである。

なお、戻り値は28685.79426565952で、2.87(出力では四捨五入されている)の1万倍となっている。