
多通貨エキスパートアドバイザーの開発(第9回):単一取引戦略インスタンスの最適化結果の収集
はじめに
これまでの記事で、すでに多くの興味深いことを実装してきました。複数の取引戦略をEAに組み込む方法や、1つのEAで複数の取引戦略インスタンスを接続するための構造も開発しました。また、最大許容ドローダウンを管理するツールを追加し、最適なパフォーマンスを発揮する戦略パラメータセットを自動選択する方法についても検討しました。さらに、異なる戦略インスタンスのグループからEAを組み立てる方法について学びました。これらの結果をうまく組み合わせれば、得られる価値はさらに高まるでしょう。
この記事では、一般的なEA開発の構造を概説します。単一の取引戦略を入力とし、出力では最良の取引結果を提供する元の取引戦略の最適化されたインスタンスが選ばれ、それを複数にコピーして使用する既製のEAが完成します。
この大まかなロードマップを作成した後、個々のセクションをより詳細に分析し、選択した段階を実装するために必要な要素を検討します。最後に、実際の実装に取りかかる準備を整えましょう。
主な段階
EAを開発する際に通過しなければならない主な段階を挙げてみましょう。
- 取引戦略を実行する:CVirtualStrategyから派生したクラスを開発し、仮想ポジションと注文をオープン、維持、クローズする取引ロジックを実装します。これは本連載の最初の4回でおこないました。
- 取引戦略の最適化:注目すべき結果を示す取引戦略のための優れた入力セットを選択します。見つからなければ、ステップ1に戻ります。
原則として、1つの銘柄と時間枠で最適化をおこなう方が便利です。遺伝的最適化の場合、独自の最適化基準も含めて、異なる最適化基準で数回実行する必要があるでしょう。総当たり攻撃最適化を使えるのは、パラメータの数が非常に少ない戦略だけです。私たちのモデル戦略でも、徹底的な探索はコストがかかりすぎます。したがって、最適化について話しながら、さらに続けます。
MetaTrader 5のストラテジーテスターで遺伝的最適化をおこないます。最適化プロセスはごく普通のことなので、記事には詳しく書かれていません。 - 集合のクラスタリング:このステップは必須ではありませんが、次のステップで時間を節約できます。ここでは、取引戦略インスタンスのパラメータセットの数を大幅に減らし、その中から適切なグループを選択します。これについては第6部で説明します。
- パラメータセットグループの選択:前段階の結果に基づき、最適化によって
最良の結果を生む取引戦略インスタンスの最も適合性の高いパラメータセットを選択します。これも主に第6部と第7部に記述されています。 - パラメータセットグループからのグループを選択:ここで、シングルインスタンスパラメータセットを組み合わせるときと同じ原理で、前段階の結果をグループにまとめます。
- 銘柄と時間枠の反復:希望するすべての銘柄と時間枠について、ステップ2~5を繰り返します。おそらく、銘柄と時間枠に加えて、いくつかの取引戦略について、他のインプットの特定のクラスについて個別に最適化を実施することが可能です。
- その他の戦略:他の取引戦略を考えている場合は、それぞれについてステップ1~6を繰り返します。
- EAの組み立て:さまざまな取引戦略、銘柄、時間枠、その他のパラメータに対して発見されたすべてのベストグループを最終的な1つのEAに集めます。
各段階が完了すると、保存して次の段階で使用する必要のあるデータが生成されます。これまでのところ、一時的な即席の手段を使ってきました。一度や二度使うには便利ですが、繰り返し使うには特に便利ではありません。
例えば、第2段階終了後の最適化結果をExcelファイルに保存し、不足している列を手作業で追加し、CSVファイルとして保存した後、第3段階で使用しました。
第3段階の結果は、ストラテジーテスターのインターフェイスから直接使用するか、あるいはエクセルファイルに保存し、そこで何らかの処理を施し、再びテスターインターフェイスから得られた結果を使用しました。
第5段階は実際に実行したわけではなく、実行する可能性に注目しただけです。そのため、実現することはありませんでした。
これらすべての受信データについて、単一の保存と使用構造を実装したいと思います。
実装オプション
基本的に、保存して使用する必要があるデータの主な種類は、複数のEAの最適化結果です。ご承知のように、ストラテジーテスターはすべての最適化結果を*.opt拡張子の別個のキャッシュファイルに記録し、テスターで再度開いたり、別のMetaTrader 5端末のテスターで開くこともできます。ファイル名は、最適化されたEAの名前と最適化パラメータに基づいて計算されたハッシュから決定されます。これにより、最適化が早期に中断された後や最適化基準を変更した後に最適化を継続する際に、既におこなわれたパスに関する情報を失うことがありません。
そのため、中間結果を保存するための最適化キャッシュファイルの使用が検討されています。fxsaberの優れたライブラリがあり、MQL5プログラムから保存されたすべての情報にアクセスできます。
しかし、実行される最適化の数が増えれば、その結果を含むファイルの数も増えます。混乱しないようにするためには、ストレージの配置やキャッシュファイルを扱うための構造を追加する必要があります。最適化が1つのサーバーでおこなわれない場合は、同期化を実施するか、すべてのキャッシュファイルを1つの場所に保存する必要があります。さらに、次の段階では、得られた最適化結果をEAにエクスポートするための処理が必要になります。
次に、すべての結果をデータベースに保存する方法を考えてみましょう。一見すると、これを実装するにはかなりの時間を要するでしょう。しかし、この作業はより小さな段階に分けることができ、完全な実装を待つことなく、その結果をすぐに使うことができるでしょう。このアプローチはまた、保存された結果の中間処理に最も便利な手段を選ぶ自由を与えてくれます。例えば、単純なSQLクエリに処理を割り当て、MQL5で計算し、PythonやRプログラムで処理します。さまざまな処理オプションを試し、最も適したものを選ぶことができるでしょう。
MQL5にはSQLiteデータベースを操作するための組み込み関数が用意されています。また、例えばMySQLと連携できるサードパーティライブラリの実装もありました。SQLiteの機能で十分かどうかはまだわかりませんが、おそらくこのデータベースで必要十分でしょう。もし十分でなければ、他のDBMSへの移行を考えることになります。
データベースの設計を始める
まず、保存したい情報の主体を特定する必要があります。もちろん、1回テストを実行することもその1つです。このエンティティのフィールドには、テスト入力データフィールドとテスト結果フィールドが含まれます。一般的に、両者は別個の存在として区別することができます。入力データの本質は、EA、最適化設定、EAシングルパスパラメータという、さらに小さなエンティティに分解することができます。しかし、最小行動の原則に導かれ続けましょう。まず始めに、以前の記事で使用したパス結果用のフィールドを持つテーブルと、パス入力に関する必要な情報を配置するための1つか2つのテキストフィールドがあれば十分です。
このようなテーブルは以下のSQLクエリで作成できます。
CREATE TABLE passes ( id INTEGER PRIMARY KEY AUTOINCREMENT, pass INT, -- pass index inputs TEXT, -- pass input values params TEXT, -- additional pass data initial_deposit REAL, -- pass results... withdrawal REAL, profit REAL, gross_profit REAL, gross_loss REAL, max_profittrade REAL, max_losstrade REAL, conprofitmax REAL, conprofitmax_trades REAL, max_conwins REAL, max_conprofit_trades REAL, conlossmax REAL, conlossmax_trades REAL, max_conlosses REAL, max_conloss_trades REAL, balancemin REAL, balance_dd REAL, balancedd_percent REAL, balance_ddrel_percent REAL, balance_dd_relative REAL, equitymin REAL, equity_dd REAL, equitydd_percent REAL, equity_ddrel_percent REAL, equity_dd_relative REAL, expected_payoff REAL, profit_factor REAL, recovery_factor REAL, sharpe_ratio REAL, min_marginlevel REAL, deals REAL, trades REAL, profit_trades REAL, loss_trades REAL, short_trades REAL, long_trades REAL, profit_shorttrades REAL, profit_longtrades REAL, profittrades_avgcon REAL, losstrades_avgcon REAL, complex_criterion REAL, custom_ontester REAL, pass_date DATETIME DEFAULT (datetime('now') ) NOT NULL );
データベースを操作するためのメソッドを含む補助的なCDatabaseクラスを作成しましょう。1つのプログラムに多くのインスタンスは必要ないので、静的なインスタンスにします。現在のところ、すべての情報を1つのデータベースに蓄積する予定なので、ソースコードでデータベースファイル名を厳格に指定することができます。
このクラスには、オープンデータベースハンドルを格納するためのs_dbフィールドが含まれます。Open()データベースを開くメソッドが、この値を設定します。オープン時にデータベースがまだ作成されていない場合は、Create()メソッドを呼び出して作成します。いったん開けば、Execute()メソッドを使用してデータベースに対して単一のSQLクエリを実行したり、ExecuteTransaction()メソッドを使用して単一のトランザクションで一括SQLクエリを実行したりできます。最後に、Close()メソッドを使用してデータベースを閉じます。
長いCDatabaseクラス名を短いDBに置き換える短いマクロを宣言することもできます。
#define DB CDatabase //+------------------------------------------------------------------+ //| Class for handling the database | //+------------------------------------------------------------------+ class CDatabase { static int s_db; // DB connection handle static string s_fileName; // DB file name public: static bool IsOpen(); // Is the DB open? static void Create(); // Create an empty DB static void Open(); // Opening DB static void Close(); // Closing DB // Execute one query to the DB static bool Execute(string &query); // Execute multiple DB queries in one transaction static bool ExecuteTransaction(string &queries[]); }; int CDatabase::s_db = INVALID_HANDLE; string CDatabase::s_fileName = "database.sqlite";
データベース作成メソッドでは、テーブルを作成するためのSQLクエリで配列を作成し、1つのトランザクションで実行するだけです。
//+------------------------------------------------------------------+ //| Create an empty DB | //+------------------------------------------------------------------+ void CDatabase::Create() { // Array of DB creation requests string queries[] = { "DROP TABLE IF EXISTS passes;", "CREATE TABLE passes (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "pass INT," "inputs TEXT," "params TEXT," "initial_deposit REAL," "withdrawal REAL," "profit REAL," "gross_profit REAL," "gross_loss REAL," ... "pass_date DATETIME DEFAULT (datetime('now') ) NOT NULL" ");" , }; // Execute all requests ExecuteTransaction(queries); }
データベースを開くメソッドでは、まず既存のデータベースファイルを開いてみます。データベースが存在しない場合は、それを作成して開き、Create()メソッドを呼び出してデータベース構造を作成します。
//+------------------------------------------------------------------+ //| Is the DB open? | //+------------------------------------------------------------------+ bool CDatabase::IsOpen() { return (s_db != INVALID_HANDLE); } ... //+------------------------------------------------------------------+ //| Open DB | //+------------------------------------------------------------------+ void CDatabase::Open() { // Try to open an existing DB file s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_COMMON); // If the DB file is not found, try to create it when opening if(!IsOpen()) { s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); // Report an error in case of failure if(!IsOpen()) { PrintFormat(__FUNCTION__" | ERROR: %s open failed with code %d", s_fileName, GetLastError()); return; } // Create the database structure Create(); } PrintFormat(__FUNCTION__" | Database %s opened successfully", s_fileName); }
複数のExecuteTransaction()クエリを実行するメソッドでは、トランザクションを作成し、すべてのSQLクエリを1つずつループで実行します。次のリクエストの実行中にエラーが発生した場合、ループを中断し、エラーを報告し、このトランザクショ ン内の以前のリクエストをすべてキャンセルします。エラーが発生しなければ、トランザクションを確認します。
//+------------------------------------------------------------------+ //| Execute multiple DB queries in one transaction | //+------------------------------------------------------------------+ bool CDatabase::ExecuteTransaction(string &queries[]) { // Open a transaction DatabaseTransactionBegin(s_db); bool res = true; // Send all execution requests FOREACH(queries, { res &= Execute(queries[i]); if(!res) break; }); // If an error occurred in any request, then if(!res) { // Report it PrintFormat(__FUNCTION__" | ERROR: Transaction failed, error code=%d", GetLastError()); // Cancel transaction DatabaseTransactionRollback(s_db); } else { // Otherwise, confirm transaction DatabaseTransactionCommit(s_db); PrintFormat(__FUNCTION__" | Transaction done successfully"); } return res; }
現在のフォルダのDatabase.mqhファイルに変更を保存します。
最適化データを収集するためにEAを修正する
最適化プロセスでローカルコンピュータ上のエージェントのみを使用する場合、OnTester()またはOnDeinit()ハンドラで、パス結果をデータベースに保存するように設定できます。ローカルネットワークやMQL5クラウドネットワークでエージェントを使用する場合、可能であれば、結果を保存することは非常に困難です。幸いなことに、MQL5は、データフレームを作成、送受信することで、テストエージェントがどこにいても、あらゆる情報を取得できる優れた標準的な方法を提供しています。
このメカニズムについては、参考文献やアルゴブックに十分詳しく書かれています。これを使うには、OnTesterInit()、OnTesterPass()、OnTesterDeinit()の最適化されたイベントハンドラを3つ追加する必要があります。
最適化は常にMetaTrader 5端末から起動されます。このようなハンドラを持つEAが最適化のためにメイン端末から起動されると、メイン端末で新しいチャートが開かれ、EAの別のインスタンスがこのチャート上で起動された後、EAインスタンスがテストエージェントに配布され、異なるパラメータセットで通常の最適化パスが実行されます。
このインスタンスは特別なモードで起動され、標準のOnInit()、OnTick()、OnDeinit()ハンドラは実行されません。代わりに実行されるのは、これら3つの新しいハンドラだけです。このモードには「最適化結果のフレームを収集するモード」という名前もあります。必要であれば、以下の方法でMQLInfoInteger()関数を呼び出すことで、EA関数内でEAがこのモードで実行されていることを確認できます。
// Check if the EA is running in data frame collection mode bool isFrameMode = MQLInfoInteger(MQL_FRAME_MODE);
名前が示すように、フレームコレクションモードでは、OnTesterInit()ハンドラは最適化の前に1回実行され、OnTesterPass()はテストエージェントのいずれかがパスを完了するたびに実行され、OnTesterDeinit()はスケジュールされた最適化パスがすべて完了した後、または最適化が中断されたときに1回実行されます。
フレーム収集モードでメイン端末チャート上に起動されたEAインスタンスは、すべてのテストエージェントからデータフレームを収集する責任を負います。「データフレーム」とは、テストエージェントとメイン端末のEAとの間のデータ交換を説明する際に使用する便利な名前に過ぎません。これは、テストエージェントが作成し、1回の最適化パスの完了後にメイン端末に送信した、名前と数値IDを持つデータセットを示します。
データフレームを作成するのは、テストエージェントの通常モードで動作するEAインスタンスのみであり、データフレームを収集して処理するのは、フレーム収集モードで動作するメイン端末のEAインスタンスのみであることに留意すべきです。では、フレームの作成から始めましょう。
EAにおけるフレームの作成は、OnTester()ハンドラ内、あるいはOnTester()から呼び出される関数やメソッド内に置くことができます。ハンドラはパス完了後に起動され、完了したパスのすべての統計的特性の値を取得し、必要に応じて、パス結果を評価するためのユーザー基準の値を計算することができます。
現在、達成可能な最大ドローダウンが10%である場合に得られるであろう予測利益を示すカスタム基準を計算するコードを持っています。
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = fixedBalance_ * 0.1 / balanceDrawdown; // Recalculate the profit double fittedProfit = profit * coeff; return fittedProfit; }
このコードをSimpleVolumesExpertSingle.mq5 EAファイルから新しいCVirtualAdvisorメソッドクラスに移動しましょう:
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { return expert.Tester(); }
移動する際には、メソッド内部でfixedBalance_変数を使用できなくなることを考慮する必要があります。しかし、その値は、CMoney::FixedBalance()メソッドを呼び出すことで、CMoney静的クラスから取得できます。その過程で、ユーザー基準の計算にもうひとつ変更を加える。予想利益を決定した後、単位時間当たり、例えば1年当たりの利益を再計算します。これによって、異なる期間のパスの結果を大まかに比較することができます。
そのためには、EAのテスト開始日を覚えておく必要があります。EAオブジェクトのコンストラクタに、現在時刻を格納するための新しいプロパティm_fromDateを追加しましょう。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... datetime m_fromDate; public: ... virtual double Tester() override; // OnTester event handler ... }; //+------------------------------------------------------------------+ //| OnTester event handler | //+------------------------------------------------------------------+ double CVirtualAdvisor::Tester() { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown; // Calculate the profit in annual terms long totalSeconds = TimeCurrent() - m_fromDate; double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ; // Perform data frame generation on the test agent CTesterHandler::Tester(fittedProfit, ~((CVirtualStrategy *) m_strategies[0])); return fittedProfit; }
その後、いくつかのカスタム最適化基準を作り、このコードをまた新しい場所に移すかもしれません。しかし今は、EAを最適化するためのさまざまなフィットネス関数を研究するという広範な話題に気を取られず、コードはそのままにしておこう。
SimpleVolumesExpertSingle.mq5 EAファイルに新しいハンドラOnTesterInit()、OnTesterPass()、OnTesterDeinit()が追加されました。我々の計画では、これらの関数のロジックはすべてのEAで同じであるべきなので、まずその実装をEAレベル(CVirtualAdvisorクラスオブジェクト)に下げます。
メイン端末でフレーム収集モードでEAを起動した場合、EAインスタンスを生成するOnInit()関数は実行されないことを考慮する必要があります。したがって、EAインスタンスの生成/削除を新しいハンドラに追加しないために、これらのイベントを処理するメソッドをCVirtualAdvisorクラスの中で静的にします。次に、以下のコードをEAに追加する必要があります。
//+------------------------------------------------------------------+ //| Initialization before starting optimization | //+------------------------------------------------------------------+ int OnTesterInit(void) { return CVirtualAdvisor::TesterInit(); } //+------------------------------------------------------------------+ //| Actions after completing the next optimization pass | //+------------------------------------------------------------------+ void OnTesterPass() { CVirtualAdvisor::TesterPass(); } //+------------------------------------------------------------------+ //| Actions after optimization is complete | //+------------------------------------------------------------------+ void OnTesterDeinit(void) { CVirtualAdvisor::TesterDeinit(); }
将来のためのもう1つの変更は、EAが作成された後に取引戦略を EAに追加するためのCVirtualAdvisor::Add()メソッドへの個別の呼び出しを取り除くことです。その代わりに、戦略の情報を直ちにEAのコンストラクタに転送し、その間にEAが独自にAdd()メソッドを呼び出します。そうすれば、このメソッドはpublic部分から外すことができます。
この方法では、OnInit()EA初期化関数は以下のようになります。
int OnInit() { CMoney::FixedBalance(fixedBalance_); // Create an EA handling virtual positions expert = new CVirtualAdvisor( new CSimpleVolumesStrategy( symbol_, timeframe_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_, 0), // One strategy instance magic_, "SimpleVolumesSingle", true); return(INIT_SUCCEEDED); }
現在のフォルダのSimpleVolumesExpertSingle.mq5ファイルに変更を保存します。
EAクラスの変更
CVirtualAdvisor EAクラスのオーバーロードを避けるために、TesterInit、TesterPass、OnTesterDeinitイベントハンドラのコードを別のCTesterHandlerクラスに移しましょう。この場合、CVirtualAdvisorクラスにメインのEAファイルとほぼ同じコードを追加する必要があります。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { ... public: ... static int TesterInit(); // OnTesterInit event handler static void TesterPass(); // OnTesterDeinit event handler static void TesterDeinit(); // OnTesterDeinit event handler }; //+------------------------------------------------------------------+ //| Initialization before starting optimization | //+------------------------------------------------------------------+ int CVirtualAdvisor::TesterInit() { return CTesterHandler::TesterInit(); } //+------------------------------------------------------------------+ //| Actions after completing the next optimization pass | //+------------------------------------------------------------------+ void CVirtualAdvisor::TesterPass() { CTesterHandler::TesterPass(); } //+------------------------------------------------------------------+ //| Actions after optimization is complete | //+------------------------------------------------------------------+ void CVirtualAdvisor::TesterDeinit() { CTesterHandler::TesterDeinit(); }
EAオブジェクトのコンストラクタのコードも少し追加してみましょう。将来の改良を考慮して、コンストラクタからすべてのアクションを新しいInit()初期化メソッドに移しました。これにより、パラメータを少し前処理すれば、異なるパラメータセットを持つ複数のコンストラクタを追加して、すべて同じ初期化メソッドを使用できるようになります。
第一引数に戦略オブジェクトか戦略グループオブジェクトをとるコンストラクタを追加しましょう。そうすれば、コンストラクタで直接EAに戦略を追加できます。この場合、OnInit() EA関数でAdd()メソッドを呼び出す必要はありません。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... datetime m_fromDate; public: CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ); ... }; ... //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ) { CVirtualStrategy *strategies[] = {p_strategy}; Init(new CVirtualStrategyGroup(strategies), p_magic, p_name, p_useOnlyNewBar); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ) { Init(p_group, p_magic, p_name, p_useOnlyNewBar); }; //+------------------------------------------------------------------+ //| EA initialization method | //+------------------------------------------------------------------+ void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ) { // Initialize the receiver with a static receiver m_receiver = CVirtualReceiver::Instance(p_magic); // Initialize the interface with the static interface m_interface = CVirtualInterface::Instance(p_magic); m_lastSaveTime = 0; m_useOnlyNewBar = p_useOnlyNewBar; m_name = StringFormat("%s-%d%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); m_fromDate = TimeCurrent(); Add(p_group); delete p_group; };
現在のフォルダのVirtualExpert.mqhに変更を保存します。
最適化イベント処理クラス
ここでは、開始前、パス完了後、最適化完了後に実行されるアクションの実装に直接焦点を当ててみましょう。CTesterHandlerクラスを作り、必要なイベントを処理するためのメソッドと、クラスの閉じた部分に置かれたいくつかの補助メソッドを追加します。
//+------------------------------------------------------------------+ //| Optimization event handling class | //+------------------------------------------------------------------+ class CTesterHandler { static string s_fileName; // File name for writing frame data static void ProcessFrames(); // Handle incoming frames static string GetFrameInputs(ulong pass); // Get pass inputs public: static int TesterInit(); // Handle the optimization start in the main terminal static void TesterDeinit(); // Handle the optimization completion in the main terminal static void TesterPass(); // Handle the completion of a pass on an agent in the main terminal static void Tester(const double OnTesterValue, const string params); // Handle completion of tester pass for agent }; string CTesterHandler::s_fileName = "data.bin"; // File name for writing frame data
メイン端末のイベントハンドラは非常にシンプルに見えます。
//+------------------------------------------------------------------+ //| Handling the optimization start in the main terminal | //+------------------------------------------------------------------+ int CTesterHandler::TesterInit(void) { // Open / create a database DB::Open(); // If failed to open it, we do not start optimization if(!DB::IsOpen()) { return INIT_FAILED; } // Close a successfully opened database DB::Close(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Handling the optimization completion in the main terminal | //+------------------------------------------------------------------+ void CTesterHandler::TesterDeinit(void) { // Handle the latest data frames received from agents ProcessFrames(); // Close the chart with the EA running in frame collection mode ChartClose(); } //+--------------------------------------------------------------------+ //| Handling the completion of a pass on an agent in the main terminal | //+--------------------------------------------------------------------+ void CTesterHandler::TesterPass(void) { // Handle data frames received from the agent ProcessFrames(); }
1つのパスが完了した後に実行されるアクションは、2つのバージョンで存在することになります。
- テストエージェント用:そこでは、通過後に必要な情報が収集され、メイン端末に送信するためのデータフレームが作成されます。これらのアクションはTester()イベントハンドラに集められます。
- メイン端末用:ここでは、テストエージェントからデータフレームを受け取り、フレームで受け取った情報を解析し、データベースに入力することができます。これらのアクションはTesterPass()ハンドラに集められます。
テストエージェントのデータフレームの生成は、EA内、つまりOnTesterハンドラ内で実行する必要があります。そのコードを EAオブジェクトレベル(CVirtualAdvisorクラス)に移動したので、ここでCTesterHandler::Tester()メソッドを追加する必要があります。カスタム最適化基準の新しく計算された値と、最適化されたEAで使用された戦略のパラメータを記述した文字列を、メソッドのパラメータとして渡します。このような文字列を形成するために、CVirtualStrategyクラスのオブジェクトには、すでに作成されている~(チルダ)を使用します。
//+------------------------------------------------------------------+ //| OnTester event handler | //+------------------------------------------------------------------+ double CVirtualAdvisor::Tester() { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown; // Calculate the profit in annual terms long totalSeconds = TimeCurrent() - m_fromDate; double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ; // Perform data frame generation on the test agent CTesterHandler::Tester(fittedProfit, ~((CVirtualStrategy *) m_strategies[0])); return fittedProfit; }
CTesterHandler::Tester()メソッド自体で、利用可能な統計的特徴の可能な名前をすべて調べ、その値を取得し、文字列に変換し、これらの文字列をstats配列に追加します。なぜ実数値を文字列に変換する必要があったのでしょうか。戦略のパラメータを文字列で記述して、1つのフレームで渡せるようにするためです。1つのフレームに、単純な型(文字列は該当しない)の値の配列か、任意のデータを含むあらかじめ作成されたファイルを渡すことができます。そこで、2つの異なるフレーム(1つは数字、もう1つはファイルからの文字列)を送信する手間を省くため、すべてのデータを文字列に変換してファイルに書き込み、その内容を1つのフレームにまとめて送信します。
//+------------------------------------------------------------------+ //| Handling completion of tester pass for agent | //+------------------------------------------------------------------+ void CTesterHandler::Tester(double custom, // Custom criteria string params // Description of EA parameters in the current pass ) { // Array of names of saved statistical characteristics of the pass ENUM_STATISTICS statNames[] = { STAT_INITIAL_DEPOSIT, STAT_WITHDRAWAL, STAT_PROFIT, ... }; // Array for values of statistical characteristics of the pass as strings string stats[]; ArrayResize(stats, ArraySize(statNames)); // Fill the array of values of statistical characteristics of the pass FOREACH(statNames, stats[i] = DoubleToString(TesterStatistics(statNames[i]), 2)); // Add the custom criterion value to it APPEND(stats, DoubleToString(custom, 2)); // Screen the quotes in the description of parameters just in case StringReplace(params, "'", "\\'"); // Open the file to write data for the frame int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI); // Write statistical characteristics FOREACH(stats, FileWriteString(f, stats[i] + ",")); // Write a description of the EA parameters FileWriteString(f, StringFormat("'%s'", params)); // Close the file FileClose(f); // Create a frame with data from the recorded file and send it to the main terminal if(!FrameAdd("", 0, 0, s_fileName)) { PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError()); } }
最後に、データフレームを受け取り、その情報をデータベースに保存する補助メソッドを考えてみましょう。このメソッドでは、現時点でまだ処理されていないすべての受信フレームをループで受け取ります。各フレームから文字配列の形でデータを取得し、文字列に変換します。次に、指定されたインデックスのパスのパラメータの名前と値を文字列にします。得られた値を使ってSQLクエリを作成し、データベースのpassesテーブルに新しい行を挿入します。作成したSQLクエリをSQLクエリ配列に追加します。
このようにして現在受信しているすべてのデータフレームを処理した後、単一のトランザクション内で配列からすべてのSQLクエリを実行します。
//+------------------------------------------------------------------+ //| Handling incoming frames | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrames(void) { // Open the database DB::Open(); // Variables for reading data from frames string name; // Frame name (not used) ulong pass; // Frame pass index long id; // Frame type ID (not used) double value; // Single frame value (not used) uchar data[]; // Frame data array as a character array string values; // Frame data as a string string inputs; // String with names and values of pass parameters string query; // A single SQL query string string queries[]; // SQL queries for adding records to the database // Go through frames and read data from them while(FrameNext(pass, name, id, value, data)) { // Convert the array of characters read from the frame into a string values = CharArrayToString(data); // Form a string with names and values of the pass parameters inputs = GetFrameInputs(pass); // Form an SQL query from the received data query = StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %s,\n'%s',\n'%s');", pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); // Add it to the SQL query array APPEND(queries, query); } // Execute all requests DB::ExecuteTransaction(queries); // Close the database DB::Close(); }
パスの入力変数の名前と値を文字列にするための補助メソッドGetFrameInputs()は、アルゴブックから引用し、ここでのニーズに合うように少し補足しました。
得られたコードを現在のフォルダのTesterHandler.mqhファイルに保存します。
動作確認
機能をテストするために、比較的短時間で反復される少数のパラメータで最適化を実行してみましょう。最適化プロセスが完了したら、ストラテジーテスターと作成したデータベースで結果を見ることができます。
図1:ストラテジーテスターでの最適化結果
図2:データベースの最適化結果
見てわかるように、データベースの結果はテスターの結果と一致しています。ユーザー基準による同じ並び替えで、両方のケースで同じ利益値の順序が観察されます。最高のパスは、10,000米ドルの初期預金と、達成可能な最大ドローダウンが初期預金(1000米ドル)の10%で、1年以内に予想利益が5000米ドルを超える可能性があると報告しています。しかし、現在のところ、私たちは最適化結果の量的特性にはあまり興味がなく、データベースに保存できるようになったという事実に関心があります。
結論
これで目標に一歩近づきました。EAパラメータの最適化結果をデータベースに保存することに成功し、EA開発の第2段階をさらに自動化するための基盤が整いました。
しかし、実施にあたり多くの課題が残っており、高額なコストを要するため、いくつかの作業は将来に持ち越す必要がありました。それでも今回の成果により、今後のプロジェクト開発に向けた方向性がより具体的に見えてきました。
現在実装されている保存処理は、最適化プロセスに関連するパス情報を保存する点では機能していますが、そこから関連する文字列群を抽出することは依然として困難です。これを解決するためには、データベース構造の改善が必要です。これは今では非常に簡単におこなうことができます。将来的には、最適化パラメータに対して異なるオプションを事前に割り当て、複数の最適化プロセスを連続的に自動起動できるようにすることを目指しています。
ご清聴ありがとうございました。またすぐにお会いしましょう。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14680



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