
グリッドおよびマーチンゲール取引システムでの機械学習 - あなたはそれに賭けますか
はじめに
外国為替市場のパターンを見つけることを目的とした機械学習を使用するためのさまざまなアプローチの研究に懸命に取り組んできました。モデルを訓練して実装する方法はすでにわかっています。しかし、取引には多数のアプローチがあり、そのほとんどすべてが最新の機械学習アルゴリズムを適用することで改善できます。最も人気のあるアルゴリズムの1つは、グリッドやマーチンゲールです。本稿を書く前に、私はインターネットで関連情報を検索して、少し探索的分析を行いました。驚いたことに、世界中のネットではこのアプローチはほとんどまたはまったくカバーされていません。そのような解決策の見通しについてコミュニティメンバーにアンケートしたところ、大多数は、このトピックへのアプローチ方法すら知らないがアイデア自体は面白そうだと答えました。ただし、アイデア自体は非常に単純に見えます。
2つの目的で一連の実験を行いましょう。1つ目はこれが一見したほど難しくないことを証明することです。2つ目はこのアプローチが適用可能で効果的かどうかを調べることです。
取引のラベル付け
主なタスクは、取引に正しくラベルを付けることです。以前の記事でこれが単一のポジションに対してどのように行われたかを思い出してみましょう。たとえば、15バーなど、取引のランダムまたは決定論的な範囲を設定します。市場がこれらの15バーで上昇している場合、取引は買いとしてラベル付けされ、そうでない場合は売りでした。注文のグリッドにも同様のロジックが使用されますが、ここでは、ポジションのグループの合計利益/損失を考慮する必要があります。これは簡単な例で説明できます。図を描いてみました。
取引期間が15バーであると仮定します(従来の時間スケールで垂直の赤線でマークされています)。単一のポジションが使用されている場合、市場はポイント間で伸びているため、買(斜めの緑色の一点鎖線)としてラベル付けされます。この市場は黒い破線の曲線として示されています。
このようなラベル付けでは、中間の市場変動は無視されます。注文のグリッド(赤と緑の水平線)を適用する場合、最初に開かれた注文を含む、トリガーされたすべての指値注文の合計利益を計算する必要があります(ポジションを開いてグリッドを同じ方向に配置することも、オプションで、ポジションを開かずに指値注文のグリッドをすぐに配置することもできます)。このようなラベル付けは、学習履歴の期間全体にわたって、スライディングウィンドウで継続されます。ML(機械学習)のタスクは、さまざまな状況全体を一般化し、新しいデータを効率的に予測することです(可能な場合)。
この場合、取引方向を選択し、データをラベル付けするためのオプションはいくつかがあります。ここでの選択タスクは、哲学的かつ実験的です。
- 最大総利益による選択 - 売りグリッドがより多くの利益を生み出す場合、このグリッドにはラベルが付けられます。
- 未決済注文数と総利益の間の加重選択 - グリッド内の各未決済注文の平均利益が反対側の平均利益よりも高い場合は、こちら側が選択されます。
- トリガーされた注文の最大数による選択 - 目的のロボットはグリッドを取引する必要があるため、このオプションは妥当に見えます。トリガーされた注文の数が最大で、ポジションの合計が利益になっている場合、こちら側が選択されます。ここでの側面は、グリッドの方向(売または買)を意味します。
これらの3つの基準は、始めるには十分に思えます。最初のものは最も単純で最大の利益を目指しているので、詳細に考えてみましょう。
コードでの取引のラベル付け
以前の記事で取引がどのようにラベル付けされたかを思い出してみましょう。
def add_labels(dataset, min, max): labels = [] for i in range(dataset.shape[0]-max): rand = random.randint(min, max) curr_pr = dataset['close'][i] future_pr = dataset['close'][i + rand] if future_pr + MARKUP < curr_pr: labels.append(1.0) elif future_pr - MARKUP > curr_pr: labels.append(0.0) else: labels.append(2.0) dataset = dataset.iloc[:len(labels)].copy() dataset['labels'] = labels dataset = dataset.dropna() dataset = dataset.drop( dataset[dataset.labels == 2].index).reset_index(drop=True) return dataset
このコードは、通常のグリッドとマーチンゲールグリッドに一般化する必要があります。もう1つのエキサイティングな機能は、注文数や注文間の距離が異なるグリッドを探索したり、マーチンゲールを適用したりできることです(ロット増加)。
これを行うには、後で使用および最適化できるグローバル変数を追加しましょう。
GRID_SIZE = 10 GRID_DISTANCES = np.full(GRID_SIZE, 0.00200) GRID_COEFFICIENTS = np.linspace(1, 3, num= GRID_SIZE)
GRID_SIZE変数には、両方向に注文した場合の番号が含まれます。
GRID_DISTANCESは、注文間の距離を設定します。距離は固定または可変(すべての注文で異なります)にすることができます。これにより、取引システムの柔軟性が向上します。
GRID_COEFFICIENTS変数には、各注文のロット乗数が含まれています。それらが一定の場合、システムは通常のグリッドを作成します。ロットが異なる場合は、マルチンゲールまたは逆マーチンゲール、あるいはロット乗数が異なるグリッドに適用可能なその他の名前になります。
以下はnumpyライブラリを初めて使用する方のためのものです。
- np.fullは、指定された数の同一の値で配列に入力します。
- np.linspaceは、2つの実数の間で均等に分散される指定された数の値を配列に入力します。上記の例では、GRID_COEFFICIENTSには次のものが含まれます。
array([1. , 1.22222222, 1.44444444, 1.66666667, 1.88888889, 2.11111111, 2.33333333, 2.55555556, 2.77777778, 3. ])
したがって、最初のロット乗数は1に等しくなるため、このロットは取引システムパラメータで指定された基本ロットに等しくなります。グリッドの次の注文では1から3までの乗数が連続して使用されます。このグリッドをすべての注文に対して固定乗数で使用するには、np.fullを呼び出します。
トリガーされた注文とトリガーされていない注文の説明はやや難しい場合があるため、何らかのデータ構造を作成する必要があります。特定のケース(サンプル)ごとに注文とポジションを記録するための辞書を作成することにしました。代わりに、データクラスオブジェクト、Pandasデータフレーム、numpy構造化配列のいずれかを使用できます。おそらく最後の解決策が最速でしょうが、ここでは重要ではありません。
注文グリッドに関する情報を格納する辞書は、訓練セットにサンプルを追加するたびに作成されます。説明が必要かもしれません。grid_stats辞書には、現在の注文グリッドの開始から終了までに必要なすべての情報が含まれています。
def add_labels(dataset, min, max, distances, coefficients): labels = [] for i in range(dataset.shape[0]-max): rand = random.randint(min, max) all_pr = dataset['close'][i:i + rand + 1] grid_stats = {'up_range': all_pr[0] - all_pr.min(), 'dwn_range': all_pr.max() - all_pr[0], 'up_state': 0, 'dwn_state': 0, 'up_orders': 0, 'dwn_orders': 0, 'up_profit': all_pr[-1] - all_pr[0] - MARKUP, 'dwn_profit': all_pr[0] - all_pr[-1] - MARKUP } for i in np.nditer(distances): if grid_stats['up_state'] + i <= grid_stats['up_range']: grid_stats['up_state'] += i grid_stats['up_orders'] += 1 grid_stats['up_profit'] += (all_pr[-1] - all_pr[0] + grid_stats['up_state']) \ * coefficients[int(grid_stats['up_orders']-1)] grid_stats['up_profit'] -= MARKUP * coefficients[int(grid_stats['up_orders']-1)] if grid_stats['dwn_state'] + i <= grid_stats['dwn_range']: grid_stats['dwn_state'] += i grid_stats['dwn_orders'] += 1 grid_stats['dwn_profit'] += (all_pr[0] - all_pr[-1] + grid_stats['dwn_state']) \ * coefficients[int(grid_stats['dwn_orders']-1)] grid_stats['dwn_profit'] -= MARKUP * coefficients[int(grid_stats['dwn_orders']-1)] if grid_stats['up_profit'] > grid_stats['dwn_profit'] and grid_stats['up_profit'] > 0: labels.append(0.0) continue elif grid_stats['dwn_profit'] > 0: labels.append(1.0) continue labels.append(2.0) dataset = dataset.iloc[:len(labels)].copy() dataset['labels'] = labels dataset = dataset.dropna() dataset = dataset.drop( dataset[dataset.labels == 2].index).reset_index(drop=True) return dataset
all_pr変数には、現在から将来までの価格が含まれています。グリッド自体を計算する必要があります。グリッドを構築するには、最初のバーから最後のバーまでの価格範囲を知りたいと思います。これらの値は、「up_range」および「dwn_range」辞書エントリに含まれています。変数「up_profit」および「dwn_profit」には、現在の履歴セグメントでの買いまたは売りのグリッドの使用による最終利益が含まれます。これらの値は、最初に開かれた1つの市場取引から受け取った利益で初期化されます。次に、指値注文がトリガーされた場合、グリッドに従って開かれた取引と合計されます。
次に、すべてのGRID_DISTANCESを反復処理して、未決の指値注文がトリガーされたかどうかを確認する必要があります。注文がup_rangeまたはdwn_rangeの範囲内にある場合、注文はトリガーされています。この場合、最後にアクティブ化された注文のレベルを格納する適切なup_stateおよびdwn_stateカウンターを増加します。次の反復で、グリッド内の新しい注文までの距離がこのレベルに追加されます。この注文が価格範囲内にある場合は、それもトリガーされています。
トリガーされたすべての注文について、追加情報が書き込まれます。たとえば、指値注文の利益が合計値に追加されます。買いポジションの場合、この利益は次の式を使用して計算されます。ここで、ポジションの始値が最後の価格(ポジションが決済することになっている価格)から差し引かれ、シリーズから選択された未決注文までの距離が加算され、結果にグリッド内のこの注文のロット乗数が乗算されます。売り注文には逆の計算が使用されます。累積マークアップが追加で計算されます。
grid_stats['up_profit'] += (all_pr[-1] - all_pr[0] + grid_stats['up_state']) \ * coefficients[int(grid_stats['up_orders']-1)] grid_stats['up_profit'] -= MARKUP * coefficients[int(grid_stats['up_orders']-1)]
コードの次のブロックは、売買グリッドの利益を確認します。累積マークアップを考慮した利益がゼロより大きく、最大である場合、対応するサンプルが訓練セットに追加されます。いずれの条件も満たされない場合は、2.0マークが追加されます。このマークが付いたサンプルは、情報がないと見なされて訓練データセットから削除されます。これらの条件は、必要なグリッド構築オプションに応じて、後で変更できます。
注文グリッドで動作するようにテスターをアップグレードする
グリッドの取引から得られる利益を正しく計算するには、ストラテジーテスターを変更する必要があります。MetaTrader 5テスターに似て、相場の履歴を順番にループし、実際の取引であるかのように取引を開始および終了することにしました。これにより、コードの理解が向上し、将来を覗き見することがなくなります。コードの要点に焦点を当てようと思います。古いテスターバージョンはここでは提供しませんが、以前の記事で見つけることができます。読者の中には、詳細を検討することなくすぐに聖杯を手に入れたいと思っていて、以下のコードを理解できない人もいるかもしれませんが、要点は明確だと思います。
def tester(dataset, markup, distances, coefficients, plot=False): last_deal = int(2) all_pr = np.array([]) report = [0.0] for i in range(dataset.shape[0]): pred = dataset['labels'][i] all_pr = np.append(all_pr, dataset['close'][i]) if last_deal == 2: last_deal = 0 if pred <= 0.5 else 1 continue if last_deal == 0 and pred > 0.5: last_deal = 1 up_range = all_pr[0] - all_pr.min() up_state = 0 up_orders = 0 up_profit = (all_pr[-1] - all_pr[0]) - markup report.append(report[-1] + up_profit) up_profit = 0 for d in np.nditer(distances): if up_state + d <= up_range: up_state += d up_orders += 1 up_profit += (all_pr[-1] - all_pr[0] + up_state) \ * coefficients[int(up_orders-1)] up_profit -= markup * coefficients[int(up_orders-1)] report.append(report[-1] + up_profit) up_profit = 0 all_pr = np.array([dataset['close'][i]]) continue if last_deal == 1 and pred < 0.5: last_deal = 0 dwn_range = all_pr.max() - all_pr[0] dwn_state = 0 dwn_orders = 0 dwn_profit = (all_pr[0] - all_pr[-1]) - markup report.append(report[-1] + dwn_profit) dwn_profit = 0 for d in np.nditer(distances): if dwn_state + d <= dwn_range: dwn_state += d dwn_orders += 1 dwn_profit += (all_pr[0] + dwn_state - all_pr[-1]) \ * coefficients[int(dwn_orders-1)] dwn_profit -= markup * coefficients[int(dwn_orders-1)] report.append(report[-1] + dwn_profit) dwn_profit = 0 all_pr = np.array([dataset['close'][i]]) continue y = np.array(report).reshape(-1, 1) X = np.arange(len(report)).reshape(-1, 1) lr = LinearRegression() lr.fit(X, y) l = lr.coef_ if l >= 0: l = 1 else: l = -1 if(plot): plt.figure(figsize=(12,7)) plt.plot(report) plt.plot(lr.predict(X)) plt.title("Strategy performance") plt.xlabel("the number of trades") plt.ylabel("cumulative profit in pips") plt.show() return lr.score(X, y) * l
歴史的に、グリッドトレーダーが感心があるのは残高カーブのみでエクイティカーブは無視されるので、ここではこの伝統を守り、複雑なテスターを過度に複雑にすることはありません。残高グラフのみを表示します。さらに、エクイティカーブはいつでもMetaTrader5ターミナルで見ることができます。
すべての価格をループして、それらをall_pr配列に追加します。上記の3つのオプションがあります。このテスターは以前の記事で検討されたため、反対のシグナルが表示されたときに注文グリッドを閉じるためのオプションについてのみ説明します。取引にラベルを付けるときと同じように、up_range変数は、ポジションを決済するまでの反復価格の範囲を格納します。次に、最初のポジション(市場によって開かれた)の利益が計算されます。次に、サイクルはトリガーされた指値注文の存在を確認します。そのような注文がある場合は、その結果が残高グラフに追加されます。同じことが売り注文/ポジションに対しても実行されます。したがって、残高グラフは、グループごとの総利益ではなく、すべての決済済みポジションを反映します。
注文グリッドを操作するための新しい方法のテスト
機械学習のためのデータ準備はお馴染みのものです。最初に価格と一連の機能を取得し、次にデータにラベルを付け(BuyおよびSellラベルを作成)、カスタムテスターでラベルを確認します。
# Get prices and labels and test it pr = get_prices(START_DATE, END_DATE) pr = add_labels(pr, 15, 15, GRID_DISTANCES, GRID_COEFFICIENTS) tester(pr, MARKUP, GRID_DISTANCES, GRID_COEFFICIENTS, plot=True)
次に、CatBoostモデルを訓練し、新しいデータでテストする必要があります。ガウス混合モデルによって生成された合成データの訓練はうまく機能するため、再度使用することにしました。
# Learn and test CatBoost model gmm = mixture.GaussianMixture( n_components=N_COMPONENTS, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]]) res = [] for i in range(10): res.append(brute_force(10000)) print('Iteration: ', i, 'R^2: ', res[-1][0]) res.sort() test_model(res[-1])
この例では、生成された10,000個のサンプルで10個のモデルを訓練し、決定係数から最適なモデルを選択します。学習プロセスは次のとおりです。
Iteration: 0 R^2: 0.8719436661855786 Iteration: 1 R^2: 0.912006346274096 Iteration: 2 R^2: 0.9532278725035132 Iteration: 3 R^2: 0.900845571741786 Iteration: 4 R^2: 0.9651728908727953 Iteration: 5 R^2: 0.966531822300101 Iteration: 6 R^2: 0.9688263099200539 Iteration: 7 R^2: 0.8789927823514787 Iteration: 8 R^2: 0.6084261786804662 Iteration: 9 R^2: 0.884741078512629
ほとんどのモデルは、新しいデータで高い決定係数を持っており、モデルの安定性が高いことを示しています。これは、訓練データと訓練外データの結果の残高グラフです。
よさそうです。次に、訓練されたモデルをMetaTrader 5でエクスポートし、ターミナルテスターで結果を確認します。テストの前に、取引エキスパートアドバイザーとインクルードファイルを準備する必要があります。訓練された各モデルには独自のファイルがあるため、簡単に保存および変更できます。
CatBoostモデルをMQL5にエクスポートする
次の関数を呼び出して、モデルをエクスポートします。
export_model_to_MQL_code(res[-1][1])
関数が少し変更されました。この変更の説明は以下のとおりです。
def export_model_to_MQL_code(model): model.save_model('catmodel.h', format="cpp", export_parameters=None, pool=None) # add variables code = '#include <Math\Stat\Math.mqh>' code += '\n' code += 'int MAs[' + str(len(MA_PERIODS)) + \ '] = {' + ','.join(map(str, MA_PERIODS)) + '};' code += '\n' code += 'int grid_size = ' + str(GRID_SIZE) + ';' code += '\n' code += 'double grid_distances[' + str(len(GRID_DISTANCES)) + \ '] = {' + ','.join(map(str, GRID_DISTANCES)) + '};' code += '\n' code += 'double grid_coefficients[' + str(len(GRID_COEFFICIENTS)) + \ '] = {' + ','.join(map(str, GRID_COEFFICIENTS)) + '};' code += '\n' # get features code += 'void fill_arays( double &features[]) {\n' code += ' double pr[], ret[];\n' code += ' ArrayResize(ret, 1);\n' code += ' for(int i=ArraySize(MAs)-1; i>=0; i--) {\n' code += ' CopyClose(NULL,PERIOD_CURRENT,1,MAs[i],pr);\n' code += ' double mean = MathMean(pr);\n' code += ' ret[0] = pr[MAs[i]-1] - mean;\n' code += ' ArrayInsert(features, ret, ArraySize(features), 0, WHOLE_ARRAY); }\n' code += ' ArraySetAsSeries(features, true);\n' code += '}\n\n' # add CatBosst code += 'double catboost_model' + '(const double &features[]) { \n' code += ' ' with open('catmodel.h', 'r') as file: data = file.read() code += data[data.find("unsigned int TreeDepth") :data.find("double Scale = 1;")] code += '\n\n' code += 'return ' + \ 'ApplyCatboostModel(features, TreeDepth, TreeSplits , BorderCounts, Borders, LeafValues); } \n\n' code += 'double ApplyCatboostModel(const double &features[],uint &TreeDepth_[],uint &TreeSplits_[],uint &BorderCounts_[],float &Borders_[],double &LeafValues_[]) {\n\ uint FloatFeatureCount=ArrayRange(BorderCounts_,0);\n\ uint BinaryFeatureCount=ArrayRange(Borders_,0);\n\ uint TreeCount=ArrayRange(TreeDepth_,0);\n\ bool binaryFeatures[];\n\ ArrayResize(binaryFeatures,BinaryFeatureCount);\n\ uint binFeatureIndex=0;\n\ for(uint i=0; i<FloatFeatureCount; i++) {\n\ for(uint j=0; j<BorderCounts_[i]; j++) {\n\ binaryFeatures[binFeatureIndex]=features[i]>Borders_[binFeatureIndex];\n\ binFeatureIndex++;\n\ }\n\ }\n\ double result=0.0;\n\ uint treeSplitsPtr=0;\n\ uint leafValuesForCurrentTreePtr=0;\n\ for(uint treeId=0; treeId<TreeCount; treeId++) {\n\ uint currentTreeDepth=TreeDepth_[treeId];\n\ uint index=0;\n\ for(uint depth=0; depth<currentTreeDepth; depth++) {\n\ index|=(binaryFeatures[TreeSplits_[treeSplitsPtr+depth]]<<depth);\n\ }\n\ result+=LeafValues_[leafValuesForCurrentTreePtr+index];\n\ treeSplitsPtr+=currentTreeDepth;\n\ leafValuesForCurrentTreePtr+=(1<<currentTreeDepth);\n\ }\n\ return 1.0/(1.0+MathPow(M_E,-result));\n\ }' file = open('C:/Users/dmitrievsky/AppData/Roaming/MetaQuotes/Terminal/D0E8209F77C8CF37AD8BF550E51FF075/MQL5/Include/' + str(SYMBOL) + '_cat_model_martin' + '.mqh', "w") file.write(code) file.close() print('The file ' + 'cat_model' + '.mqh ' + 'has been written to disc')
訓練中に使用されたグリッド設定が保存されるようになりました。それらはまた取引で使用されます。
標準のターミナルパックおよび指標バッファからの移動平均は使用されなくなりました。代わりに、すべての機能は関数本体で計算されます。オリジナルの機能を追加する場合は、そのような機能もエクスポート機能に追加する必要があります。
緑は、ターミナルのインクルードフォルダーへのパスを強調表示します。.mqhファイルを保存し、エキスパートアドバイザーに接続できます。
.mqhファイル自体を見てみましょう(CatBoostモデルはここでは省略されています)
#include <Math\Stat\Math.mqh> int MAs[14] = {5,25,55,75,100,125,150,200,250,300,350,400,450,500}; int grid_size = 10; double grid_distances[10] = {0.003,0.0035555555555555557,0.004111111111111111,0.004666666666666666,0.005222222222222222, 0.0057777777777777775,0.006333333333333333,0.006888888888888889,0.0074444444444444445,0.008}; double grid_coefficients[10] = {1.0,1.4444444444444444,1.8888888888888888,2.333333333333333, 2.7777777777777777,3.2222222222222223,3.6666666666666665,4.111111111111111,4.555555555555555,5.0}; void fill_arays( double &features[]) { double pr[], ret[]; ArrayResize(ret, 1); for(int i=ArraySize(MAs)-1; i>=0; i--) { CopyClose(NULL,PERIOD_CURRENT,1,MAs[i],pr); double mean = MathMean(pr); ret[0] = pr[MAs[i]-1] - mean; ArrayInsert(features, ret, ArraySize(features), 0, WHOLE_ARRAY); } ArraySetAsSeries(features, true); }
ご覧のとおり、すべてのグリッド設定が保存され、モデルを使用する準備が整いました。エキスパートアドバイザーエキスパートアドバイザーに接続するだけです。
#include <EURUSD_cat_model_martin.mqh>
次に、エキスパートアドバイザーがシグナルを処理するロジックについて説明します。例としてOnTick()関数を使用します。ボットは、追加でダウンロードする必要があるMT4Ordersライブラリを使用します。
void OnTick() { //--- if(!isNewBar()) return; TimeToStruct(TimeCurrent(), hours); double features[]; fill_arays(features); if(ArraySize(features) !=ArraySize(MAs)) { Print("No history available, will try again on next signal!"); return; } double sig = catboost_model(features); // Close positions by an opposite signal if(count_market_orders(0) || count_market_orders(1)) for(int b = OrdersTotal() - 1; b >= 0; b--) if(OrderSelect(b, SELECT_BY_POS) == true) { if(OrderType() == 0 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic && sig > 0.5) if(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 0, Red)) { } if(OrderType() == 1 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic && sig < 0.5) if(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 0, Red)) { } } // Delete all pending orders if there are no pending orders if(!count_market_orders(0) && !count_market_orders(1)) { for(int b = OrdersTotal() - 1; b >= 0; b--) if(OrderSelect(b, SELECT_BY_POS) == true) { if(OrderType() == 2 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic ) if(OrderDelete(OrderTicket())) { } if(OrderType() == 3 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic ) if(OrderDelete(OrderTicket())) { } } } // Open positions and pending orders by signals if(countOrders() == 0 && CheckMoneyForTrade(_Symbol,LotsOptimized(),ORDER_TYPE_BUY)) { double l = LotsOptimized(); if(sig < 0.5) { OrderSend(Symbol(),OP_BUY,l, Ask, 0, Bid-stoploss*_Point, Ask+takeprofit*_Point, NULL, OrderMagic); double p = Ask; for(int i=0; i<grid_size; i++) { p = NormalizeDouble(p - grid_distances[i], _Digits); double gl = NormalizeDouble(l * grid_coefficients[i], 2); OrderSend(Symbol(),OP_BUYLIMIT,gl, p, 0, p-stoploss*_Point, p+takeprofit*_Point, NULL, OrderMagic); } } else { OrderSend(Symbol(),OP_SELL,l, Bid, 0, Ask+stoploss*_Point, Bid-takeprofit*_Point, NULL, OrderMagic); double p = Ask; for(int i=0; i<grid_size; i++) { p = NormalizeDouble(p + grid_distances[i], _Digits); double gl = NormalizeDouble(l * grid_coefficients[i], 2); OrderSend(Symbol(),OP_SELLLIMIT,gl, p, 0, p+stoploss*_Point, p-takeprofit*_Point, NULL, OrderMagic); } } } }
fill_arrays関数は、features配列を満たすCatBoostモデルのフィーチャを準備します。次に、この配列はcatboost_model()関数に渡され、0; 1の範囲のシグナルが返されます。
買い注文の例からわかるように、ここではgrid_size変数が使用されています。grid_distancesの距離にある保留中の注文の数が表示されます。標準ロットには、注文番号に対応するgrid_coefficients配列の係数が掛けられます。
ボットがコンパイルされたら、テストに進むことができます。
MetaTrader5テスターでボットを確認する
テストは、ボットが訓練された時間枠で実行する必要があります。この場合はH1です。ボットはバーの開きを明示的に制御しているため、始値を使用してテストできます。ただし、グリッドを使用しているため、精度を高めるためにM1OHLCを選択できます。
この特定のボットは、次の期間に訓練されました。
START_DATE = datetime(2020, 5, 1) TSTART_DATE = datetime(2019, 1, 1) FULL_DATE = datetime(2018, 1, 1) END_DATE = datetime(2022, 1, 1)
- 2020年の5か月目から現在までの間隔は訓練期間であり、50/50が訓練と検証のサブサンプルに分割されます。
- 2019年の最初の月から、モデルはR ^ 2に従って評価され、最適なモデルが選択されました。
- 2018年の最初の月から、モデルはカスタムテスターでテストされました。
- 合成データは訓練に使用されました(ガウス混合モデルによって生成されました)
- CatBoostモデルには強力な正則化があり、訓練サンプルの過剰適合を回避するのに役立ちます。
これらすべての要因は、2018年から現在までの間に特定のパターンが見つかったことを示しています(これはカスタムテスターによっても確認されています)。
MetaTrader5ストラテジーテスターでどのように見えるかを見てみましょう。
エクイティドローダウンが表示されることを除いて、バランスチャートは私のカスタムテスターと同じように見えます。<朗報です。ボットがグリッドだけを正確に取引していることを確認しましょう。
これが2015年からの間隔でのテスト結果です。
グラフによると、見つかったパターンは2016年の終わりから現在まで機能し、残りの間隔では失敗します。この場合、最初のロットは最小限であり、ボットが生き残るのに役立ちました。少なくとも、ボットは2017年の初めから有効であることがわかっています。これに基づいて、収益性を高めるためにリスクを高めることができます。ロボットは印象的な結果を示しています。3年間で1600%、ドローダウンは40%で、預金全体を失うという仮想的なリスクがあります。
また、ボットは各ポジションにストップロスとテイクプロフィットを使用します。SLとTPは、パフォーマンスを犠牲にしてリスクを制限しながら使用できます。
かなり積極的なグリッドを使用したことに注意してください。
GRID_COEFFICIENTS = np.linspace(1, 5, num= GRID_SIZE)
array([1. , 1.44444444, 1.88888889, 2.33333333, 2.77777778, 3.22222222, 3.66666667, 4.11111111, 4.55555556, 5. ])
最後の乗数は5です。これは、シリーズの最後の注文のロットが最初のロットの5倍であることを意味し、追加のリスクが伴います。より中程度のモードを選択できます。
2016年以前の期間にボットが機能しなくなったのはなぜでしょうか。この質問に対する意味のある答えはありません。外国為替市場には7年の長いサイクルがあるようですが、そのパターンはまったく関連していません。これは別のトピックであり、より詳細な調査が必要です。
終わりに
本稿では、ブースティングモデルまたはニューラルネットワークを訓練してマーチンゲールを取引するために使用できる手法について説明しようとしました。独自の取引ロボットを作成できる既製のソリューションを取り上げています。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/8826





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