
多通貨エキスパートアドバイザーの開発(第16回):異なるクォート履歴がテスト結果に与える影響
はじめに
前回は、リアル口座で取引するための多通貨EAの準備を始めました。その過程で、異なるブローカーごとの取引銘柄名のサポート、取引戦略の設定変更時の自動取引終了、およびEAがさまざまな理由で再起動した際の正しい復元といった機能を追加しました。
しかし、準備作業はこれで終わりではありません。他にも必要な手順がいくつかありますが、それらについては後ほど説明します。ここでは、異なるブローカー間で一貫した取引結果を確保するという重要な課題に焦点を当てていきます。異なるブローカーでは、同じ取引銘柄であっても提供されるクォートデータが完全に一致するわけではありません。そのため、特定のクォートデータを用いてテストや最適化を行うと、それに最適化されたパラメータが選択されます。当然ながら、異なるクォートデータで取引を開始した場合、その違いが小さければ取引結果への影響も最小限に抑えられると期待されます。
しかし、この問題を慎重に検討せずに放置するのは危険です。そこで、異なるブローカーのクォートデータを用いたテストを実施し、EAの動作を確認していきましょう。
結果の比較
まず、MetaQuotes-Demoサーバーからのクォート情報でEAを起動しましょう。最初の起動では、リスクマネージャーが有効になっていました。ただし、今後は、他のクォートではリスクマネージャーがテスト期間の終了よりも大幅に早く取引を完了したため、全体像を把握するためにこれを無効にします。こうすることで、より公平な結果の比較が可能になります。以下が結果です。
図1:リスクマネージャーなしのMetaQuotes-Demoサーバークォートのテスト結果
ここで、端末を別のブローカーのリアルサーバーに接続し、同じパラメータでEAテストを再度実行してみましょう。
図2:リスクマネージャーなしの別のブローカーのリアルサーバーのクォートのテスト結果
これは予想外の出来事です。口座残高は1年も経たないうちに完全に使い果たされました。この動作の背後にある理由を理解して、何らかの形で状況を修正できるかどうかを理解できるようにしてみましょう。
理由を探る
完了したパスのテスターレポートをXMLファイルとして保存し、それを開いて、完了した取引のリストが始まる場所を見つけましょう。開いているファイルウィンドウを配置して、両方のレポートの取引リストの上部部分を同時に表示できるようにします。
図3:異なるサーバーからのクォートをテストする際にEAが実行した取引リストの上部部分
レポートの最初の数行からでも、ポジションが異なる時間に建てられたことは明らかです。したがって、異なるサーバー上の同じ瞬間のクォートに違いがあったとしても、開始時間の違いほどの破壊的な影響は及ぼさない可能性が高いです。
戦略でポジションを建てる決定がどの瞬間に下されるかを見てみましょう。SimpleVolumesStrategy.mqh取引戦略の単一インスタンスのクラスを実装するファイルを確認する必要があります。コードを調べてみると、オープンシグナルを返すSignalForOpen()メソッドが見つかります。
//+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int CSimpleVolumesStrategy::SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(m_iVolumesHandle, 0, 0, m_signalPeriod, m_volumes); // If the required amount of numbers have been copied if(res == m_signalPeriod) { // Calculate their average value double avrVolume = ArrayAverage(m_volumes); // If the current volume exceeds the specified level, then if(m_volumes[0] > avrVolume * (1 + m_signalDeviation + m_ordersTotal * m_signaAddlDeviation)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(m_symbol, m_timeframe, 0) < iClose(m_symbol, m_timeframe, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; }
エントリーシグナルは、現在の取引商品のティックボリューム値によって決定されることがわかります。価格(現在および過去の両方)は、エントリーシグナルの形成には関与しません。より正確に言えば、ポジションを建てる必要があると判断された後にそれらの参加がおこなわれ、ポジションを建てる方向にのみ影響します。したがって、問題はまさに、異なるサーバーから受信したティックボリューム値に大きな違いがあることにあるようです。
これは、異なるブローカーがローソク足の価格チャートを視覚的に一致させるためには、最短期間M1のローソク足の始値、終値、高値、安値を構築するのに1分あたり4つの正しいティックを与えるだけで十分であるため、十分に可能です。価格が安値と高値の間の指定された制限内であった中間ティックの数は重要ではありません。つまり、ブローカーは、履歴に保存するティックの数と、それらを1つのローソク足内で時間の経過とともにどのように分配するかを自由に決定できます。また、同じブローカーであっても、デモ口座と実際の口座のサーバーでまったく同じ画像が表示されない場合があることも覚えておく価値があります。
もしこれが本当なら、この障害を回避するのは容易でしょう。しかし、このような回避策を実装するには、まず、観察された矛盾の原因を正しく特定して、努力が無駄にならないようにする必要があります。
パスのマッピング
仮説をテストするには、次のツールが必要になります。
- 履歴の保存:テスター実行の終了時に取引履歴(ポジションのエントリーとクローズ)を保存する機能をEAに追加しましょう。保存はファイルまたはデータベースのいずれかにおこなうことができます。このツールは今のところ補助的なツールとしてのみ使用されるため、ファイルに保存する方が使いやすいでしょう。将来的にこれをより永続的に使用したい場合は、履歴をデータベースに保存する機能を追加するように拡張できます。
- 取引再現:ポジションを建てるためのルールを一切含まず、別のEAによって保存された履歴からポジションのエントリーとクローズのみを再現する新しいEAを作成しましょう。とりあえず履歴をファイルに保存することにしたので、このEAは取引履歴を含むファイルの名前を入力として受け入れ、その中に保存されている取引を読み取って実行します。
これらのツールを作成したら、まずMetaQuotes-Demoサーバーからのクオートを使用してテスターでEAを起動し、このテストパスの取引履歴をファイルに保存します。これが最初のパスになります。次に、保存された履歴ファイルを使用して、別のサーバーからのクォートでテスターで新しい取引再現EAを起動します。これは2回目のパスになります。以前に得られた取引結果の違いが、実際にはティックボリュームデータが大きく異なることによるものであり、価格自体はほぼ同じである場合、2回目のパスでは1回目のパスの結果と同様の結果が得られるはずです。
履歴の保存
履歴保存を実装する方法はいくつかあります。たとえば、OnTester()イベントから呼び出されるメソッドをCVirtualAdvisorクラスに追加できます。この方法では、既存のクラスを拡張して、実際にはなくてもよい機能を追加することになります。そこで、この特定の問題を解決するために、別のクラスCExpertHistoryを作成しましょう。このクラスのオブジェクトを複数作成する必要はないので、静的に、つまり静的プロパティとメソッドのみを含むようにすることができます。
クラスの主なpublicメソッドはExport()のみです。残りのメソッドは補助的な役割を果たします。Export()メソッドは、履歴を書き込むファイルの名前と共有端末データフォルダを使用するフラグの2つのパラメータを受け取ります。デフォルトのファイル名は空の文字列になる場合があります。この場合、補助メソッドを使用してGetHistoryFileName()ファイルを生成します。共有フォルダへの書き込みフラグを使用して、履歴ファイルを保存する場所(共有データフォルダまたはローカル端末データフォルダ)を選択できます。テスターで実行する場合、共有フォルダを開くよりもテストエージェントのローカルフォルダを開くのが難しいため、デフォルトでは、共有フォルダへの書き込み用にフラグ値が設定されます。
クラスのプロパティとして、書き込み用にCSVファイルを開くときに指定する区切り文字、補助メソッドで使用できるように開いたファイル自体のハンドル、および保存されるデータの列名の配列が必要になります。
//+------------------------------------------------------------------+ //| Export trade history to file | //+------------------------------------------------------------------+ class CExpertHistory { private: static string s_sep; // Separator character static int s_file; // File handle for writing static string s_columnNames[]; // Array of column names // Write deal history to file static void WriteDealsHistory(); // Write one row of deal history to file static void WriteDealsHistoryRow(const string &fields[]); // Get the first deal date static datetime GetStartDate(); // Form a file name static string GetHistoryFileName(); public: // Export deal history static void Export( string exportFileName = "", // File name for export. If empty, the name is generated int commonFlag = FILE_COMMON // Save the file in shared data folder ); }; // Static class variables string CExpertHistory::s_sep = ","; int CExpertHistory::s_file; string CExpertHistory::s_columnNames[] = {"DATE", "TICKET", "TYPE", "SYMBOL", "VOLUME", "ENTRY", "PRICE", "STOPLOSS", "TAKEPROFIT", "PROFIT", "COMMISSION", "FEE", "SWAP", "MAGIC", "COMMENT" };
メインのExport()メソッドでは、指定された名前または生成された名前で書き込み用のファイルを作成して開きます。ファイルが正常に開かれた場合は、取引履歴保存メソッドを呼び出してファイルを閉じます。
//+------------------------------------------------------------------+ //| Export deal history | //+------------------------------------------------------------------+ void CExpertHistory::Export(string exportFileName = "", int commonFlag = FILE_COMMON) { // If the file name is not specified, then generate it if(exportFileName == "") { exportFileName = GetHistoryFileName(); } // Open the file for writing in the desired data folder s_file = FileOpen(exportFileName, commonFlag | FILE_WRITE | FILE_CSV | FILE_ANSI, s_sep); // If the file is open, if(s_file > 0) { // Set the deal history WriteDealsHistory(); // Close the file FileClose(s_file); } else { PrintFormat(__FUNCTION__" | ERROR: Can't open file [%s]. Last error: %d", exportFileName, GetLastError()); } }
GetHistoryFileName()メソッドでは、ファイル名は複数のフラグメントで構成されます。まず、__VERSION__定数で指定されている場合は、EA名とバージョンを名前の先頭に追加します。次に、取引履歴の開始日と終了日を追加します。GetStartDate()メソッドを呼び出して、履歴内の最初の取引の日付によって開始日を決定します。履歴はテスト実行が完了した後にエクスポートされるため、終了日は現在の時刻によって決定されます。つまり、履歴保存メソッドを呼び出した時点の現在時刻が、まさにテスト終了時刻となります。3番目に、初期残高、最終残高、ドローダウン、シャープ比など、いくつかのパス特性の値をファイル名に追加します。
名前が長すぎる場合は、許容できる長さまで短縮し、.history.csv拡張子を追加します。
//+------------------------------------------------------------------+ //| Form the file name | //+------------------------------------------------------------------+ string CExpertHistory::GetHistoryFileName() { // Take the EA name string fileName = MQLInfoString(MQL_PROGRAM_NAME); // If a version is specified, add it #ifdef __VERSION__ fileName += "." + __VERSION__; #endif fileName += " "; // Add the history start and end date fileName += "[" + TimeToString(GetStartDate(), TIME_DATE); fileName += " - " + TimeToString(TimeCurrent(), TIME_DATE) + "]"; fileName += " "; // Add some statistical characteristics fileName += "[" + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT), 0); fileName += ", " + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT) + TesterStatistics(STAT_PROFIT), 0); fileName += ", " + DoubleToString(TesterStatistics(STAT_EQUITY_DD_RELATIVE), 0); fileName += ", " + DoubleToString(TesterStatistics(STAT_SHARPE_RATIO), 2); fileName += "]"; // If the name is too long, shorten it if(StringLen(fileName) > 255 - 13) { fileName = StringSubstr(fileName, 0, 255 - 13); } // Add extension fileName += ".history.csv"; return fileName; }
履歴をファイルに書き込む方法では、まずデータ列の名前が書かれた行であるヘッダーを書き込みます。次に、利用可能なすべての履歴を選択し、すべての取引を反復処理し始めます。各取引のプロパティを取得します。これが取引の開始または残高操作である場合、すべての取引プロパティの値を含む配列を形成し、それをWriteDealsHistoryRow()メソッドに渡して単一の取引を書き込みます。
//+------------------------------------------------------------------+ //| Write deal history to file | //+------------------------------------------------------------------+ void CExpertHistory::WriteDealsHistory() { // Write a header with column names WriteDealsHistoryRow(s_columnNames); // Variables for each deal properties uint total; ulong ticket = 0; long entry; double price; double sl, tp; double profit, commission, fee, swap; double volume; datetime time; string symbol; long type, magic; string comment; // Take the entire history HistorySelect(0, TimeCurrent()); total = HistoryDealsTotal(); // For all deals for(uint i = 0; i < total; i++) { // If the deal is successfully selected, if((ticket = HistoryDealGetTicket(i)) > 0) { // Get the values of its properties time = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME); type = HistoryDealGetInteger(ticket, DEAL_TYPE); symbol = HistoryDealGetString(ticket, DEAL_SYMBOL); volume = HistoryDealGetDouble(ticket, DEAL_VOLUME); entry = HistoryDealGetInteger(ticket, DEAL_ENTRY); price = HistoryDealGetDouble(ticket, DEAL_PRICE); sl = HistoryDealGetDouble(ticket, DEAL_SL); tp = HistoryDealGetDouble(ticket, DEAL_TP); profit = HistoryDealGetDouble(ticket, DEAL_PROFIT); commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION); fee = HistoryDealGetDouble(ticket, DEAL_FEE); swap = HistoryDealGetDouble(ticket, DEAL_SWAP); magic = HistoryDealGetInteger(ticket, DEAL_MAGIC); comment = HistoryDealGetString(ticket, DEAL_COMMENT); if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL || type == DEAL_TYPE_BALANCE) { // Replace the separator characters in the comment with a space StringReplace(comment, s_sep, " "); // Form an array of values for writing one deal to the file string string fields[] = {TimeToString(time, TIME_DATE | TIME_MINUTES | TIME_SECONDS), IntegerToString(ticket), IntegerToString(type), symbol, DoubleToString(volume), IntegerToString(entry), DoubleToString(price, 5), DoubleToString(sl, 5), DoubleToString(tp, 5), DoubleToString(profit), DoubleToString(commission), DoubleToString(fee), DoubleToString(swap), IntegerToString(magic), comment }; // Set the values of a single deal to the file WriteDealsHistoryRow(fields); } } } }
WriteDealsHistoryRow()メソッドでは、渡された配列のすべての値を指定された区切り文字を使用して1つの文字列に結合し、開いているCSVファイルに書き込みます。接続には、Macros.mqhファイルのマクロコレクションに追加された新しいマクロJOINを使用しました。
//+------------------------------------------------------------------+ //| Write one row of deal history to the file | //+------------------------------------------------------------------+ void CExpertHistory::WriteDealsHistoryRow(const string &fields[]) { // Row to be set string row = ""; // Concatenate all array values into one row using a separator JOIN(fields, row, ","); // Write a row to the file FileWrite(s_file, row); }
現在のフォルダ内のExpertHistory.mqhファイルに変更を保存します。
ここで必要なのは、ファイルをEAファイルに接続し、CExpertHistory::Export()メソッドの呼び出しをOnTester()イベントハンドラに追加することだけです。
... #include "ExpertHistory.mqh" ... //+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { CExpertHistory::Export(); return expert.Tester(); }
変更をカレントディレクトリのSimpleVolumesExpert.mq5ファイルに保存します。
EAのテストを始めましょう。テストが完了すると、共有データフォルダに次の名前のファイルが表示されます。
SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv
名前から、取引履歴が2年間(2021年と2022年)をカバーし、開始口座残高が10,000米ドル、最終残高が34,518米ドルであることがわかります。テスト期間中、株式による最大相対ドローダウンは1,294米ドルで、シャープ比率は3.75でした。結果のファイルをExcelで開くと、次のようになります。
図4:取引履歴をCSVファイルにアンロードした結果
データは有効なようです。次に、CSVファイルを使用して別の口座で取引を再現できるEAの開発に移りましょう。
取引再現
取引戦略を作成して、新しいEAの実装を始めましょう。実際、いつ、どのポジションを建てるかについて他の人の指示に従うことも、取引戦略と呼ぶことができます。シグナルのソースが信頼できるものであれば、それを使用しない手はありません。したがって、CVirtualStrategyから継承した新しいクラスCHistoryStrategyを作成しましょう。メソッドに関しては、コンストラクタ、ティック処理メソッド、および文字列に変換するメソッドを実装する必要があります。最後のメソッドは必要ありませんが、このメソッドは親クラスでは抽象的であるため、継承のために存在する必要があります。
新しいクラスに次のプロパティを追加するだけです。
- m_symbols:銘柄名(取引商品)の配列
- m_history:取引履歴ファイルから読み取るための2次元配列(N行*15列)
- m_totalDeals:履歴内の取引数
- m_currentDeal:現在の取引インデックス
- m_symbolInfo:銘柄のプロパティに関するデータを取得するためのオブジェクト
//+------------------------------------------------------------------+ //| Trading strategy for reproducing the history of deals | //+------------------------------------------------------------------+ class CHistoryStrategy : public CVirtualStrategy { protected: string m_symbols[]; // Symbols (trading instruments) string m_history[][15]; // Array of deal history (N rows * 15 columns) int m_totalDeals; // Number of deals in history int m_currentDeal; // Current deal index CSymbolInfo m_symbolInfo; // Object for getting information about the symbol properties public: CHistoryStrategy(string p_params); // Constructor virtual void Tick() override; // OnTick event handler virtual string operator~() override; // Convert object to string };
戦略コンストラクタは、初期化文字列という1つの引数を受け入れる必要があります。この要件は継承からも生じます。初期化文字列には必要な値がすべてパックされる必要があります。コンストラクタは文字列からそれらを読み取り、必要に応じて使用します。この単純な戦略では、初期化文字列に1つの値(履歴ファイルの名前)を渡すだけで済みます。戦略に関するすべての追加データは履歴ファイルから取得されます。次に、コンストラクタを次のように実装します。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHistoryStrategy::CHistoryStrategy(string p_params) { m_params = p_params; // Read the file name from the parameters string fileName = ReadString(p_params); // If the name is read, then if(IsValid()) { // Attempting to open a file in the data folder int f = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ','); // If failed to open a file, then try to open the file from the shared folder if(f == INVALID_HANDLE) { f = FileOpen(fileName, FILE_COMMON | FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ','); } // If this does not work, report an error and exit if(f == INVALID_HANDLE) { SetInvalid(__FUNCTION__, StringFormat("ERROR: Can't open file %s from common folder %s, error code: %d", fileName, TerminalInfoString(TERMINAL_COMMONDATA_PATH), GetLastError())); return; } // Read the file up to the header string (usually it comes first) while(!FileIsEnding(f)) { string s = FileReadString(f); // If we find a header string, read the names of all columns without saving them if(s == "DATE") { FORI(14, FileReadString(f)); break; } } // Read the remaining rows until the end of the file while(!FileIsEnding(f)) { // If the array for storing the read history is filled, increase its size if(m_totalDeals == ArraySize(m_history)) { ArrayResize(m_history, ArraySize(m_history) + 10000, 100000); } // Read 15 values from the next file string into the array string FORI(15, m_history[m_totalDeals][i] = FileReadString(f)); // If the deal symbol is not empty, if(m_history[m_totalDeals][SYMBOL] != "") { // Add it to the symbol array if there is no such symbol there yet ADD(m_symbols, m_history[m_totalDeals][SYMBOL]); } // Increase the counter of read deals m_totalDeals++; } // Close the file FileClose(f); PrintFormat(__FUNCTION__" | OK: Found %d rows in %s", m_totalDeals, fileName); // If there are read deals except for the very first one (account top-up), then if(m_totalDeals > 1) { // Set the exact size for the history array ArrayResize(m_history, m_totalDeals); // Current time datetime ct = TimeCurrent(); PrintFormat(__FUNCTION__" |\n" "Start time in tester: %s\n" "Start time in history: %s", TimeToString(ct, TIME_DATE), m_history[0][DATE]); // If the test start date is greater than the history start date, then report an error if(StringToTime(m_history[0][DATE]) < ct) { SetInvalid(__FUNCTION__, StringFormat("ERROR: For this history file [%s] set start date less than %s", fileName, m_history[0][DATE])); } } // Create virtual positions for each symbol CVirtualReceiver::Get(GetPointer(this), m_orders, ArraySize(m_symbols)); // Register the event handler for a new bar on the minimum timeframe FOREACH(m_symbols, IsNewBar(m_symbols[i], PERIOD_M1)); } }
コンストラクタでは、初期化文字列からファイル名を読み取って、それを開こうとします。ファイルがローカルまたは共有データフォルダから正常に開かれた場合は、その内容を読み取り、m_history配列に入力します。読み取ると同時に、銘柄名のm_symbols配列も入力します。新しい名前が見つかるとすぐに、その名前を配列に追加します。これはADD()マクロによっておこなわれます。
途中で、m_totalDealsプロパティで読み取られた取引エントリの数をカウントします。これは、次の取引に関する情報を記録するために使用されるm_history配列の最初の次元のインデックスとして使用します。ファイルの内容がすべて読み取られたら、ファイルを閉じます。
次に、テスト開始日が履歴開始日より大きいかどうかを確認します。このような状況は許容できません。この場合、履歴の最初から一部の取引をモデル化することができなくなるためです。これにより、テスト中に取引結果が歪む可能性があります。したがって、取引履歴がテスト開始日より前に開始されていない場合にのみ、コンストラクタが有効なオブジェクトを作成できるようにします。
コンストラクタの重要なポイントは、履歴で遭遇した異なる銘柄名の数に厳密に従って仮想位置を割り当てることです。この戦略の目的は、各銘柄に必要なポジションのオープンボリュームを提供することであるため、銘柄ごとに1つの仮想ポジションのみを使用してこれを実行できます。
ティック処理メソッドは、読み取られた取引の配列でのみ機能します。一度に複数の銘柄を開いたり閉じたりする可能性があるため、現在の時間よりも長くない取引の履歴からすべての行を処理するループを配置します。残りの取引エントリは、現在の時間が増加し、時間がすでに到来している新しい取引が表示される次のティックで処理されます。
処理する必要がある取引が少なくとも1つ見つかった場合、まずm_symbols配列でその銘柄とインデックスを検索します。このインデックスを使用して、m_orders配列のどの仮想位置がこの銘柄を担当するかを決定します。何らかの理由でインデックスが見つからない場合(すべてが正しく動作している場合は、まだ発生しないはずです)、処理をスキップするだけです。口座の残高取引を反映する取引もスキップします。
ここから最も興味深い部分が始まります。読み取り取引を処理する必要があります。ここでは2つのケースが考えられます。この銘柄に建てられている仮想ポジションがないか、仮想ポジションが建てられているかです。
前者の場合、すべてが簡単です。適切な量で取引の方向にポジションを建てます。後者では、特定の銘柄の現在の位置のボリュームを増やすか減らす必要がある場合があります。さらに、ポジションの方向が変わるほど減らす必要がある場合もあります。
計算を簡単にするために、次の操作をおこないます。
- 新しい取引のボリュームを「符号付き」形式に変換します。つまり、売り方向だった場合は、そのボリュームをマイナスにします。
- 新しい銘柄と同じ銘柄のオープン取引のボリュームを取得します。CVirtualOrder::Volume()メソッドは、符号付き形式でボリュームをすぐに返します。
- すでに建てられているポジションのボリュームを新しいポジションのボリュームに追加します。新しい取引を考慮した後もオープンのままにしておくべき新しいボリュームを取得します。このボリュームも「符号付き」形式になります。
- 建てられている仮想ポジションをクローズします。
- 新しいボリュームがゼロでない場合は、銘柄の新しい仮想ポジションを建てます。新しいボリュームの符号によって方向を決定します(正-買い、負-売り)。新しいボリュームの係数は、ボリュームとして仮想ポジションオープンメソッドに渡されます。
この手順の後、履歴から処理された取引のカウンタを増やし、次のループ反復に進みます。この時点で処理する取引がもうない場合、または履歴内の取引が終了した場合は、ティック処理は完了です。
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CHistoryStrategy::Tick() override { //--- while(m_currentDeal < m_totalDeals && StringToTime(m_history[m_currentDeal][DATE]) <= TimeCurrent()) { // Deal symbol string symbol = m_history[m_currentDeal][SYMBOL]; // Find the index of the current deal symbol in the array of symbols int index; FIND(m_symbols, symbol, index); // If not found, then skip the current deal if(index == -1) { m_currentDeal++; continue; } // Deal type ENUM_DEAL_TYPE type = (ENUM_DEAL_TYPE) StringToInteger(m_history[m_currentDeal][TYPE]); // Current deal volume double volume = NormalizeDouble(StringToDouble(m_history[m_currentDeal][VOLUME]), 2); // If this is a top-up/withdrawal, skip the deal if(volume == 0) { m_currentDeal++; continue; } // Report information about the read deal PrintFormat(__FUNCTION__" | Process deal #%d: %s %.2f %s", m_currentDeal, (type == DEAL_TYPE_BUY ? "BUY" : (type == DEAL_TYPE_SELL ? "SELL" : EnumToString(type))), volume, symbol); // If this is a sell deal, then make the volume negative if(type == DEAL_TYPE_SELL) { volume *= -1; } // If the virtual position for the current deal symbol is open, if(m_orders[index].IsOpen()) { // Add its volume to the volume of the current trade volume += m_orders[index].Volume(); // Close the virtual position m_orders[index].Close(); } // If the volume for the current symbol is not 0, if(MathAbs(volume) > 0.00001) { // Open a virtual position of the required volume and direction m_orders[index].Open(symbol, (volume > 0 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), MathAbs(volume)); } // Increase the counter of handled deals m_currentDeal++; } }
取得したコードを現在のフォルダのHistoryStrategy.mqhファイルに保存します。
次に、既存のSimpleVolumesExpert.mq5に基づいてEAファイルを作成しましょう。望ましい結果を得るには、履歴を含むファイルの名前を指定できる入力をEAに追加する必要があります。
input group "::: Testing the deal history" input string historyFileName_ = ""; // File with history
データベースから戦略初期化文字列をロードするコードの部分は不要になったため、削除します。
初期化文字列で、CHistoryStrategyクラス戦略の単一インスタンスの作成を設定する必要があります。この戦略は、引数として履歴を含むファイル名を受け取ります。
// Prepare the initialization string for an EA with a group of several strategies string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " class CHistoryStrategy(\"%s\")\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f" " )\n" " ,%d,%s,%d\n" ")", historyFileName_, scale_, rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_, rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxOverallProfitDate_, rmMaxRestoreTime_, rmLastVirtualProfitFactor_, magic_, "HistoryReceiver", useOnlyNewBars_ );
これでEAファイルへの変更は完了です。現在のフォルダにHistoryReceiverExpert.mq5として保存します。
これで、取引履歴を再現できる実用的なEAが完成しました。実際のところ、その機能は多少広範囲にわたります。履歴内の取引は固定残高での取引に基づいて設定されているにもかかわらず、口座残高の増加に伴ってポジションの量を増やすと、取引結果がどのようになるか簡単にわかります。取引履歴が異なるリスクマネージャーパラメータで設定されている場合でも(またはリスクマネージャーが無効になっている場合でも)、異なるリスクマネージャーパラメータを適用して、取引への影響を評価できます。テスターを通過すると、取引履歴は自動的に新しいファイルに保存されます。
しかし、これらの追加機能がまだ必要なく、リスクマネージャーを使用したくなく、それに関連付けられた未使用の入力が気に入らない場合は、追加機能のない新しいEAクラスを作成できます。このクラスでは、ステータスの保存やチャート上の仮想ポジションを描画するためのインターフェース、そしてまだあまり使用されていないその他のものも削除できます。
このようなクラスを実装すると、次のようになります。
//+------------------------------------------------------------------+ //| Trade history replay EA class | //+------------------------------------------------------------------+ class CVirtualHistoryAdvisor : public CAdvisor { protected: CVirtualReceiver *m_receiver; // Receiver object that brings positions to the market bool m_useOnlyNewBar; // Handle only new bar ticks datetime m_fromDate; // Test start time public: CVirtualHistoryAdvisor(string p_param); // Constructor ~CVirtualHistoryAdvisor(); // Destructor virtual void Tick() override; // OnTick event handler virtual double Tester() override; // OnTester event handler virtual string operator~() override; // Convert object to string }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualHistoryAdvisor::CVirtualHistoryAdvisor(string p_params) { // Save the initialization string m_params = p_params; // Read the file name from the initialization string string fileName = ReadString(p_params); // Read the work flag only at the bar opening m_useOnlyNewBar = (bool) ReadLong(p_params); // If there are no read errors, if(IsValid()) { if(!MQLInfoInteger(MQL_TESTER)) { // Otherwise, set the object state to invalid SetInvalid(__FUNCTION__, "ERROR: This expert can run only in tester"); return; } if(fileName == "") { // Otherwise, set the object state to invalid SetInvalid(__FUNCTION__, "ERROR: Set file name with deals history in "); return; } string strategyParams = StringFormat("class CHistoryStrategy(\"%s\")", fileName); CREATE(CHistoryStrategy, strategy, strategyParams); Add(strategy); // Initialize the receiver with the static receiver m_receiver = CVirtualReceiver::Instance(65677); // Save the work (test) start time m_fromDate = TimeCurrent(); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualHistoryAdvisor::~CVirtualHistoryAdvisor() { if(!!m_receiver) delete m_receiver; // Remove the recipient DestroyNewBar(); // Remove the new bar tracking objects } //+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualHistoryAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes bool isNewBar = UpdateNewBar(); // If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { return; } // Start handling in strategies CAdvisor::Tick(); // Receiver handles virtual positions m_receiver.Tick(); // Adjusting market volumes m_receiver.Correct(); } //+------------------------------------------------------------------+ //| OnTester event handler | //+------------------------------------------------------------------+ double CVirtualHistoryAdvisor::Tester() { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // Fixed balance for trading from settings double fixedBalance = CMoney::FixedBalance(); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = fixedBalance * 0.1 / MathMax(1, balanceDrawdown); // Calculate the profit in annual terms long totalSeconds = TimeCurrent() - m_fromDate; double totalYears = totalSeconds / (365.0 * 24 * 3600); double fittedProfit = profit * coeff / totalYears; // If it is not specified, then take the initial balance (although this will give a distorted result) if(fixedBalance < 1) { fixedBalance = TesterStatistics(STAT_INITIAL_DEPOSIT); balanceDrawdown = TesterStatistics(STAT_EQUITY_DDREL_PERCENT); coeff = 0.1 / balanceDrawdown; fittedProfit = fixedBalance * MathPow(1 + profit * coeff / fixedBalance, 1 / totalYears); } return fittedProfit; } //+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CVirtualHistoryAdvisor::operator~() { return StringFormat("%s(%s)", typename(this), m_params); } //+------------------------------------------------------------------+
このクラスのEAは、初期化文字列で、履歴ファイルの名前と、分足バーの開始時にのみ動作するフラグの2つのパラメータのみを受け入れます。このコードを現在のフォルダのVirtualHistoryAdvisor.mqhファイルに保存します。
このクラスを使用するEAファイルも、以前のバージョンに比べて多少短縮できます。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Testing the deal history" input string historyFileName_ = ""; // File with history input group "::: Money management" sinput double fixedBalance_ = 10000; // - Used deposit (0 - use all) in the account currency input double scale_ = 1.00; // - Group scaling multiplier input group "::: Other parameters" input bool useOnlyNewBars_ = true; // - Work only at bar opening datetime fromDate = TimeCurrent(); // Operation start time CVirtualHistoryAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Set parameters in the money management class CMoney::DepoPart(scale_); CMoney::FixedBalance(fixedBalance_); // Prepare the initialization string for the deal history replay EA string expertParams = StringFormat( "class CVirtualHistoryAdvisor(\"%s\",%f,%d)", historyFileName_, useOnlyNewBars_ ); // Create an EA handling virtual positions expert = NEW(expertParams); // If the EA is not created, then return an error if(!expert) return INIT_FAILED; // Successful initialization return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(!!expert) delete expert; } //+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { return expert.Tester(); } //+------------------------------------------------------------------+
このコードを現在のフォルダのSimpleHistoryReceiverExpert.mq5ファイルに保存します。
テスト結果
保存された取引履歴を含むファイルの正しい名前を指定して、作成されたEAの1つを起動してみましょう。まず、履歴を取得するために使用されたのと同じクォートサーバー(MetaQuotes-Demo)で起動してみましょう。得られたテスト結果は元の結果と完全に一致しました。これは予想外にも良い結果であり、計画が正しく実行されたことを示していると言えるでしょう。
次に、別のサーバーでEAを実行すると何が起こるかを見てみましょう。
図5:他のブローカーのリアルサーバーのクォートで取引履歴を再現した結果
残高曲線グラフは、MetaQuotes-Demoの初期取引結果のチャートとほとんど区別がつきません。ただし、数値は若干異なります。比較のために、元の値をもう一度見てみましょう。
図6:MetaQuotes-Demoサーバーのクォートの初期テスト結果
総利益と正規化平均年間利益、シャープレシオがわずかに減少し、ドローダウンがわずかに増加しています。ただし、これらの結果は、別のブローカーの実際のサーバーのクォートでEAを実行したときに最初に見た全預金の損失とは比較できません。これは非常に励みになり、EAを実際の取クォートに準備する際に解決しなければならない新しいタスクの層が開かれます。
結論
そろそろ暫定的な結論をまとめる時です。特定の取引戦略を使用する場合、クォートサーバーを変更すると非常に深刻な影響を及ぼす可能性があることを確認できました。しかし、そのような挙動の理由を理解した上で、ポジションを建てるためのシグナルのロジックを元のクォートサーバーに残し、実際のポジションのオープンおよびクローズ操作のみを新しいサーバーに渡すことで、取引結果を再び比較可能にできることも示しました。
この方法を実現するために、テスト後に取引履歴を保存し、その履歴をもとに取引を再現できる2つの新しいツールを開発しました。ただし、これらのツールはテスターでのみ使用可能であり、実際の取引には適用できません。しかし、テスト結果からこのアプローチの有効性が確認されたため、実の取引においても、EA間の責務をこのように分担する実装を進めることができます。
具体的には、EAを2つの独立したものに分割する必要があります。1つ目のEAは、最も適したクォートサーバー上で動作し、ポジションを建てるべきかを判断して実行します。同時に、ポジションのリストを2つ目のEAが受け取れる形式で送信する必要があります。2つ目のEAは、別の端末で動作し、必要に応じて異なるクォートサーバーに接続されます。そして、1つ目のEAが送信したデータに基づき、対応するポジションの量を常に維持します。これにより、この記事の冒頭で特定した制限を回避することができます。
さらにこの仕組みを発展させることも可能です。前述の方法では、両方の端末が1台のコンピューター上で動作することを前提としていますが、必ずしもそうする必要はありません。それぞれ異なるコンピューター上で動作させることもできます。重要なのは、1つ目のEAが特定の通信チャネルを通じてポジション情報を2つ目のEAに渡せることです。もちろん、この方法では、エントリーのタイミングや価格の厳密な遵守が求められる取引戦略には適していません。しかし、当初の目的は、エントリーの精度がそれほど厳密でない戦略の運用にありました。そのため、こうしたシステムを構築する際には、通信チャネルの遅延が問題にならないようにすることが重要です。
とはいえ、あまり先走るのは控えましょう。次回の記事では、この方向性に沿ってさらに体系的に検討を進めていきます。
ご精読ありがとうございました。またすぐにお会いしましょう。
アーカイブ内容
# | 名前 | バージョン | 詳細 | 最近の変更 | |
---|---|---|---|---|---|
MQL5/Experts/Article.15330 | |||||
1 | Advisor.mqh | 1.04 | EA基本クラス | 第10回 | |
2 | Database.mqh | 1.03 | データベースを扱うクラス | 第13回 | |
3 | ExpertHistory.mqh | 1.00 | 取引履歴をファイルにエクスポートするクラス | 第16回 | |
4 | Factorable.mqh | 1.01 | 文字列から作成されたオブジェクトの基本クラス | 第10回 | |
5 | HistoryReceiverExpert.mq5 | 1.00 | リスクマネージャーとの取引履歴を再生するためのEA | 第16回 | |
6 | HistoryStrategy.mqh | 1.00 | 取引履歴を再生するための取引戦略のクラス | 第16回 | |
7 | Interface.mqh | 1.00 | さまざまなオブジェクトを視覚化するための基本クラス | 第4回 | |
8 | Macros.mqh | 1.02 | 配列操作に便利なマクロ | 第16回 | |
9 | Money.mqh | 1.01 | 基本的なお金の管理クラス | 第12回 | |
10 | NewBarEvent.mqh | 1.00 | 特定の銘柄の新しいバーを定義するクラス | 第8回 | |
11 | Receiver.mqh | 1.04 | オープンボリュームを市場ポジションに変換するための基本クラス | 第12回 | |
12 | SimpleHistoryReceiverExpert.mq5 | 1.00 | 取引履歴を再生するための簡易EA | 第16回 | |
13 | SimpleVolumesExpert.mq5 | 1.19 | 複数のモデル戦略グループを並列操作するためのEA。パラメータは最適化データベースからロードする必要があります。 | 第16回 | |
14 | SimpleVolumesStrategy.mqh | 1.09 | ティックボリュームを使用した取引戦略のクラス | 第15回 | |
15 | Strategy.mqh | 1.04 | 取引戦略基本クラス | 第10回 | |
16 | TesterHandler.mqh | 1.02 | 最適化イベント処理クラス | 第13回 | |
17 | VirtualAdvisor.mqh | 1.06 | 仮想ポジション(注文)を扱うEAのクラス | 第15回 | |
18 | VirtualChartOrder.mqh | 1.00 | グラフィカル仮想位置クラス | 第4回 | |
19 | VirtualFactory.mqh | 1.04 | オブジェクトファクトリクラス | 第16回 | |
20 | VirtualHistoryAdvisor.mqh | 1.00 | トレード履歴再生EAクラス | 第16回 | |
21 | VirtualInterface.mqh | 1.00 | EAGUIクラス | 第4回 | |
22 | VirtualOrder.mqh | 1.04 | 仮想注文とポジションのクラス | 第8回 | |
23 | VirtualReceiver.mqh | 1.03 | オープンボリュームを市場ポジションに変換するクラス(レシーバー) | 第12回 | |
24 | VirtualRiskManager.mqh | 1.02 | リスクマネジメントクラス(リスクマネージャー) | 第15回 | |
25 | VirtualStrategy.mqh | 1.05 | 仮想ポジションを使った取引戦略のクラス | 第15回 | |
26 | VirtualStrategyGroup.mqh | 1.00 | 取引戦略グループのクラス | 第11回 | |
27 | VirtualSymbolReceiver.mqh | 1.00 | 銘柄レシーバークラス | 第3回 | |
MQL5/Files | |||||
1 | SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv | エクスポート後に取得されたSimpleVolumesExpert.mq5 EA取引の履歴。SimpleHistoryReceiverExpert.mq5またはHistoryReceiverExpert.mq5 EAを使用してテスターで取引を再生するために使用できます。 |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/15330




- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索