上位100件の最適化パス(その1)最適化分析器の開発
Andrey Azatskiy | 21 12月, 2018
はじめに
現代の技術は金融取引の分野に深く浸透しており、技術なしの金融取引を想像することはほとんど不可能です。それにもかかわらず、取引が手作業で行われ、売買する資産の量を示す複雑な手サイン(あっという間に忘れ去られようとしている)が存在したのは、つい最近までのことです。
パーソナルコンピュータは、オンライン取引を文字通り家庭にもたらすことによって、従来の取引方法に急速に取って代わりました。今では相場をリアルタイムで見て適切な決定を下すことができます。さらに、市場技術におけるオンライン技術の出現により、手作業トレーダーのランクの低下が加速しています。現在、取引の半分以上は取引アルゴリズムによって行われており、MetaTrader 5はこのために最も便利な端末のうち、一位の座を占めています。
このプラットフォームには多数の利点がありますが、欠点もあります。ここで説明するアプリケーションではその軽減をを試みます。本稿では、取引アルゴリズム最適化パラメータの選択を改善するために設計されたEasyAndFastGUIライブラリを使用してMQL5で完全に書かれたプログラムの開発について説明します。また、遡及的取引分析および一般的なEA評価の分析に新しい機能を追加しています。
まず、EAの最適化にはかなりの時間がかかります。もちろん、これは、テスターでより高品質の方法でティックを生成されるためです(OHLCを選択しても、ローソク足ごとに4つのティックが生成されます)。また、EAの評価を向上するためのその他の追加機能のためでもあります。しかし、一般家庭のPCでは馬力が足りず、最適化には数日から数週間かかります。EAパラメータを選択した後に間違っていることに気づき、最適化パスの統計といくつかの評価比率のほかに手元には何もないことがよくあります。
各最適化パスごとに本格的な統計情報があって、複数のパラメータでそれぞれを絞り込む能力(条件付きフィルタを含む)があったらよいでしょう。また、取引統計を持ち切り戦略と比較し、すべての統計をお互いに課すことも良いでしょう。さらに、各取引の結果の後続処理のために、すべての取引履歴データをファイルにアップロードする必要があることがあります。
一部の戦略は市場の種類に依存するため、アルゴリズムがどのような種類のスリッページに耐えることができ、特定の時間間隔でアルゴリズムがどのように動作するかを見たい場合もあります。フラットベースの戦略が例として役立ちます。この戦略では、トレンド期間中は負け、フラットな期間中は利益が得られます。また、一般的なPLグラフとは別に、特定の期間(日付別)を比率およびその他の追加(単純に価格表ではなく)として表示するとよいでしょう。
フォワードテストにも注意を払う必要があります。 それらは非常に有益ですが、グラフは、ストラテジーテスターの標準レポートでは前のものの続きとして表示されます。 初心者のトレーダーは、ロボットが急激にすべての利益を失った後、回復し始めた(または悪化した)と簡単に結論づけるかもしれません。 ここで説明するプログラムでは、すべてのデータが最適化タイプ(フォワードまたは履歴)の観点から見直されます。
また、Grailsの多くのEA開発者が探し求めていることについて言及することも重要です。ロボットによっては、月に1000%以上の利益を得るものがあります。これらは市場を凌駕しているように見えるかもしれませんが(持ち切り戦略)、実際にはすべてが全く異なって見えます。記述されたプログラムが示すように、これらのロボットは実際に1000%の利益をあげることができますが、市場を凌駕するものではありません。
このプログラムでは、フルロット(増減など)のロボットを使用した取引と1つのロット(取引可能な最小ロット)を使用したロボットの取引の模倣との間の分析の分離が特徴です。持ち切り取引グラフを構築する場合、説明されているプログラムは、ロボットによるロット管理を考慮します(ロットが増加するとより多くの資産を買い、ロットが減少するとより少なくの資産を買う)。この2つのグラフを比較すると、最良の最適化パスの1つで非現実的な結果を示した私のテストロボットが市場を凌駕できないことが判明しました。したがって、取引戦略をより客観的に評価するためには、1ロット取引グラフを見るべきです。ここでは、ロボットと持ち切り戦略の両方の最小許容取引量でのPL(利益/損失 - 時間に伴う利益のグラフ)が表示されます。
さて、プログラムの開発方法をより詳細に見てみましょう。
最適化分析器の構造
プログラム構造は、次のように図式化することができます。
生成された最適化分析器は、特定のロボットと結びついておらず、その一部でもありません。<しかし、MQL5のグラフィカルインターフェイスの構築の詳細により、MQL5 EA開発テンプレートがプログラムの基礎として使用されました。プログラムが非常に大きくなった(数千行のコード)ため、より具体性と一貫性を持たせるために、上の図に表示されているようにブロックに分割されてからクラスに分けられました。ロボットテンプレートは、アプリケーションを起動するための出発点にすぎません。ブロックのそれぞれについて、以下でより詳細に考察します。ここでは、それらの関係について説明します。アプリケーションを使用するには、次のものが必要です。
- 取引アルゴリズム
- DLL Sqlite3
- 上記のグラフィカルインターフェイスライブラリに必要な編集を加えたもの(下記のグラフィックスブロックで説明)
ロボット自体は好きな用に開発することができます(OOPを使用、ロボットテンプレート内の関数、DLLからインポート...)。最も重要なのは、MQL5ウィザードが提供するロボット開発テンプレートを適用することです。これは、各最適化パスが配置された後に、クラスが必要なデータをデータベースにアップロードするデータベースブロックの1つのファイルを接続します。ストラテジーテスターでロボットを起動するときにデータベースが形成されるため、この部分は独立しており、アプリケーション自体に依存しません。
計算ブロックは前の記事「トレード履歴のカスタム表示とレポート図の作成」を改良したものです。
データベースと計算ブロックは、解析されたロボットと記述されたアプリケーションの両方で使用されます。したがって、それらはIncludeディレクトリに配置されます。これらのブロックは、大部分の作業を実行し、プレゼンタクラスを介してグラフィカルインターフェイスに接続されます。
プレゼンタクラスはプログラムブロック間を接続します。ブロックの各々は、グラフィカルインターフェイスにおいてそれぞれの機能を有します。他の論理ブロックへのリダイレクトだけでなく、ボタン押下などのイベントも処理します。それらから得られたデータはプレゼンタに返されます。それらはここで処理され、適切なグラフがプロットされ、表が記入されるなどの、グラフィック部分とのやりとりが行われます。
プログラムのグラフィカルな部分は概念論理を実行しません。代わりに、必要なインターフェイスを持つウィンドウを作成し、ボタンを押している間に適切なプレゼンタ関数を呼び出します。
プログラム自体はMQL5プロジェクトとして書かれているので、より構造化された方法で開発し、必要なすべてのファイルを1つの場所にまとめることができます。プロジェクトにはさらに別のクラスがあり、これは計算ブロックで説明されます。このクラスは、このプログラム専用に書かれたクラスで、私が開発した方法を使って最適化パスをソートします。実際には、「最適化の選択」タブ全体が特定の基準によるデータのサンプリングを減らします。
普遍的並び替えクラスは、プログラムに単独で追加されています。それはいずれのブロックにもはまりませんが、プログラムの重要な部分のままです。したがって、この記事のこの部分で簡単に検討します。
名前が示すように、クラスはデータの並び替えを扱います。そのアルゴリズムはサードパーティウェブサイトの選択ソート(ロシア語)からとられました。
//+------------------------------------------------------------------+ //| 並び変えスタイルの列挙体 | //+------------------------------------------------------------------+ enum SortMethod { Sort_Ascending,// 昇順 Sort_Descendingly// 降順 }; //+------------------------------------------------------------------+ //| 渡されたデータ型を並び替えるクラス | //+------------------------------------------------------------------+ class CGenericSorter { public: // デフォルトのコンストラクタ CGenericSorter(){method=Sort_Descendingly;} // 並び替えメソッド template<typename T> void Sort(T &out[],ICustomComparer<T>*comparer); // 並び替えの種類を選択する void Method(SortMethod _method){method=_method;} // 並び替えメソッドを取得する SortMethod Method(){return method;} private: // 並び替えメソッド SortMethod method; };
クラスにはテンプレートSortメソッドが含まれています。このメソッドはデータを並び変えます。テンプレートメソッドでは、クラスや構造体を含む渡されたデータを並び変えることができます。データ比較メソッドは、IСustomComparer<T>インターフェイスを実装する別のクラスで記述する必要があります。 Compareメソッドの従来のIСomparerインターフェイスでは参照データは参照によっては渡されません。参照渡しは構造体をMQL5言語のメソッドに渡す条件の1つであるため、独自の IСomparer 型のインターフェイスを開発する必要がありました。
CGenericSorter::Methodクラスメソッドは、戻り値を受け取り、データ並び変えタイプ(昇順または降順)をオーバーロードします。このクラスは、データが並び変えられるプログラムのすべてのブロックで使用されます。
グラフィック
警告
グラフィカルインターフェイスの開発中に、適用されたライブラリ(EasyAndFastGUI)でバグが検出されました。これは、ComboBoxのグラフィック要素が、補充中に不完全に一部の変数をクリアするということです。ライブラリ開発者の推奨(ロシア語)によれば、これを修正するために m_item_index_focus =WRONG_VALUE; をCListView::Clear(const bool redraw=false)メソッドに追加します。 このメソッドは、ListView.mqhファイルの600行目にあります。このファイルのパスは下記です。 |
---|
EasyAndFastGUIライブラリに基づいてMQL5でウィンドウを作成するには、後続のすべてのウィンドウを満たすためのコンテナとして機能するクラスが必要です。 クラスは、CwindEventsクラスから派生する必要があります。 クラス内でメソッドを再定義する必要があります。
//--- 初期化/初期化会場 void OnDeinitEvent(const int reason){CWndEvents::Destroy();}; //--- チャートイベントハンドラ virtual void OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);//
ウィンドウを作成するための空白は、次のようになります。
class CWindowManager : public CWndEvents { public: CWindowManager(void){presenter = NULL;}; ~CWindowManager(void){}; //=============================================================================== // メソッドの呼び出しとイベント //=============================================================================== //--- 初期化/初期化会場 void OnDeinitEvent(const int reason){CWndEvents::Destroy();}; //--- チャートイベントハンドラ virtual void OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam); //--- プログラムのグラフィカルインターフェイスを作成する bool CreateGUI(void); private: //--- メインウィンドウ CWindow m_window; }
ウィンドウ自体はクラス内にCwindow型で作成されます。ただし、ウィンドウを表示する前に、いくつかのウィンドウプロパティを定義する必要があります。この特定の場合、ウィンドウの作成方法は次のようになります。
bool CWindowManager::CreateWindow(const string text) { //--- ウィンドウポインタをウィンドウ配列に追加する CWndContainer::AddWindow(m_window); //--- 座標 int x=(m_window.X()>0) ?m_window.X() : 1; int y=(m_window.Y()>0) ?m_window.Y() : 1; //--- プロパティ m_window.XSize(WINDOW_X_SIZE+25); m_window.YSize(WINDOW_Y_SIZE); m_window.Alpha(200); m_window.IconXGap(3); m_window.IconYGap(2); m_window.IsMovable(true); m_window.ResizeMode(false); m_window.CloseButtonIsUsed(true); m_window.FullscreenButtonIsUsed(false); m_window.CollapseButtonIsUsed(true); m_window.TooltipsButtonIsUsed(false); m_window.RollUpSubwindowMode(true,true); m_window.TransparentOnlyCaption(true); //--- ツールヒントを設定する m_window.GetCloseButtonPointer().Tooltip("Close"); m_window.GetFullscreenButtonPointer().Tooltip("Fullscreen/Minimize"); m_window.GetCollapseButtonPointer().Tooltip("Collapse/Expand"); m_window.GetTooltipButtonPointer().Tooltip("Tooltips"); //--- フォームを作成する if(!m_window.CreateWindow(m_chart_id,m_subwin,text,x,y)) return(false); //--- return(true); }
このメソッドの前提条件は、ウィンドウをアプリケーションウィンドウの配列に追加してフォームを作成する文字列です。その後、アプリケーションが実行され、OnEventイベントがトリガされると、ライブラリメソッドの1つが、ウィンドウの配列にリストされているすべてのウィンドウにわたってループで実行されます。次に、ウィンドウ内のすべての要素を調べ、管理インターフェイスがクリックされる、表の行が強調表示されるなどのイベントを探します。したがって、新しいアプリケーションウインドウを作成する際には、参照配列にウインドウへの参照を追加する必要があります。
開発されたアプリケーションは、インターフェイスをタブで分割したもので4つのタブコンテナがあります。
//--- タブ CTabs main_tab; // メインタブ CTabs tab_up_1; // 設定と結果表のタブ CTabs tab_up_2; // 統計、パラメータの選択、共通グラフのタブ CTabs tab_down; // 統計とファイルへのアップロードのタブ
これらはフォームでは次のように表示されます(スクリーンショットでは赤)。
- main_tabにはプログラムインターフェイスの残りの部分から選択されたすべての最適化パス(「最適化データ」)の表があります。この表には、設定タブのフィルタ条件を満たすすべての結果が含まれています。結果はコンボボックスの「Sort by」で選択された比率で並び替えられます。得られたデータは、並び替えられた形式で記述された表に転送されます。残りのプログラムインターフェイスのタブには、別の3つのタブコンテナが含まれています。
- tab_up_1には、プログラムの初期設定と並び替え結果を含む表が含まれています。前述の条件フィルタに加えて、[Settings(設定)]タブではデータベースの選択と追加データの入力が行われます。たとえば、データ選択結果表には、表の[Optimisation Data(最適化データ)]タブにすでに追加されているすべてのデータを入力することも、特定数の最良のパラメータを選択して入力する(選択された比率で降順に並び替えて絞り込み)こともできます。
- tab_up_2にはタブが3つあり、それぞれに3種類のタスクを実行するインターフェイスが含まれています。最初のタブには、選択した最適化パスに関する完全なレポートが含まれており、一定期間の取引履歴を考慮してシミュレーションを行うことができます。2番目のタブは、最適化パスのフィルタとして機能し、関心のあるパラメータの最も適切な間隔を選択することによって、異なるパラメータに対する戦略の感度を定義し、最適化結果の数を絞り込むのに役立ちます。最後のタブは、最適化結果の表のグラフィック表示であり、選択された最適化パラメータの総数を示します。
- tab_downには5つのタブがあり、そのうち4つは選択したパラメータで最適化中のEAの取引レポートを表示し、最後のタブではデータをファイルにアップロードします。最初のタブには、推定比率の表が表示されます。2番目のタブは取引日ごとの損益を提供します。3番目のタブは、持ち切り戦略に課された損益グラフ(黒色のグラフ)を表し、4番目のタブは、一部の選択された比率の変化に加えて、EAの取引結果の分析によって得られる興味深く情報豊富なグラフのいくつかを示しています。
タブを作成するプロセスは似ています。唯一の違いはコンテンツです。例として、メインタブの作成方法を示します。
//+------------------------------------------------------------------+ //| メインタブ | //+------------------------------------------------------------------+ bool CWindowManager::CreateTab_main(const int x_gap,const int y_gap) { //--- メイン要素へのポインタを保存する main_tab.MainPointer(m_window); //--- タブ幅の配列 int tabs_width[TAB_MAIN_TOTAL]; ::ArrayInitialize(tabs_width,45); tabs_width[0]=120; tabs_width[1]=120; //--- string tabs_names[TAB_UP_1_TOTAL]={"Analysis","Optimisation Data"}; //--- プロパティ main_tab.XSize(WINDOW_X_SIZE-23); main_tab.YSize(WINDOW_Y_SIZE); main_tab.TabsYSize(TABS_Y_SIZE); main_tab.IsCenterText(true); main_tab.PositionMode(TABS_LEFT); main_tab.AutoXResizeMode(true); main_tab.AutoYResizeMode(true); main_tab.AutoXResizeRightOffset(3); main_tab.AutoYResizeBottomOffset(3); //--- main_tab.SelectedTab((main_tab.SelectedTab()==WRONG_VALUE)?0 : main_tab.SelectedTab()); //--- 指定されたプロパティを持つタブを追加する for(int i=0; i<TAB_MAIN_TOTAL; i++) main_tab.AddTab((tabs_names[i]!="")?tabs_names[i]: "Tab "+string(i+1),tabs_width[i]); //--- コントロール要素を作成する if(!main_tab.CreateTabs(x_gap,y_gap)) return(false); //--- オブジェクトグループの共通配列にオブジェクトを追加する CWndContainer::AddToElementsArray(0,main_tab); return(true); }
コンテンツが異なることに加えて、コードでの主な文字列は次のとおりです。
- メイン要素へのポインタの追加 - タブコンテナは、それが割り当てられる要素を知っている必要がある
- コントロール要素を作成する文字列
- コントロールの共通リストへの要素の追加
階層的に、コントロール要素が次です。アプリケーションでは11種類のコントロール要素が使用されました。それらはすべて同様の方法で作成されます。したがって、コントロール要素を作成するためにはそれらを追加するメソッドが記述されています。そのうちの1つのみの実装を考えてみましょう。
bool CWindowManager::CreateLable(const string text, const int x_gap, const int y_gap, CTabs &tab_link, CTextLabel &lable_link, int tabIndex, int lable_x_size) { //--- メイン要素へのポインタを保存する lable_link.MainPointer(tab_link); //--- タブに割り当てる tab_link.AddToElementsArray(tabIndex,lable_link); //--- 設定 lable_link.XSize(lable_x_size); //--- 作成 if(!lable_link.CreateTextLabel(text,x_gap,y_gap)) return false; //--- オブジェクトを一般オブジェクトグループ配列に追加する CWndContainer::AddToElementsArray(0,lable_link); return true; }
渡されたコントロール要素(CTextLabel)は、タブと共にコンテナとして割り当てられた要素を覚えておく必要があります。次に、タブコンテナは、要素が配置されているタブを覚えています。その後、要素に必要な設定と初期データが入力されます。最終的に、オブジェクトはオブジェクトの一般的な配列に追加されます。
ラベルと同様に、クラスコンテナの内部でフィールドとして定義されている他の要素が追加されます。私はいくつかの要素を分離し、それらのいくつかを '保護された'クラス領域に配置しました。これらはプレゼンター経由でアクセスする必要のない要素です。他のいくつかの要素は'public'に置かれました。これらは、いくつかの条件やラジオボタンを定義する要素です。その状態は、プレゼンタからチェックする必要があります。言い換えれば、アクセスが望ましくないすべての要素とメソッドは、プレゼンターへの参照とともに、クラスの「保護された」部分または「私的な」部分にヘッダーを持っています。プレゼンター参照の追加は、既に追加されたプレゼンターの存在が最初にチェックされ、それに対する参照がまだ追加されていない場合はプレゼンターが保存されるパブリックメソッドの形式で行われます。これは、プログラムの実行中に動的なプレゼンターの置換を避けるために行われます。
ウィンドウ自体は、CreateGUIメソッドで作成されます。
bool CWindowManager::CreateGUI(void) { //--- ウィンドウを作成する if(!CreateWindow("Optimisation Selection")) return(false); //--- タブを作成する if(!CreateTab_main(120,20)) return false; if(!CreateTab_up_1(3,44)) return(false); int indent=WINDOW_Y_SIZE-(TAB_UP_1_BOTTOM_OFFSET+TABS_Y_SIZE-TABS_Y_SIZE); if(!CreateTab_up_2(3,indent)) return(false); if(!CreateTab_down(3,33)) return false; //--- コントロールを作成する if(!Create_all_lables()) return false; if(!Create_all_buttons()) return false; if(!Create_all_comboBoxies()) return false; if(!Create_all_dropCalendars()) return false; if(!Create_all_textEdits()) return false; if(!Create_all_textBoxies()) return false; if(!Create_all_tables()) return false; if(!Create_all_radioButtons()) return false; if(!Create_all_SepLines()) return false; if(!Create_all_Charts()) return false; if(!Create_all_CheckBoxies()) return false; // ウィンドウを見せる CWndEvents::CompletedGUI(); return(true); }
その実装からわかるように、コントロール要素自体を直接作成するのではなく、これらの要素を作成するための他のメソッドを呼び出すだけです。このメソッドで最終的にコード文字列に含める必要があるのは「CWndEvents::CompletedGUI();」です。
これはグラフィックスの作成を完了してユーザの画面にプロットします。各コントロール要素(分離線、ラベル、ボタンなど)の作成は同様のメソッドで実装されており、グラフィカルコントロール要素を作成するための上記のアプローチを適用しています。メソッドヘッダはクラスの「プライベート」部分にあります。
//=============================================================================== // コントロールの作成: //=============================================================================== //--- すべてのラベル bool Create_all_lables(); bool Create_all_buttons(); bool Create_all_comboBoxies(); bool Create_all_dropCalendars(); bool Create_all_textEdits(); bool Create_all_textBoxies(); bool Create_all_tables(); bool Create_all_radioButtons(); bool Create_all_SepLines(); bool Create_all_Charts(); bool Create_all_CheckBoxies();
グラフィックについてお話しするときにイベントモデルの部分をスキップすることは不可能です。EasyAndFastGUIを使用して開発されたグラフィックアプリケーションでの正しい処理のためには、以下の手順を実行する必要があります。
イベントハンドラメソッドを作成します(たとえば、ボタンの押下)。このメソッドは、パラメータとして「id」と「lparam」を受け入れる必要があります。最初のパラメータはグラフィカルイベントのタイプを示し、2番目のパラメータはインタラクションが発生したオブジェクトのIDを示します。メソッドの実装は、すべての場合で同様です。
//+------------------------------------------------------------------+ //| Btn_Update_Click | //+------------------------------------------------------------------+ void CWindowManager::Btn_Update_Click(const int id,const long &lparam) { if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON && lparam==Btn_update.Id()) { presenter.Btn_Update_Click(); } }
まず、条件を確認します(ボタンが押されたか、リスト要素が選択されたか...)。次に、lparamを確認して、メソッドに渡されたIDと必須のリスト要素のIDを比較します。
すべてのボタン押下イベントの宣言は、クラスの「プライベート」部分にあります。イベントは、その応答を取得するために呼び出されます。宣言されたイベントは、オーバーロードされたOnEventメソッドで呼び出されます。
//+------------------------------------------------------------------+ //| OnEvent | //+------------------------------------------------------------------+ void CWindowManager::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { Btn_Update_Click(id,lparam); Btn_Load_Click(id,lparam); OptimisationData_inMainTable_selected(id,lparam); OptimisationData_inResults_selected(id,lparam); Update_PLByDays(id,lparam); RealPL_pressed(id,lparam); OneLotPL_pressed(id,lparam); CoverPL_pressed(id,lparam); RealPL_pressed_2(id,lparam); OneLotPL_pressed_2(id,lparam); RealPL_pressed_4(id,lparam); OneLotPL_pressed_4(id,lparam); SelectHistogrameType(id,lparam); SaveToFile_Click(id,lparam); Deals_passed(id,lparam); BuyAndHold_passed(id,lparam); Optimisation_passed(id,lparam); OptimisationParam_selected(id,lparam); isCover_clicked(id,lparam); ChartFlag(id,lparam); show_FriquencyChart(id,lparam); FriquencyChart_click(id,lparam); Filtre_click(id,lparam); Reset_click(id,lparam); RealPL_pressed_3(id,lparam); OneLotPL_pressed_3(id,lparam); ShowAll_Click(id,lparam); DaySelect(id,lparam); }
このメソッドは、ロボットテンプレートから呼び出されます。したがって、イベントモデルは、ロボットテンプレート(以下に提供)からグラフィカルインターフェイスまでを対象とします。GUIは、その後のプレゼンタによる処理に向けて、すべての処理、並べ替え、リダイレクションを実行します。ロボットテンプレート自体は、プログラムの出発点です。これは次のようになります。
#include "Presenter.mqh" CWindowManager _window; CPresenter Presenter(&_window); //+------------------------------------------------------------------+ //| エキスパート初期化関数 | //+------------------------------------------------------------------+ int OnInit() { //--- if(!_window.CreateGUI()) { Print(__FUNCTION__," > Failed to create the graphical interface!"); return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| エキスパート初期化解除関数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- _window.OnDeinitEvent(reason); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { _window.ChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+
データベースの使用
このプロジェクトのかなり広範な部分を検討する前に、選択過程について言及する価値があります。プロジェクト初期の目標の1つは、最適化自体を完了した後に最適化結果を処理する能力を提供することと、いつでもこれらの結果を利用できるようにすることでした。データをファイルに保存することは、直ちに不適当とみなされました。複数の表(実際には1つの大きな表を形成するが、行数は異なる)またはファイルを作成する必要があるからです。
どちらもあまり便利ではありません。さらに、この方法は実装がより困難です。2番目の方法は、最適化フレームを作成することです。ツールキット自体は良いですが、最適化プロセス中に最適化を行うつもりはありません。さらに、フレーム機能はデータベースの機能ほど良くはありません。さらに、フレームはMetaTrader用に設計されていますが、データベースは必要に応じて任意の第三者の分析プログラムで使用できます。
正しいデータベースを選択するのは簡単でした。簡単にリンクできて追加のソフトウェアを必要としない、高速かつ普及したデータベースが必要でした。Sqliteデータベースはすべての基準を満たしています。上記の特性のため、これは非常に人気があるデータベースです。使用するには、プロバイダーが提供するデータベースをDLLプロジェクトに接続します。DLLデータはC言語で書かれており、MQL5アプリケーションと簡単にリンクすることができます。第三者の言語でコードを一行も書く必要がないため、プロジェクトは複雑になりません。これは、すばらしいことです。このアプローチの欠点の1つは、DLL Sqliteがデータベースの連携に便利なAPIを提供していないため、少なくともデータベースを操作するための最小限のラッパーを記述する必要があることです。この機能を記述した例はSQL と MQL5:SQLITE データベースとの連携稿で効率的に説明されています。このプロジェクトでは、WinApiとのやりとりとDLLからMQL5へのいくつかの関数のインポートに関連するコードの一部を上記の記事から借用しました。ラッパーについては、自分で書くことに決めました。
その結果、データベース処理ブロックは、データベースと連携するための便利なラッパーを持つSqlite3フォルダと、開発されたプログラム用に特別に作成されたOptimisationSelectorフォルダで構成されています。両方のフォルダは、MQL5/Includeディレクトリに位置します。前述したように、Windows標準ライブラリの多くの関数がデータベースとの連携に使用されます。アプリケーションのこの部分のすべての関数は、WinApiディレクトリにあります。上記に加えて、共有リソース(Mutex)を作成するためのコードもコードベースから借用しました。2つのソースからデータベースと連携する場合(つまり、最適化アナライザが最適化中に使用されたデータベースを開く場合)、プログラムによって得られたデータは常に完全でなければなりません。これが、共有リソースが必要な理由です。一方(最適化プロセスまたはアナライザ)がデータベースをアクティブにすると、その操作が完了されるまでもう一方が待機することになります。Sqliteデータベースは複数のスレッドから読み取ることができます。本稿の主題からそれるため、MQL5からsqlite3データベースに連携するためのラッパーの詳細は検討しません。代わりに、実装方法と適用メソッドのいくつかの点についてのみ説明します。すでに説明したように、データベースに連携するためラッパーはSqlite3フォルダーにあります。そこには3つのファイルがあります。順番に見て行きましょう。
- 最初に必要なことは、データベースに連携するために必要な関数をDLLからインポートすることです。最低限必要な機能を含むラッパーを作成することが目標だったので、データベース開発者が提供している関数全体の1%もインポートしませんでした。必要な関数はすべてsqlite_amalgmation.mqhファイルにインポートされます。これらの関数は、開発者のウェブサイトによくコメントされており、上記のファイルにもラベルが付けられています。必要に応じて、ヘッダファイル全体を同じ方法でインポートすることができます。結果として、すべての関数の完全なリストが得られ、それらにアクセスできます。下記はインポートされた関数のリストです。
#import "Sqlite3_32.dll" int sqlite3_open(const uchar &filename[],sqlite3_p32 &paDb);// データベースを開く int sqlite3_close(sqlite3_p32 aDb); // データベースを閉じる int sqlite3_finalize(sqlite3_stmt_p32 pStmt);// 文を完了する int sqlite3_reset(sqlite3_stmt_p32 pStmt); // 文をリセットする int sqlite3_step(sqlite3_stmt_p32 pStmt); // 文を読むときに次の行に移動する int sqlite3_column_count(sqlite3_stmt_p32 pStmt); // 列の数を計算する int sqlite3_column_type(sqlite3_stmt_p32 pStmt,int iCol); // 選択された列の型を取得する int sqlite3_column_int(sqlite3_stmt_p32 pStmt,int iCol);// 値をintに変換する long sqlite3_column_int64(sqlite3_stmt_p32 pStmt,int iCol); // 値をint64に変換する double sqlite3_column_double(sqlite3_stmt_p32 pStmt,int iCol); // 値をdoubleに変換する const PTR32 sqlite3_column_text(sqlite3_stmt_p32 pStmt,int iCol);// テキストの値を取得する int sqlite3_column_bytes(sqlite3_stmt_p32 apstmt,int iCol); // 渡されたセルからラインが占めるバイト数を取得する int sqlite3_bind_int64(sqlite3_stmt_p32 apstmt,int icol,long a);// (int64型の)値を持つリクエストを結合する int sqlite3_bind_double(sqlite3_stmt_p32 apstmt,int icol,double a);// (double型の)値を持つリクエストを結合する int sqlite3_bind_text(sqlite3_stmt_p32 apstmt,int icol,char &a[],int len,PTRPTR32 destr);// (string型(C++のchar* ))の)値を持つリクエストを結合する int sqlite3_prepare_v2(sqlite3_p32 db,const uchar &zSql[],int nByte,PTRPTR32 &ppStmt,PTRPTR32 &pzTail);// リクエストを準備する int sqlite3_exec(sqlite3_p32 aDb,const char &sql[],PTR32 acallback,PTR32 avoid,PTRPTR32 &errmsg);// Sqlの実行 int sqlite3_open_v2(const uchar &filename[],sqlite3_p32 &ppDb,int flags,const char &zVfs[]); // データベースをパラメータで開く #import
開発者によって提供されたデータベースは、Librariesフォルダに配置され、DLLデータベースラッパーが動作するビット数に応じてSqlite3_32.dllまたはSqlite3_64.dllと名付けられます。本稿添付のファイルからDLLデータを取り出し、Sqlite Amalgmationから自分でコンパイルする、または、Sqlite開発者のWebサイトからDLLデータを取り出すことができます。その存在はプログラムの前提条件です。EAにDLLのインポートを許可することも必要です。
- もう一つは、データベースに接続するためのラッパーを書くことです。これは、データベースへの接続を作成し、デストラクタで解放する(データベースの接続を切断する)クラスでなければなりません。また、単純な文字列Sqlコマンドを実行し、トランザクションを管理し、クエリ(文)を作成することができます。説明されたすべての機能がCsqliteManagerクラスに実装されました。これによって、データベースとのやりとりのプロセスが開始されます。
//+------------------------------------------------------------------+ //| データベースの接続および管理クラス | //+------------------------------------------------------------------+ class CSqliteManager { public: CSqliteManager(){db=NULL;} // 空のコンストラクタ CSqliteManager(string dbName); // 名前を渡す CSqliteManager(string dbName,int flags,string zVfs); // 名前と接続フラグを渡す CSqliteManager(CSqliteManager &other) { db=other.db; } // コピーコンストラクタ ~CSqliteManager(){Disconnect();};// デストラクタ void Disconnect(); // データベースの接続を切断する bool Connect(string dbName,int flags,string zVfs); // パラメータによるデータベースエージェントへの接続 bool Connect(string dbName); // 名前によるデータベースへの接続 void operator=(CSqliteManager &other){db=other.db;}// 割り当て演算子 sqlite3_p64 DB() { return db; }; // データベースのポインタを取得する sqlite3_stmt_p64 Create_statement(const string sql); // 文を作成する bool Execute(string sql); // 命令を実行する void Execute(string sql,int &result_code,string &errMsg); // 命令を実行し、エラーコードとメッセージを提供する void BeginTransaction(); // トランザクション開始 void RollbackTransaction(); // トランザクションロールバック void CommitTransaction(); // トランザクションを確認する private: sqlite3_p64 db; // データベース void stringToUtf8(const string strToConvert,// UTF-8エンコーディングで配列に変換される文字列 uchar &utf8[],// 変換されたstrToConvert文字列が配置されるUTF-8エンコーディングの配列 const bool untilTerminator=true) { // UTF-8エンコーディングに変換され、UTF-8配列にコピーされる銘柄の数 //--- int count=untilTerminator ?-1 : StringLen(strToConvert); StringToCharArray(strToConvert,utf8,0,count,CP_UTF8); } };
コードから分かるように、結果のクラスには、データベースに2種類の接続を作成する機能があります(テキストとパラメータの指定)。Create_sttementメソッドはデータベースへのリクエストを作成し、そのポインタを返します。Exequteメソッドのオーバーロードは単純な文字列クエリを実行し、トランザクションメソッドはトランザクションを作成して受け入れ/キャンセルします。データベース自体への接続は、db変数に格納されます。Disconnectメソッドを適用した場合、またはデフォルトのコンストラクタを使用してクラスを作成した場合(データベースに接続する時間がなかった場合)、変数はNULLです。Connectメソッドを繰り返し呼び出すと、以前に接続したデータベースから切断して新しいデータベースに接続します。データベースに接続するには、UTF-8形式の文字列を渡す必要があるため、クラスには、文字列を必要なデータ形式に変換する特殊な「プライベート」メソッドがあります。
- 次の作業は、クエリー(文)を使用して作業するためのラッパーを作成することです。データベースへのリクエストを作成して破棄する必要があります。リクエストはCsqliteManagerによって作成されますが、メモリは管理されません。言い換えれば、リクエストを作成した後で、必要性がなくなったときにリクエストを破棄する必要があります。さもないと、データベースからの切断が許可されないため、データベースを使用して作業を完了しようとすると、 データベースがビジー状態だというエラーを受けます。また、文のラッパークラスは、渡されたパラメータが「INSERT INTO table_1 VALUES(@ID,@Param_1,@Param_2);」として形成されたときにリクエストを満たすべきです。さらに、与えられたクラスは、そこに配置されたクエリを実行できる必要があります(Exequteメソッド)。
typedef bool(*statement_callback)(sqlite3_stmt_p64); // クエリ実行時のコールバック(正常の場合「true」を実行) //+------------------------------------------------------------------+ //| データベースへのクエリのクラス | //+------------------------------------------------------------------+ class CStatement { public: CStatement(){stmt=NULL;} // 空のコンストラクタ CStatement(sqlite3_stmt_p64 _stmt){this.stmt=_stmt;} // 文へのポインタをパラメータとして持つコンストラクタ ~CStatement(void){if(stmt!=NULL)Sqlite3_finalize(stmt);} // デストラクタ sqlite3_stmt_p64 get(){return stmt;} // 文へのポインタを取得する void set(sqlite3_stmt_p64 _stmt); // 文へのポインタを設定する bool Execute(statement_callback callback=NULL); // 文を実行する bool Parameter(int index,const long value); // パラメータを追加する bool Parameter(int index,const double value); // パラメータを追加する bool Parameter(int index,const string value); // パラメータを追加する private: sqlite3_stmt_p64 stmt; };
パラメータメソッドのオーバーロードがリクエストパラメータを記入します。「set」メソッドは、渡された文を「stmt」変数に保存します。新しいリクエストを保存する前に古いリクエストがクラスに保存されていることがわかった場合、以前に保存したリクエストに対してSqlite3_finalizeメソッドが呼び出されます。
- データベース処理ラッパーの終わりのクラスは、データベースからの応答を読み取ることができるCSqliteReaderです。以前のクラスと同様に、クラスはデストラクタでsqlite3_resetメソッドを呼び出します。リクエストが破棄されるので、再度処理することができます。新しいバージョンのデータベースでは、この関数を呼び出す必要はありませんが、開発者がそのまま残したようです。私はラッパーでそれを使用しています。また、このクラスは、データベースからの応答を文字列ごとに読み取って読み取られたデータを適切な形式に変換するという主な仕事を果たす必要があります。
//+------------------------------------------------------------------+ //| データベースからの応答を読み取るクラス | //+------------------------------------------------------------------+ class CSqliteReader { public: CSqliteReader(){statement=NULL;} // 空のコンストラクタ CSqliteReader(sqlite3_stmt_p64 _statement) { this.statement=_statement; }; // 文へのポインタを受け取るコンストラクタ CSqliteReader(CSqliteReader &other) : statement(other.statement) {} // コピーコンストラクタ ~CSqliteReader() { Sqlite3_reset(statement); } // デストラクタ void set(sqlite3_stmt_p64 _statement); // 文への参照を追加する void operator=(CSqliteReader &other){statement=other.statement;}// リーダー割り当て演算子 void operator=(sqlite3_stmt_p64 _statement) {set(_statement);}// 文の割り当て演算子 bool Read(); // 文字列を読む int FieldsCount(); // 列の数を数える int ColumnType(int col); // 列のタイプを取得する bool IsNull(int col); // value == SQLITE_NULLかどうかを確認する long GetInt64(int col); // 'int'に変換する double GetDouble(int col);// 'double'に変換する string GetText(int col);// 'string'に変換する private: sqlite3_stmt_p64 statement; // 文へのポインタ };
Sqlite3.dllからアップロードされたデータベースに連携するための関数を使用して記述されたクラスを実装したので、記述されているプログラムからデータベースで動作するクラスを記述する必要があります。
作成されたデータベースの構造は次のとおりです。
持ち切りの表:
- Time — X軸(時間間隔ラベル)
- PL_total — ロボットに比例してロットを増矢した場合の利益/損失
- PL_oneLot — 単一のロットを常に取引した場合の利益/損失
- DD_total — EAと同じ方法で多く取引擦る場合のドローダウン
- DD_oneLot — 単一のロットを取引した場合のドローダウン
- isForvard — 前方グラフのプロパティ
OptimisationParamsの表:
- ID — データベース内の一意の自動入力エントリインデックス
- HistoryBorder — 履歴最適化完了日
- TF — 時間枠
- Param_1...Param_n — パラメータ
- InitalBalance — 初期入金額
ParamsCoefitientsの表:
- ID — 外部キー、OptimisationParams(ID)への参照
- isForvard — 前方最適化プロパティ
- isOneLot — 比率が基づいたチャートのプロパティ
- DD — ドローダウン
- averagePL — PLグラフによる平均損益
- averageDD — 平均ドローダウン
- averageProfit — 平均利益
- profitFactor — 利益率
- recoveryFactor — 回復率
- sharpRatio — シャープレシオ
- altman_Z_Score — Altman Zスコア
- VaR_absolute_90 — VaR 90
- VaR_absolute_95 — VaR 95
- VaR_absolute_99 — VaR 99
- VaR_growth_90 — VaR 90
- VaR_growth_95 — VaR 95
- VaR_growth_99 — VaR 99
- winCoef — 勝率
- customCoef — カスタム比率
ParamTypeの表:
- ParamName — ロボットパラメータ名
- ParamType — ロボットパラメータ型(int/double/string)
TradingHistoryの表:
- ID — OptimisationParams(ID)への外部キー参照
- isForvard — 前方最適化フラグ
- Symbol — 銘柄
- DT_open — 開始日付け
- Day_open — 開始日
- DT_close — 終了日付
- Day_close — 終了日
- Volume — ロット数
- isLong — long/shortプロパティ
- Price_in — エントリ価格
- Price_out — エグジット価格
- PL_oneLot — 単一のロットを常に取引した場合の利益
- PL_forDeal — 以前と同じ方法で取引した場合の利益
- OpenComment — エントリコメント
- CloseComment — エグジットコメント
上記のデータベース構造から、表がEAパラメータを格納するOptimisationParams表を参照するためにいくつかの外部キーを使用することがわかります。入力パラメータの各列には、その名前が付いています(たとえば、Fast/Slow — 高速/低速移動平均)。また、各列には特定のデータ形式が必要です。多くのSqliteデータベースは、表の列のデータ形式を定義せずに作成されます。この場合、すべてのデータは行として格納されます。しかし、特定のプロパティによって比率を並び替える必要があるため、正確なデータ形式を知る必要があります。これは、データベースからアップロードされたデータを元の形式に変換することを意味します。
これには、データをデータベースに入力する前にその形式を知っておく必要があります。テンプレートメソッドを作成してそれへのコンバータを送る、またはEA変数の名前と組み合わされたいくつかのデータ型(すべてのデータ型を変換可能)の汎用ストレージであるクラスを作成するなどのいくつかのオプションがあります。ここでは、2番目のオプションを選択し、CDataKeeperクラスを作成しました。記述されたクラスは、3つのデータ型[int、double、string]を格納できます。EA入力形式として使用できる他のすべてのデータ型はこれらと相互に変換できます。
//+------------------------------------------------------------------+ //| EAパラメータの入力データの型 | //+------------------------------------------------------------------+ enum DataTypes { Type_INTEGER,// int Type_REAL,// double。float Type_Text // string }; //+------------------------------------------------------------------+ //| 2つのCDataKeeperを比較した結果 | //+------------------------------------------------------------------+ enum CoefCompareResult { Coef_Different,// 異なるデータ型または変数名 Coef_Equal,// 変数は等しい Coef_Less, // 現在の変数が渡された物より小さい Coef_More // 現在の変数が渡された物より大きい }; //+---------------------------------------------------------------------+ //| 1つの特定のロボットインプットを格納するクラス | //| [int, double, string]型のデータを格納できる | //+---------------------------------------------------------------------+ class CDataKeeper { public: CDataKeeper(); // コンストラクタ CDataKeeper(const CDataKeeper&other); // コピーコンストラクタ CDataKeeper(string _variable_name,int _value); // パラメトリックコンストラクタ CDataKeeper(string _variable_name,double _value); // パラメトリックコンストラクタ CDataKeeper(string _variable_name,string _value); // パラメトリックコンストラクタ CoefCompareResult Compare(CDataKeeper &data); // 比較メソッド DataTypes getType(){return variable_type;}; // データ型を取得する string getName(){return variable_name;}; // パラメータ名を取得する string valueString(){return value_string;}; // パラメータを取得する int valueInteger(){return value_int;}; // パラメータを取得する double valueDouble(){return value_double;}; // パラメータを取得する string ToString(); // パラメータを文字列に変換する。これが文字列パラメータの場合は、両側から文字列に一重引用符が追加される <<'>> private: string variable_name,value_string; // 変数名と文字列変数 int value_int; // Int変数 double value_double; // Double変数 DataTypes variable_type; // 変数型 int compareDouble(double x,double y) // 小数点以下10桁までのDouble型の比較 { double diff=NormalizeDouble(x-y,10); if(diff>0) return 1; else if(diff<0) return -1; else return 0; } };
3つのコンストラクタのオーバーロードでは変数名を最初のパラメータ、言及された型の1つに変換された値を2番目のパラメータとして受け取ります。これらの値は、クラスのグローバル変数に保存され、「value_」の後に型の指定が続きます。getType()メソッドは上記の列挙型として型を返し、getName()メソッドは変数名を返します。「value」で始まるメソッドは必要な型の変数を返しますが、valueDouble()メソッドが呼び出されてそのクラスに格納されている変数の型が「int」の場合はNULLが返されます。ToString()メソッドは、任意の変数の値を文字列形式に変換します。ただし、変数が最初に文字列であった場合は、単一引用符が追加されます。これは、より便利にSQLリクエストを形成するためです。Compare(CDataKeeper &ther)メソッドは、下記を比較しながらCDataKeeper型の2つのオブジェクトを比較します。
- EA変数名
- 変数型
- 変数値
最初の2つが一致しない場合は、2つの異なるパラメータを比較している(たとえば、高速移動平均の期間と低速期間の比較)ため、比較の必要がありません。 比較されるデータの型は同じでなければいけないからです。この場合、CoefCompareResult型のCoef_Different値が返されます。それ以外の場合は、比較が行われ、必要な結果が返されます。比較メソッド自体は次のように実装されています。
//+------------------------------------------------------------------+ //| 現在のパラメータと渡されたパラメータを比較する | //+------------------------------------------------------------------+ CoefCompareResult CDataKeeper::Compare(CDataKeeper &data) { CoefCompareResult ans=Coef_Different; if(StringCompare(this. variable_name,data.getName())==0 && this.variable_type==data.getType()) // 名前と型を比較する { switch(this.variable_type) // 値を比較する { case Type_INTEGER : ans=(this.value_int==data.valueInteger() ?Coef_Equal :(this.value_int>data.valueInteger() ?Coef_More : Coef_Less)); break; case Type_REAL : ans=(compareDouble(this.value_double,data.valueDouble())==0 ?Coef_Equal :(compareDouble(this.value_double,data.valueDouble())>0 ?Coef_More : Coef_Less)); break; case Type_Text : ans=(StringCompare(this.value_string,data.valueString())==0 ?Coef_Equal :(StringCompare(this.value_string,data.valueString())>0 ?Coef_More : Coef_Less)); break; } } return ans; }
変数の型に依存しない表現によって、名前、変数のデータ型、およびその値の両方を考慮に入れて、より便利な形式で変数を使用することができます。
次のタスクは、上記のデータベースを作成することです。これにはCDatabaseWriterクラスを使用します。
//+---------------------------------------------------------------------------------+ //| ユーザ比率を計算するコールバック | //| 比率計算を必要とする履歴型の履歴データとフラグが | //| 入力に渡される | //+---------------------------------------------------------------------------------+ typedef double(*customScoring_1)(const DealDetales &history[],bool isOneLot); //+---------------------------------------------------------------------------------+ //| ユーザ比率を計算するコールバック | //| データベースへの接続(読み取り専用)、履歴、リクエストされた比率のタイプフラグが | //| 入力に渡される | //+---------------------------------------------------------------------------------+ typedef double(*customScoring_2)(CSqliteManager *dbManager,const DealDetales &history[],bool isOneLot); //+---------------------------------------------------------------------------------+ //| データをデータベースに保存し、その前にデータベースを作成するクラス | //+---------------------------------------------------------------------------------+ class CDBWriter { public: // OnInitのリセットのいずれかを呼び出す void OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // コールバック1 void OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // コールバック2 void OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT);// コールバックやユーザ比率なし(ゼロに等しい) double OnTesterEvent();// OnTesterで呼び出す void OnTickEvent();// OnTickで呼び出す private: CSqliteManager dbManager; // データベースのコネクタ CDataKeeper coef_array[]; // 入力パラメータ datetime DT_Border; // 最終ローソク足の日付(OnTickEventで計算) double r; // リスクなしの比率 customScoring_1 scoring_1; // コールバック customScoring_2 scoring_2; // コールバック int scoring_type; // コールバックの種類 [1,2] string DBPath; // データベースへのパス double balance; // 残高 ENUM_TIMEFRAMES TF; // 時間枠 void CreateDB(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF);// データベースと補助オブジェクトを作成する bool isForvard();// 現在の最適化の種類を定義する(履歴/フォワード) void WriteLog(string s,string where);// ファイルログエントリ int setParams(bool IsForvard,CReportCreator *reportCreator,DealDetales &history[],double &customCoef);// 表に入力する void setBuyAndHold(bool IsForvard,CReportCreator *reportCreator);// 持ち切り履歴を記入する bool setTraidingHistory(bool IsForvard,DealDetales &history[],int ID);// 取引履歴を埋める bool setTotalResult(TotalResult &coefData,bool isOneLot,long ID,bool IsForvard,double customCoef);// 表に比率を記入する bool isHistoryItem(bool IsForvard,DealDetales &item,int ID); // パラメータが取引履歴の表に既存するか確認する };
クラスはカスタムロボット自体のみで使用されます。その目的は、記述されたプログラム、つまり必要な構造と内容を持つデータベースの入力パラメータを作成することです。ご覧のように、3つのパブリックメソッドがあります(オーバーロードされたメソッドは1つのメソッドとみなされます)。
- OnInitEvent
- OnTesterEvent
- OnTickEvent
それぞれは、必要なパラメータが渡される、対応するロボットテンプレートのコールバックで呼び出されます。OnInitEventメソッドは、データベースに連携するクラスを準備するために設計されています。オーバーロードは次のように実装されます。
//+------------------------------------------------------------------+ //| データベースと接続を作成する | //+------------------------------------------------------------------+ void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double _r,ENUM_TIMEFRAMES _TF) { CreateDB(_DBPath,inputData_array,_r,_TF); scoring_2=scoringFunction; scoring_type=2; } //+------------------------------------------------------------------+ //| データベースと接続を作成する | //+------------------------------------------------------------------+ void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double _r,ENUM_TIMEFRAMES _TF) { CreateDB(_DBPath,inputData_array,_r,_TF); scoring_1=scoringFunction; scoring_type=1; } //+------------------------------------------------------------------+ //| データベースと接続を作成する | //+------------------------------------------------------------------+ void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],double _r,ENUM_TIMEFRAMES _TF) { CreateDB(_DBPath,inputData_array,_r,_TF); scoring_type=0; }
実装でわかるように、メソッドはクラスフィールドに必要な値を割り当ててデータベースを作成します。コールバックメソッドは、ユーザが個人的に(カスタム比率を計算する必要がある場合)実装する必要があります。コールバックを使用せずにオーバーロードを使用する必要があります。この場合、カスタム比率はゼロに等しくなります。ユーザの比率は、EA最適化パスを評価するカスタムメソッドです。これを実装するために、必要な2種類のデータを持つ2つの関数へのポインタが作成されました。
- 最初の関数(customScoring_1)は、取引履歴と、計算が必要な最適化パスを定義するフラグ(実際に取引されたロットまたは1つのロットを取引 - 計算のためのすべてのデータが渡された配列に存在する)を受け取ります。
- 2番目のコールバックタイプ(customScoring_2)は、作業が実行されたデータベースへのアクセス権を取得しますが、ユーザによる予期しない編集を防ぐための読み取り専用の権限が必要です。
- 残高、時間枠、リスクフリーレート値の割り当て
- データベースへの接続の確立と共有リソース(Mutex)の占有
- まだ作成されていない場合は、表のデータベースの作成
OnTickEvent パブリックティックは、各ティックで分足ローソク足の日時を保存します。戦略をテストするとき、現在のパスがフォワードであるかどうかを定義することは不可能ですが、データベースにも同様のパラメータがあります。しかし、テスターが履歴テストの後にフォワードテストを実行することはわかっています。したがって、各ティックで日付を変数に上書きしながら、最適化プロセスの最後で最終の日付を見つけます。OptimisationParamsの表にはHistoryBorderパラメータがあります。これは保存された日時と同じです。この行は、履歴最適化中にのみこの表に追加されます。このパラメータを使用した最初のパス(履歴最適化パスと同じ)では、データベース内の必須フィールドに日付が追加されます。次のパスの1つの間に、これらのパラメータを持つエントリがすでにデータベースに存在することがわかっている場合、2つのオプションがあります。
- ユーザが何らかの理由で履歴の最適化を停止して再開した
- フォワード最適化
これらを区別するためには、現在のパスに格納されている最後の日付とデータベースの日付を比較します。現在の日付がデータベース内の日付より大きい場合はフォワードパスで、それ以外の場合は履歴パスです。最適化を同じ比率で2回実行する必要があることを考慮して、新しいデータのみをデータベースに入力するか、現在のパスで行われたすべての変更をキャンセルします。メソッドは、データをデータベースに保存します。これは次のように実装されています。
//+------------------------------------------------------------------+ //| データをすべてデータベースに保存して | //| カスタム比を返す | //+------------------------------------------------------------------+ double CDBWriter::OnTesterEvent() { DealDetales history[]; CDealHistoryGetter historyGetter; historyGetter.getDealsDetales(history,0,TimeCurrent()); // 取引履歴を取得する CMutexSync sync; // 同期オブジェクト if(!sync.Create(getMutexName(DBPath))) { Print(Symbol()+" MutexSync create ERROR!"); return 0; } CMutexLock lock(sync,(DWORD)INFINITE); // セグメントをブラケット内にロックする bool IsForvard=isForvard(); // 現在のテスター反復が前のテスター反復であるかどうかを調べる CReportCreator rc; string Symb[]; rc.Get_Symb(history,Symb); // 銘柄リストを取得する rc.Create(history,Symb,balance,r); // レポートを作成する(持ち切りレポートは自動的に作成される)。 double ans=0; dbManager.BeginTransaction(); // トランザクション開始 CStatement stmt(dbManager.Create_statement("INSERT OR IGNORE INTO ParamsType VALUES(@ParamName,@ParamType);")); // EAパラメータ型リストの保存リクエスト if(stmt.get()!=NULL) { for(int i=0;i<ArraySize(coef_array);i++) { stmt.Parameter(1,coef_array[i].getName()); stmt.Parameter(2,(int)coef_array[i].getType()); stmt.Execute(); // パラメータ型と名前を保存する } } int ID=setParams(IsForvard,&rc,history,ans); // EAのパラメータと評価率を保存してIDを取得する if(ID>0)// ID > 0の場合、パラメータは正常に保存された { if(setTraidingHistory(IsForvard,history,ID)) // 取引履歴を保存して保存されたことを確認する { setBuyAndHold(IsForvard,&rc); // 持ち切り履歴を保存する(最初の保存中に1回のみ保存) dbManager.CommitTransaction(); // トランザクション終了を確認する } else dbManager.RollbackTransaction(); // さもなければ、トランザクションをキャンセルする } else dbManager.RollbackTransaction(); // さもなければ、トランザクションをキャンセルする return ans; }
メソッドは最初に、前の記事で説明したクラスを使用して取引履歴を作成します。次に、共有リソース(Mutex)を受け取り、データを保存します。これを達成するには、まず、現在の最適化パスが(前述の方法に従って)順方向であるかどうかを定義してから、銘柄リスト(取引されたすべての銘柄)を取得します。
したがって、例えばスプレッド取引EAがテストされた場合、取引された両方の銘柄の取引履歴がアップロードされます。その後、レポートが生成され(後述のクラスを使用)、データベースに書き込まれます。正しいレコードのトランザクションが作成されます。表に記入するときにエラーが発生したり、不正なデータが取得された場合、トランザクションはキャンセルされます。 最初に、比率が保存され、うまくいけば、取引履歴とそれに続く持ち切り履歴が保存されます。後者は、最初のデータ入力中に1回だけ保存されます。データ保存エラーの場合は、Common/FilesフォルダのLog Fileが生成されます。
データベースを作成した後、それを読み取る必要があります。記述されたプログラムではデータベース読み込みクラスが既に使用されています。それはより簡単で次のようになります。
//+------------------------------------------------------------------+ //| データベースからデータを読むクラス | //+------------------------------------------------------------------+ class CDBReader { public: void Connect(string DBPath);// データベースに接続するメソッド bool getBuyAndHold(BuyAndHoldChart_item &data[],bool isForvard);// 持ち切り履歴を計算するメソッド bool getTraidingHistory(DealDetales &data[],long ID,bool isForvard);// EAによる取引履歴を計算するメソッド bool getRobotParams(CoefData_item &data[],bool isForvard);// EAパラメータと比を計算するメソッド private: CSqliteManager dbManager; // データベースマネージャ string DBPath; // データベースへのパス bool getParamTypes(ParamType_item &data[]);// 入力型と名前を計算する };
ここで関心のある4つの表を読み、3つのパブリックメソッドを実装し、これらの表のデータで構造体配列を作成します。
- 最初のメソッド(getBuyAndHold)は、渡されたフラグに応じて、前方および過去の期間について、参照ごとにBuyAndHoldの履歴を返します。アップロードが成功すると、メソッドは「true」を返し、それ以外の場合は「false」を返します。アップロードは、「Buy And Hold」表から実行されます。
- getTradingHistoryメソッドは、渡されたIDの取引履歴とそれに応じたisForvardフラグを返します。アップロードは、「TradingHistory」表から実行されます。
- getRobotParamsメソッドは、2つの表からのアップロードを結合します(ロボットパラメータが取得されたParamsCoefitientsと計算された評価比率が配置されているOptimisationParams)。
したがって、記述されたクラスでは、データベースに直接連携することはできませんが、必要なデータを提供するクラスを使用して、データベース連携アルゴリズム全体を隠すことができます。これらのクラスは、書かれたデータベースのラッパーと連携して作業を簡素化します。上記のラッパーは、データベース開発者が提供するDLLを介してデータベースと連携します。データベースそのものは、すべての必要条件を満たすものであり、実際には、このプログラムと他の分析アプリケーションの両方で輸送と処理に便利なファイルです。このアプローチのもう1つの利点は、1つのアルゴリズムの長期的な操作によって、各最適化からデータベースを収集し、履歴とトラッキングパラメータの変更パターンを蓄積できることです。
計算
ブロックは2つのクラスで構成されています。最初のブロックは取引レポートを生成するためのもので、前の記事で説明した取引レポートを生成するクラスを改良したものです。
2番目はフィルタクラスです。最適化サンプルを渡された範囲で並び替え、個々の最適化率の値ごとに収益性の高い損失のある取引の頻度を表示するグラフを作成することができます。このクラスの別の目的は、最適化の終了時(すなわち、最適化期間全体にわたったPL)に実際に取引されたPLの正規分布グラフを作成することです。つまり、最適化パスが1,000ある場合、1,000の最適化結果が得られます(最適化終了時のPL)。ここで興味のある分布はそれらに基づいています。
この分布は、得られた値の非対称性がどの方向にシフトしているかを示します。長い尾と分布のセンターが利益ゾーンにある場合、ロボットはほとんど有益な最適化パスを生成し、それに応じて良好です。それ以外の場合は、ほとんど利益の出ないパスが生成されます。定義の非対称性が損失ゾーンにシフトすると、これは選択されたパラメータが主に利益ではなく損失を引き起こすことも意味します。
取引レポートを生成するクラスから始めてこのブロックを見てみましょう。記述されたクラスは、「History manager」フォルダのインクルードディレクトリにあり、次のヘッダを持ちます。
//+------------------------------------------------------------------+ //| 取引履歴統計を生成するクラス | //+------------------------------------------------------------------+ class CReportCreator { public: //============================================================================================================================================= // 計算/再計算 //============================================================================================================================================= void Create(DealDetales &history[],DealDetales &BH_history[],const double balance,const string &Symb[],double r); void Create(DealDetales &history[],DealDetales &BH_history[],const string &Symb[],double r); void Create(DealDetales &history[],const string &Symb[],const double balance,double r); void Create(DealDetales &history[],double r); void Create(const string &Symb[],double r); void Create(double r=0); //============================================================================================================================================= // ゲッター //============================================================================================================================================= bool GetChart(ChartType chart_type,CalcType calc_type,PLChart_item &out[]); // PLグラフを取得 bool GetDistributionChart(bool isOneLot,DistributionChart &out); // 分散グラフを取得 bool GetCoefChart(bool isOneLot,CoefChartType type,CoefChart_item &out[]); // 比率グラフを取得 bool GetDailyPL(DailyPL_calcBy calcBy,DailyPL_calcType calcType,DailyPL &out); // 日ごとのPLグラフを取得 bool GetRatioTable(bool isOneLot,ProfitDrawdownType type,ProfitDrawdown &out); // 極値の表を取得 bool GetTotalResult(TotalResult &out); // TotalResult表を取得 bool GetPL_detales(PL_detales &out); // PL_detales表を取得 void Get_Symb(const DealDetales &history[],string &Symb[]); // 取引された銘柄の配列を取得 void Clear(); // 統計をクリアする private: //============================================================================================================================================= // プライベートデータ型 //============================================================================================================================================= // PLグラフ型の構造体 struct PL_keeper { PLChart_item PL_total[]; PLChart_item PL_oneLot[]; PLChart_item PL_Indicative[]; }; // 日ごとのP/Lグラフ型の構造体 struct DailyPL_keeper { DailyPL avarage_open,avarage_close,absolute_open,absolute_close; }; // 極点の表の構造体 struct RatioTable_keeper { ProfitDrawdown Total_max,Total_absolute,Total_percent; ProfitDrawdown OneLot_max,OneLot_absolute,OneLot_percent; }; // 行内の利益と損失の量を計算するための構造体 struct S_dealsCounter { int Profit,DD; }; struct S_dealsInARow : public S_dealsCounter { S_dealsCounter Counter; }; // 補助的データの計算に使用される構造体 struct CalculationData_item { S_dealsInARow dealsCounter; int R_arr[]; double DD_percent; double Accomulated_DD,Accomulated_Profit; double PL; double Max_DD_forDeal,Max_Profit_forDeal; double Max_DD_byPL,Max_Profit_byPL; datetime DT_Max_DD_byPL,DT_Max_Profit_byPL; datetime DT_Max_DD_forDeal,DT_Max_Profit_forDeal; int Total_DD_numDeals,Total_Profit_numDeals; }; struct CalculationData { CalculationData_item total,oneLot; int num_deals; bool isNot_firstDeal; }; // 比率グラフの作成に使用される構造体 struct CoefChart_keeper { CoefChart_item OneLot_ShartRatio_chart[],Total_ShartRatio_chart[]; CoefChart_item OneLot_WinCoef_chart[],Total_WinCoef_chart[]; CoefChart_item OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[]; CoefChart_item OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[]; CoefChart_item OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[]; }; // 終了日によって取引履歴を並び替えるクラス class CHistoryComparer : public ICustomComparer<DealDetales> { public: int Compare(DealDetales &x,DealDetales &y); }; //============================================================================================================================================= // キーパー: //============================================================================================================================================= CHistoryComparer historyComparer; // 比較クラス CChartComparer chartComparer; // 比較クラス // 補助的構造体 PL_keeper PL,PL_hist,BH,BH_hist; DailyPL_keeper DailyPL_data; RatioTable_keeper RatioTable_data; TotalResult TotalResult_data; PL_detales PL_detales_data; DistributionChart OneLot_PDF_chart,Total_PDF_chart; CoefChart_keeper CoefChart_data; double balance,r; // 初期入金とリスクフリー比率 // 並び変えクラス CGenericSorter sorter; //============================================================================================================================================= // 計算 //============================================================================================================================================= // PLを計算する void CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type); // PLヒストグラムを計算する void CalcPLHist(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type); // プロットに使用する補助的構造体を計算する void CalcData(const DealDetales &deal,CalculationData &out,bool isBH); void CalcData_item(const DealDetales &deal,CalculationData_item &out,bool isOneLot); // 1日のP/Lを計算する void CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal); void cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy); void avarageDay(PLDrawdown &day); // 銘柄を比較する bool isSymb(const string &Symb[],string symbol); // 利益率を計算する void ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot); // 回復率を計算する void RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot); // 勝率を計算する void WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot); // シャープレシオを計算する double ShartRatio_calc(PLChart_item &data[]); void ShartRatio_chart_calc(CoefChart_item &out[],PLChart_item &data[],const DealDetales &deal); // 分布を計算する void NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]); double PDF_calc(double Mx,double Std,double x); // VaRを計算する double VaR(double quantile,double Mx,double Std); // Zスコアを計算する void AltmanZScore_chart_calc(CoefChart_item &out[],double N,double R,double W,double L,const DealDetales &deal); // TotalResult_item構造体を計算する void CalcTotalResult(CalculationData &data,bool isOneLot,TotalResult_item &out); // PL_detales_item構造体を計算する void CalcPL_detales(CalculationData_item &data,int deals_num,PL_detales_item &out); // 日時から日を取得する ENUM_DAY_OF_WEEK getDay(datetime DT); // データをクリアする void Clear_PL_keeper(PL_keeper &data); void Clear_DailyPL(DailyPL &data); void Clear_RatioTable(RatioTable_keeper &data); void Clear_TotalResult_item(TotalResult_item &data); void Clear_PL_detales(PL_detales &data); void Clear_DistributionChart(DistributionChart &data); void Clear_CoefChart_keeper(CoefChart_keeper &data); //============================================================================================================================================= // コピー: //============================================================================================================================================= void CopyPL(const PLChart_item &src[],PLChart_item &out[]); // PLグラフをコピーする void CopyCoefChart(const CoefChart_item &src[],CoefChart_item &out[]); // 比率グラフをコピーする };
このクラスは、以前のバージョンとは異なり、以前の2倍のデータを計算し、より多くのタイプのグラフを作成します。「Create」メソッドのオーバーロードもレポートを計算します。
実際、レポートはCreateメソッド呼び出し時に1回だけ生成されます。その後、「Get」で始まるメソッドで以前に計算されたデータのみが取得されます。一度入力パラメータを反復処理するメインループは、最も引数の多いCreateメソッドにあります。このメソッドは引数を繰り返し処理し、一連のデータを即時に計算します。これに基づいて、必要なすべてのデータが同じ繰り返しで構築されます。
これにより、1回のパスで興味のあるものすべてを構築することができます。グラフを取得するためのこのクラスの以前のバージョンは、初期データを再度繰り返します。その結果、すべての比率の計算はミリ秒で終了しますが、必要なデータを取得するにはさらに時間がかかります。 クラスの「プライベート」部分には、より便利なデータコンテナとしてそのクラス内でのみ使用される一連の構造があります。取引履歴の並び替えは、前述の汎用並び替えメソッドを使用して実行されます。
getterのそれぞれを呼び出すときに取得されるデータについて説明します。
メソッド | パラメータ | チャートの種類 |
---|---|---|
GetChart | chart_type = _PL, calc_type = _Total | PLグラフ — 実際の取引履歴による |
GetChart | chart_type = _PL, calc_type = _OneLot | PLグラフ — 単一ロットを取引した場合 |
GetChart | chart_type = _PL, calc_type = _Indicative | PLグラフ — 指標 |
GetChart | chart_type = _BH, calc_type = _Total | BHグラフ — ロボットとしてロットを管理する場合 |
GetChart | chart_type = _BH, calc_type = _OneLot | BHグラフ — 単一ロットを取引した場合 |
GetChart | chart_type = _BH, calc_type = _Indicative | BHグラフ — 指標 |
GetChart | chart_type = _Hist_PL, calc_type = _Total | PLヒストグラム — 実際の取引履歴による |
GetChart | chart_type = _Hist_PL, calc_type = _OneLot | PLヒストグラム — 単一ロットの取引 |
GetChart | chart_type = _Hist_PL, calc_type = _Indicative | PLヒストグラム — 指標 |
GetChart | chart_type = _Hist_BH, calc_type = _Total | BHヒストグラム — ロボットとしてロットを管理する場合 |
GetChart | chart_type = _Hist_BH, calc_type = _OneLot | BHヒストグラム — 単一ロットを取引した場合 |
GetChart | chart_type = _Hist_BH, calc_type = _Indicative | BHヒストグラム — 指標 |
GetDistributionChart | isOneLot = true | 単一ロットを取引した場合の分布とVaR |
GetDistributionChart | isOneLot = false | 以前と同じ方法で取引した場合の分布とVaR |
GetCoefChart | isOneLot = true, type=_ShartRatio_chart | 単一ロットを取引した場合のシャープレシオ |
GetCoefChart | isOneLot = true, type=_WinCoef_chart | 単一ロットを取引した場合の勝率 |
GetCoefChart | isOneLot = true, type=_RecoveryFactor_chart | 単一ロットを取引した場合の時間による回復率 |
GetCoefChart | isOneLot = true, type=_ProfitFactor_chart | 単一のロットを取引した場合の時間による利益率 |
GetCoefChart | isOneLot = true, type=_AltmanZScore_chart | Z — 単一のロットを取引した場合の時間によるAltmanスコア |
GetCoefChart | isOneLot = false, type=_ShartRatio_chart | 以前と同じ方法で取引した場合のシャープレシオ |
GetCoefChart | isOneLot = false, type=_WinCoef_chart | 以前と同じ方法で取引した場合の勝率 |
GetCoefChart | isOneLot = false, type=_RecoveryFactor_chart | 以前と同じ方法で取引した場合の回復率 |
GetCoefChart | isOneLot = false, type=_ProfitFactor_chart | 以前と同じ方法で取引した場合の利益率 |
GetCoefChart | isOneLot = false, type=_AltmanZScore_chart | Z — 以前と同じ方法で取引した場合のAltmanスコア |
GetDailyPL | calcBy=CALC_FOR_CLOSE, calcType=AVERAGE_DATA | 終了時点での日の平均PL |
GetDailyPL | calcBy=CALC_FOR_CLOSE, calcType=ABSOLUTE_DATA | 終了時点での日の合計PL |
GetDailyPL | calcBy=CALC_FOR_OPEN, calcType=AVERAGE_DATA | 開始時点での日の平均PL |
GetDailyPL | calcBy=CALC_FOR_OPEN, calcType=ABSOLUTE_DATA | 開始時点での日の合計PL |
GetRatioTable | isOneLot = true, type = _Max | 単一ロットを取引した場合 - 取引ごとの最大のP/L |
GetRatioTable | isOneLot = true, type = _Absolute | 単一ロットを取引した場合 - 総P/L |
GetRatioTable | isOneLot = true, type = _Percent | 単一ロットを取引した場合 - 損益% |
GetRatioTable | isOneLot = false, type = _Max | 単一ロットを取引した場合 - 取引ごとの最大のP/L |
GetRatioTable | isOneLot = false, type = _Absolute | 以前と同じ方法で取引した場合 — total profit/loss |
GetRatioTable | isOneLot = false, type = _Percent | 以前と同じ方法で取引した場合 — 損益% |
GetTotalResult | 比率表 | |
GetPL_detales | PL曲線の簡単な要約 | |
Get_Symb | 取引履歴に存在する銘柄の配列 |
PLグラフ — 実際の取引履歴による:
グラフは通常のPLグラフに等しいです。すべてのテスターパスの後で、端末でこれを見ることができます.
PLグラフ — 単一ロットを取引した場合:
このグラフは、前述の取引量が異なるグラフと似ており、あたかも常に単一ロットを取引しているかのように計算されます。エントリ価格とエグジット価格は、EA市場の出入口の合計数で平均価格として計算されます。また、EAによって取引された利益に基づいて取引利益も計算されますが、比率を使用して1つのロットを取引するように得られた利益に変換されます。
PLグラフ — 指標:
正規化されたPLグラフです。PL> 0の場合、PLはこの時点までに達成された最大損失配分で除算されます。さもなければ、PLは今までに達成された最大利益取引で除算されます。
ヒストグラムグラフも同様の方法で構築されます。
分布とVaR:
パラメトリックVaRは絶対データと成長の両方を使用して構築されています。
分布グラフについても同様です。
比率グラフ:
各ループ反復において、この特定の反復で利用可能な履歴全体にわたる適切な方程式に基づいて構築されます。
1日の利益のグラフ:
表に記載されている4つの可能な利益の組み合わせによって構築されています。ヒストグラムのように見えます。
上記のデータを作成する方法は、次のようになります。
//+------------------------------------------------------------------+ //| 比率の計算/再計算 | //+------------------------------------------------------------------+ void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r) { Clear(); // データをクリアする // 残高を保存する this.balance=_balance; if(this.balance<=0) { CDealHistoryGetter dealGetter; this.balance=dealGetter.getBalance(history[ArraySize(history)-1].DT_open); } if(this.balance<0) this.balance=0; // リスクなしの比率を保存する if(_r<0) _r=0; this.r=r; // 補助的構造体 CalculationData data_H,data_BH; ZeroMemory(data_H); ZeroMemory(data_BH); // 取引履歴の並び替え sorter.Method(Sort_Ascending); sorter.Sort<DealDetales>(history,&historyComparer); // 取引履歴のループ for(int i=0;i<ArraySize(history);i++) { if(isSymb(Symb,history[i].symbol)) CalcData(history[i],data_H,false); } // 持ち切りの履歴と適切なループのソート sorter.Sort<DealDetales>(BH_history,&historyComparer); for(int i=0;i<ArraySize(BH_history);i++) { if(isSymb(Symb,BH_history[i].symbol)) CalcData(BH_history[i],data_BH,true); } // 1日の平均PL (平均されたタイプ) avarageDay(DailyPL_data.avarage_close.Mn); avarageDay(DailyPL_data.avarage_close.Tu); avarageDay(DailyPL_data.avarage_close.We); avarageDay(DailyPL_data.avarage_close.Th); avarageDay(DailyPL_data.avarage_close.Fr); avarageDay(DailyPL_data.avarage_open.Mn); avarageDay(DailyPL_data.avarage_open.Tu); avarageDay(DailyPL_data.avarage_open.We); avarageDay(DailyPL_data.avarage_open.Th); avarageDay(DailyPL_data.avarage_open.Fr); // 損益率の表に記入する RatioTable_data.data_H.oneLot.Accomulated_Profit; RatioTable_data.data_H.oneLot.Accomulated_DD; RatioTable_data.data_H.oneLot.Max_Profit_forDeal; RatioTable_data.data_H.oneLot.Max_DD_forDeal; RatioTable_data.data_H.oneLot.Total_Profit_numDeals/data_H.num_deals; RatioTable_data.data_H.oneLot.Total_DD_numDeals/data_H.num_deals; RatioTable_data.Total_absolute.Profit=data_H.total.Accomulated_Profit; RatioTable_data.Total_absolute.Drawdown=data_H.total.Accomulated_DD; RatioTable_data.Total_max.Profit=data_H.total.Max_Profit_forDeal; RatioTable_data.Total_max.Drawdown=data_H.total.Max_DD_forDeal; RatioTable_data.Total_percent.Profit=data_H.total.Total_Profit_numDeals/data_H.num_deals; RatioTable_data.Total_percent.Drawdown=data_H.total.Total_DD_numDeals/data_H.num_deals; // 正規分布を計算する NormalPDF_chart_calc(OneLot_PDF_chart,PL.PL_oneLot); NormalPDF_chart_calc(Total_PDF_chart,PL.PL_total); // TotalResult CalcTotalResult(data_H,true,TotalResult_data.oneLot); CalcTotalResult(data_H,false,TotalResult_data.total); // PL_detales CalcPL_detales(data_H.oneLot,data_H.num_deals,PL_detales_data.oneLot); CalcPL_detales(data_H.total,data_H.num_deals,PL_detales_data.total); }
実装からわかるように、データの一部はループが履歴を通過するときに計算され、一部のデータは構造体のデータ(CalculationData data_H、data_BH\)に基づいてすべてのループを通過した後に計算されます。
CalcDataメソッドは、Createメソッドと同様の方法で実装されています。これは、各反復で計算を実行するはずのメソッドを呼び出す唯一のメソッドです。最終データを計算するすべてのメソッドは、上記の構造に含まれる情報に基づいて計算されます。記載された構造体への記入/再記入は、以下のメソッドによって行われます。
//+------------------------------------------------------------------+ //| 補助的データを計算する | //+------------------------------------------------------------------+ void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out, bool isOneLot) { double pl=(isOneLot ?deal.pl_oneLot : deal.pl_forDeal); // PL int n=0; // 損益額 if(pl>=0) { out.Total_Profit_numDeals++; n=1; out.dealsCounter.Counter.DD=0; out.dealsCounter.Counter.Profit++; } else { out.Total_DD_numDeals++; out.dealsCounter.Counter.DD++; out.dealsCounter.Counter.Profit=0; } out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD); out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit); // 損益のシリーズ int s=ArraySize(out.R_arr); if(!(s>0 && out.R_arr[s-1]==n)) { ArrayResize(out.R_arr,s+1,s+1); out.R_arr[s]=n; } out.PL+=pl; // 総合的PL // 最大利益 / DD if(out.Max_DD_forDeal>pl) { out.Max_DD_forDeal=pl; out.DT_Max_DD_forDeal=deal.DT_close; } if(out.Max_Profit_forDeal<pl) { out.Max_Profit_forDeal=pl; out.DT_Max_Profit_forDeal=deal.DT_close; } // 累積利益/DD out.Accomulated_DD+=(pl>0 ?0 : pl); out.Accomulated_Profit+=(pl>0 ?pl : 0); // 利益による極値 double maxPL=MathMax(out.Max_Profit_byPL,out.PL); if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// データの保存に確認があと1つ必要 { out.DT_Max_Profit_byPL=deal.DT_close; out.Max_Profit_byPL=maxPL; } double maxDD=out.Max_DD_byPL; double DD=0; if(out.PL>0)DD=out.PL-maxPL; else DD=-(MathAbs(out.PL)+maxPL); maxDD=MathMin(maxDD,DD); if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// データの保存に確認があと1つ必要 { out.Max_DD_byPL=maxDD; out.DT_Max_DD_byPL=deal.DT_close; } out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ?maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0)); }
これは、各計算メソッドのすべての入力データを計算する基本的なメソッドです。このアプローチ(入力データのこのメソッドへの計算の移動)は、クラスの以前のバージョンで発生した履歴ループでの過度のパスを回避し、トレーディングレポートを作成します。このメソッドは、CalcDataメソッドの内部で呼び出されます。
最適化パス結果フィルタのクラスには、次のヘッダがあります。
//+--------------------------------------------------------------------------+ //| 最適化パスをデータベースからアンロードした後に並び替えするクラス | //+--------------------------------------------------------------------------+ class CParamsFiltre { public: CParamsFiltre(){sorter.Method(Sort_Ascending);} // デフォルトのコンストラクタ int Total(){return ArraySize(arr_main);}; // アンロードされたパラメータの総数(最適化データ表による) void Clear(){ArrayFree(arr_main);ArrayFree(arr_result);}; // すべての配列をクリアする void Add(LotDependency_item &customCoef,CDataKeeper ¶ms[],long ID,double total_PL,bool addToResult); // 配列に新しい値を追加する double GetCustomCoef(long ID,bool isOneLot);// IDによってカスタム比を取得する void GetParamNames(CArrayString &out);// EAパラメータ名を取得する void Get_UniqueCoef(UniqCoefData_item &data[],string paramName,CArrayString &coefValue); // 一意の比率を取得する void Filtre(string Name,string from,string till,long &ID_Arr[]);// arr_result配列を並び変える void ResetFiltre(long &ID_arr[]);// 絞り込みをリセットする bool Get_Distribution(Chart_item &out[],bool isMainTable);// 両方の配列で分布を作成する bool Get_Distribution(Chart_item &out[],string Name,string value);// 選択されたデータで分布を作成する private: CGenericSorter sorter; // ソーター CCoefComparer cmp_coef;// 比率を比較する CChartComparer cmp_chart;// グラフを比較する bool selectCoefByName(CDataKeeper &_input[],CDataKeeper &out,string Name);// 名前で比率を選択する double Mx(CoefStruct &_arr[]);// 算術平均 double Std(CoefStruct &_arr[],double _Mx);// 標準偏差 CoefStruct arr_main[]; // 最適化データ表の同等 CoefStruct arr_result[];// 結果表の同等 };
クラスの構造を分析し、いくつかのメソッドについてより詳しく説明します。ご覧のように、クラスには2つのグローバル配列arr_mainとarr_resultがあります。配列は、最適化のデータを格納します。最適化を使用してデータベースから表をアンロードした後、データベースは2つの表に分割されます。
- main - 条件付き並び替え中に破棄されたデータを除き、すべてのアンロードデータが取得されます。
- result — 最初に選択された最良のN個のデータが得られます。その後、記述されたクラスはこの特定の表をソートし、それに応じてそのエントリの数を減らしたり、リセットしたりします。
記述された配列は、EAのIDとパラメータ、および配列名に従って上記の表の他のデータを格納します。本質的に、このクラスは2つの機能を実行します。1つ目は表を使用した操作のための便利なデータストレージであること、2つ目は選択された最適化パスの結果の表を並び替えることです。並び替えクラスと2つの比較クラスは、上記の配列の並び替えプロセスと、記述された表に従って構築された分布の並び替えに関係しています。
このクラスはEA比、つまりCdataKeeperクラスの形式での表現で動作するため、プライベートメソッドselectCoefByNameが作成されます。1つの必要な比率を選択し、1つの特定の最適化パスの渡されたEA比率の配列から参照によって結果を返します。
Addメソッドは、addToResult == trueの場合はデータベースにアップロードされた行を両方の配列に追加、または、addToResult == falseの場合はarr_main配列のみに追加します。IDは各最適化パスの一意のパラメータであるため、特定の選択されたパスの定義に関するすべての作業は、それに基づいています。提供された配列のうち、このパラメータのユーザ計算された比率が得られます。評価はプログラム参加なしでEA最適化の間に計算されるので、プログラム自体はカスタム評価を計算するための方程式を知りません。このため、これらの配列にカスタム評価を保存する必要があります。リクエストされたら、渡されたIDでGetCustomCoefメソッドを使用して取得します。
最も重要なクラスメソッドは次のとおりです。
- Filtre — 渡された範囲(/から)までの選択された比率の値を含むように、結果表を並び替えます。
- ResetFiltre — 並び替えされた情報全体をリセットします。
- Get_Distribution(Chart_item &out[],bool isMainTable) — isMainTableパラメータを使用して指定された選択された表に従って、実際に取引されるPLによる配信を構築します。
- Get_Distribution(Chart_item &out[],string Name,string value) — 選択されたパラメータ(Name)が渡された値(value)と等しい新しい配列を作成します。つまり、arr_result配列に沿ったパスはループで実行されます。ループの各反復中、関心のあるパラメータは、すべてのEAパラメータの中からその名前(selectCoefByName関数を使用)によって選択されます。また、その値が必要な値(value)と等しいかどうかが確認されます。等しい場合、arr_result配列の値が一時配列に追加されます。次に、一時配列による分布が作成され、返されます。言い換えれば、これは、名前によって選択されたパラメータの値が渡された値と等しいことが検出されたすべての最適化パスを選択する方法です。これは、この特定のパラメータがEA全体にどれだけ影響を与えるかを推定するために必要です。記述されたクラスの実装はコードで適切にコメントされているため、ここではこれらのメソッドの実装を提供しません。
プレゼンター
プレゼンターはコネクタとして機能します。これは、アプリケーションのグラフィックレイヤと前述のロジックとの間のリンクの一種です。このアプリケーションでは、プレゼンターは抽象(IPresenterインターフェイス)を使用して実装されます。このインターフェイスには、必要なコールバックメソッドの名前が含まれています。 それらは、必要なインターフェイスを継承するプレゼンタクラスで実装されます。この区別は、アプリケーションを完成させるために作成されました。プレゼンターブロックを書き換える必要がある場合は、グラフィックブロックまたはアプリケーションロジックに影響を与えずに簡単に実行できます。記載されたインターフェイスは、以下のように表されます。
//+------------------------------------------------------------------+ //| プレゼンターインターフェイス | //+------------------------------------------------------------------+ interface IPresenter { void Btn_Update_Click(); // データをダウンロードしてフォーム全体を構築する void Btn_Load_Click(); // レポートを作成する void OptimisationData(bool isMainTable);// 表の最適化行を選択する void Update_PLByDays(); // 日ごとの損益をアップロードする void DaySelect();// PL tableから1日を選択する void PL_pressed(PLSelected_type type);// 選択した履歴のPLグラフを構築する void PL_pressed_2(bool isRealPL);// 「他のチャート」グラフを構築する void SaveToFile_Click();// データファイルを(サンドボックスに)保存する void SaveParam_passed(SaveParam_type type);// ファイルに書き出す日付を選択する void OptimisationParam_selected(); // 最適化パラメータを選択して「Optimisation selection」表に記入する void CompareTables(bool isChecked);// 結果表で分布を構築する(共通(メイン)表との相関) void show_FriquencyChart(bool isChecked);// 損益の頻度グラフを表示する void FriquencyChart_click();// 比率表の行を選択して分布を作成する void Filtre_click();// 選択した条件で並び替える void Reset_click();// 絞り込みをリセットする void PL_pressed_3(bool isRealPL);// 結果表のすべてのデータによって損益グラフを構築する void PL_pressed_4(bool isRealPL);// 統計表を構築する void setChartFlag(bool isPlot);// PL_pressed_3(bool isRealPL)メソッドからグラフを構築する(または構築しない)条件 };
プレゼンタークラスは、必要なインターフェイスを実装しており、次のようになります。
class CPresenter : public IPresenter { public: CPresenter(CWindowManager *_windowManager); // コンストラクタ void Btn_Update_Click();// データをダウンロードしてフォーム全体を構築する void Btn_Load_Click();// レポートを作成する void OptimisationData(bool isMainTable);// 表の最適化行を選択する void Update_PLByDays();// 日ごとの損益をアップロードする void PL_pressed(PLSelected_type type);// 選択した履歴のPLグラフを構築する void PL_pressed_2(bool isRealPL);// 「他のチャート」グラフを構築する void SaveToFile_Click();// データファイルを(サンドボックスに)保存する void SaveParam_passed(SaveParam_type type);// ファイルに書くデータを選択する void OptimisationParam_selected();// 最適化パラメータを選択して「Optimisation selection」表に記入する void CompareTables(bool isChecked);// 結果表で分布を構築する(共通(メイン)表との相関) void show_FriquencyChart(bool isChecked);// 損益の頻度グラフを表示する void FriquencyChart_click();// 比率表の行を選択して分布を作成する void Filtre_click();// 選択した条件で並び替える void PL_pressed_3(bool isRealPL);// 結果表のすべてのデータによって損益グラフを構築する void PL_pressed_4(bool isRealPL);// 統計表を構築する void DaySelect();// PL表の日を選ぶ void Reset_click();// フィルタをリセットする void setChartFlag(bool isPlot);// PL_pressed_3(bool isRealPL) メソッドからグラフを構築する(または構築しない)条件 private: CWindowManager *windowManager;// ウィンドウクラスへの参照 CDBReader dbReader;// データベース連携クラス CReportCreator reportCreator; // データ処理クラス CGenericSorter sorter; // 並び変えクラス CoefData_comparer coefComparer; // データ比較クラス void loadData();// データベースからデータをアップロードして表を記入する void insertDataTo_main_Table(bool isResult,const CoefData_item &data[]); // 結果表と「メイン」表(最適化パス比を持つ表)にデータを挿入する void insertRowTo_main_Table(CTable *tb,int n,const CoefData_item &data); // 最適化パス表への直接データ挿入 void selectChartByID(long ID,bool recalc=true);// IDによってグラフを選択する void createReport();// レポートを作成する string getCorrectPath(string path,string name);// ファイルへの正しいパスを取得する bool getPLChart(PLChart_item &data[],bool isOneLot,long ID); bool curveAdd(CGraphic *chart_ptr,const PLChart_item &data[],bool isHist);// グラフを他のチャートに追加する bool curveAdd(CGraphic *chart_ptr,const CoefChart_item &data[],double borderPoint);// グラフを他のチャートに追加する bool curveAdd(CGraphic *chart_ptr,const Distribution_item &data);// グラフを他のチャートに追加する void setCombobox(CComboBox *cb_ptr,CArrayString &arr,bool isFirstIndex=true);// コンボボックスパラメータを設定する void addPDF_line(CGraphic *chart_ptr,double &x[],color clr,int width,string _name=NULL);// 分布グラフの滑らかな線を追加する void plotMainPDF();// 「メイン」表(最適化データ)によって分布を構築する) void updateDT(CDropCalendar *dt_ptr,datetime DT);// ドロップダウンカレンダーを更新する CParamsFiltre coefKeeper;// 最適化パスを分布別に並び替える CArrayString headder; // 比率表のヘッダ bool _isUpbateClick; // 更新ボタンの押下とデータベースからのデータアップロードのフラグ long _selectedID; // 選択されたすべてのPLグラフのID(損失の場合は赤、収益の場合は緑) long _ID,_ID_Arr[];// データをアップロードした後に「Result」表に選択されたIDの配列 bool _IsForvard_inTables,_IsForvard_inReport; // 最適化パスの表の最適化データ型のフラグ datetime _DT_from,_DT_till; double _Gap; // 以前に選択した最適化グラフの追加されたギャップの保存された種類(スプレッド拡張/スリップシミュレーション...) };
それぞれのコールバックは非常によくコメントされているので、ここでは説明しません。これは、アプリケーションのすべてのフォーム動作が実装されている部分だと言うことが必要なだけです。それは、グラフの構築、コンボボックスの記入、データベースからのデータのアップロードおよび処理方法の呼び出し、また、さまざまなクラスを接続する他の操作を含みます。
終わりに
テスターを通過する可能性のあるすべての最適化パラメータを使用して表を処理するアプリケーションを開発しました。また、すべての最適化パスをデータベースに保存する機能をEAに追加しました。関心のあるパラメータを選択すると詳細な取引レポートが得られるのに加えて、最適化履歴全体から選択した一定期間を徹底的に見たり、同期間のすべての比率を見ることができます。Gapパラメータを増加させてスリッページをシミュレートし、グラフや比率の動作への影響を確認することもできます。もう1つの追加は、特定の間隔の比率値によって最適化結果を並び変えるる機能です。
上位100件の最適化パスを取得する最も簡単な方法は、CDBWriterクラスをサンプルEAと同じように(添付ファイル内)接続し、条件付きフィルタを設定(たとえば、Profit Factor> = 1で直ちに負けトレードの組み合わせを除外)し、「Update」をクリックして「Show n params」パラメータを100のままにすることです。この場合、(フィルターに応じて)上位100件の最適化パスが結果表に表示されます。結果として得られるアプリケーションの各オプションと、より洗練された比率の選択方法については、次の記事で詳しく説明します。
本稿には以下のファイルが添付されています。
Experts/2MA_Martin — テストEAプロジェクト
- 2MA_Martin.mq5 — EAテンプレートコード最適化データをデータベースに保存するDBWriter.mqhファイルが含まれています。
- Robot.mq5 — EA論理
- Robot.mqh — Robot.mq5ファイルで実装されているヘッダファイル
- Trade.mq5 — EA取引論理
- Trade.mqh — Trade.mq5ファイルで実装されているヘッダファイル
Experts/OptimisationSelector — 説明されたアプリケーションプロジェクト
- OptimisationSelector.mq5 — プロジェクトコード全体を呼び出すEAのテンプレート
- ParamsFiltre.mq5 — フィルタと結果表による分布
- ParamsFiltre.mqh — ParamsFiltre.mq5ファイルで実装されているヘッダファイル
- Presenter.mq5 — プレゼンター
- Presenter.mqh — Presenter.mq5ファイルで実装されているヘッダファイル
- Presenter_interface.mqh — プレゼンターインターフェイス
- Window_1.mq5 — グラフィックス
- Window_1.mqh — Window_1.mq5ファイルで実装されているヘッダファイル
Include/CustomGeneric
- GenericSorter.mqh — データの並び替え
- ICustomComparer.mqh — ICustomSorterインターフェイス
Include/History manager
- DealHistoryGetter.mqh — 端末から取引履歴をアンロードし、必要なビューに変換する
- ReportCreator.mqh — 取引履歴を作成するクラス
Include/OptimisationSelector
- DataKeeper.mqh — 比率名に関連付けられたEAの比率を格納するクラス
- DBReader.mqh — データベースから必要な表を読み込むクラス
- DBWriter.mqh — データベースに書き込むクラス
Include/Sqlite3
- sqlite_amalgmation.mqh — データベースに連携するためのインポート関数
- SqliteManager.mqh — データベースコネクタと文のクラス
- SqliteReader.mqh — データベースから読み取るクラス
- memcpy.mqh — memcpy 関数をインポートする
- Mutex.mqh — Mutex作成関数をインポートする
- strcpy.mqh — strcpy関数をインポートする
- strlen.mqh — strlen関数をインポートする
ライブラリ
- Sqlite3_32.DLL — DLL Sqlite for 32-bit terminals
- Sqlite3_64.DLL — DLL Sqlite for 64-bit terminals
テストデータベース
- 2MA_Martin optimisation data - Sqlite database