网格和马丁格尔交易系统中的机器学习。 您敢为其打赌吗?·进阶篇
◍ 把回测器改造成能逐笔清算网格盈亏
网格策略回测里最容易被忽略的是:多数人会盯着余额曲线,却把净值曲线丢在一边。我们沿用这个习惯,测试器只画余额图形,净值留在 MT5 终端里看,避免把回测框架搞重。 要让网格利润算得准,测试器得像 MT5 那样顺序吃历史报价、按真实逻辑开平。核心循环把每个 close 价塞进 all_pr 数组,遇到反向信号才结算当前网格——不是等整组平仓,而是每平一单就往 report 里加一次,余额图反映的是已平仓位的累积,不是组合总利润。 下面这段是测试器主体。last_deal=2 表示尚无持仓方向;pred<=0.5 判为空头、否则多头。反向信号触发时,up_range 存价格穿越区间,首仓利润扣掉 markup 点差后入账,再遍历 distances 看挂单是否被打到,命中就按 coefficients 加权累加。卖出侧逻辑对称。 外汇与贵金属网格本身带高杠杆与连亏扩张风险,回测余额好看不代表实盘能扛住滑点与断连。 别把余额曲线当圣杯 只画余额会掩盖浮亏暴露。开 MT5 对照终端净值,若两者背离过大,说明测试器漏了某类挂单触发,优先查 distances 与 coefficients 的索引对齐。
def tester(dataset, markup, distances, coefficients, plot=False): last_deal = class="type">int(class="num">2) all_pr = np.array([]) report = [class="num">0.0] for i in range(dataset.shape[class="num">0]): pred = dataset[&class="macro">#x27;labels&class="macro">#x27;][i] all_pr = np.append(all_pr, dataset[&class="macro">#x27;close&class="macro">#x27;][i]) if last_deal == class="num">2: last_deal = class="num">0 if pred <= class="num">0.5 else class="num">1 class="kw">continue if last_deal == class="num">0 and pred > class="num">0.5: last_deal = class="num">1 up_range = all_pr[class="num">0] - all_pr.min() up_state = class="num">0 up_orders = class="num">0 up_profit = (all_pr[-class="num">1] - all_pr[class="num">0]) - markup report.append(report[-class="num">1] + up_profit) up_profit = class="num">0 for d in np.nditer(distances): if up_state + d <= up_range: up_state += d up_orders += class="num">1 up_profit += (all_pr[-class="num">1] - all_pr[class="num">0] + up_state) * coefficients[class="type">int(up_orders-class="num">1)] up_profit -= markup * coefficients[class="type">int(up_orders-class="num">1)] report.append(report[-class="num">1] + up_profit) up_profit = class="num">0 all_pr = np.array([dataset[&class="macro">#x27;close&class="macro">#x27;][i]]) class="kw">continue if last_deal == class="num">1 and pred < class="num">0.5: last_deal = class="num">0 dwn_range = all_pr.max() - all_pr[class="num">0] dwn_state = class="num">0 dwn_orders = class="num">0 dwn_profit = (all_pr[class="num">0] - all_pr[-class="num">1]) - markup report.append(report[-class="num">1] + dwn_profit) dwn_profit = class="num">0
下行网格的逐单累计与斜率判定
这段逻辑跑的是下行网格的逐单推进:遍历每一条间距 d,只要当前累计跌幅 dwn_state 加上 d 还没超出设定的 dwn_range,就把它吃进同一次建仓序列,挂单计数 dwn_orders 加 1。 每吃一单,dwn_profit 先按「首根收盘价 all_pr[0] + 累计跌幅 - 末根收盘价」乘以该单对应的手数系数 coefficients,再扣掉同系数的 markup 成本,然后把这一笔累加到 report 里,随后把 dwn_profit 清零等下一单。 一旦 d 超出范围,all_pr 被重置成当前收盘价的单元素数组,continue 跳出本次循环重开序列。这样 report 里装的是一段段下行网格各自的累计盈亏曲线。 最后用 LinearRegression 对 report 做最小二乘拟合,X 是成交序号、y 是累计盈亏。若斜率系数 l>=0 记 1,否则记 -1,返回的是拟合优度 R² 乘上这个方向符号——外汇与贵金属杠杆高,信号仅代表历史样本下的方向倾向,实盘须自行在 MT5 回测验证。 绘图分支在 plot=True 时画出原始曲线与拟合线,标题直接标 cumulative profit in pips,横轴 number of trades。想看自己品种的表现,把 dataset 换成对应品种日线 close 即可跑通。
for d in np.nditer(distances): if dwn_state + d <= dwn_range: dwn_state += d dwn_orders += class="num">1 dwn_profit += (all_pr[class="num">0] + dwn_state - all_pr[-class="num">1]) * coefficients[class="type">int(dwn_orders-class="num">1)] dwn_profit -= markup * coefficients[class="type">int(dwn_orders-class="num">1)] report.append(report[-class="num">1] + dwn_profit) dwn_profit = class="num">0 all_pr = np.array([dataset[&class="macro">#x27;close&class="macro">#x27;][i]]) class="kw">continue y = np.array(report).reshape(-class="num">1, class="num">1) X = np.arange(len(report)).reshape(-class="num">1, class="num">1) lr = LinearRegression() lr.fit(X, y) l = lr.coef_ if l >= class="num">0: l = class="num">1 else: l = -class="num">1 if(plot): plt.figure(figsize=(class="num">12,class="num">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() class="kw">return lr.score(X, y) * l
「用合成数据筛出最稳的网格模型」
给机器学习喂数据这套流程已经跑通了:先拉价格、造特征、打买卖标签,再丢进自定义测试器验证标签质量。这一步不做扎实,后面模型再花哨也是空中楼阁。 这回换 CatBoost 上场,训练样本直接用高斯混合模型造出来的合成数据。实测在 10,000 个生成样本上训 10 个模型,按 R^2 挑最好的那个,思路很直接——合成数据够用就别死磕真实行情的稀缺样本。 从迭代输出看,10 次里大多数 R^2 都站在 0.9 以上,最高摸到 0.9688,最低也有 0.6084。这种跨训练集和样本外数据都高分的分布,说明模型稳定性倾向不错,不是过拟合一波流。 选完模型就能导进 MT5 终端测了。每个训好的模型自带独立文件,换模型就是改个引用,不用动 EA 主体结构。外汇和贵金属波动剧烈,这类网格策略实盘前务必在测试器跑足够样本,高风险不是吓唬人。
# Get prices and labels and test it pr = get_prices(START_DATE, END_DATE) pr = add_labels(pr, class="num">15, class="num">15, GRID_DISTANCES, GRID_COEFFICIENTS) tester(pr, MARKUP, GRID_DISTANCES, GRID_COEFFICIENTS, plot=True) # Learn and test CatBoost model gmm = mixture.GaussianMixture( n_components=N_COMPONENTS, covariance_type=&class="macro">#x27;full&class="macro">#x27;, n_init=class="num">1).fit(pr[pr.columns[class="num">1:]]) res = [] for i in range(class="num">10): res.append(brute_force(class="num">10000)) print(&class="macro">#x27;Iteration: &class="macro">#x27;, i, &class="macro">#x27;R^class="num">2: &class="macro">#x27;, res[-class="num">1][class="num">0]) res.sort() test_model(res[-class="num">1]) Iteration: class="num">0 R^class="num">2: class="num">0.8719436661855786 Iteration: class="num">1 R^class="num">2: class="num">0.912006346274096 Iteration: class="num">2 R^class="num">2: class="num">0.9532278725035132 Iteration: class="num">3 R^class="num">2: class="num">0.900845571741786 Iteration: class="num">4 R^class="num">2: class="num">0.9651728908727953 Iteration: class="num">5 R^class="num">2: class="num">0.966531822300101 Iteration: class="num">6 R^class="num">2: class="num">0.9688263099200539 Iteration: class="num">7 R^class="num">2: class="num">0.8789927823514787 Iteration: class="num">8 R^class="num">2: class="num">0.6084261786804662 Iteration: class="num">9 R^class="num">2: class="num">0.884741078512629
◍ 把训练好的 CatBoost 塞进 MT5 的 Include 目录
导模型这步核心就一个动作:用 save_model 以 cpp 格式落盘,再把网格参数和特征填充函数拼进同一份 .mqh,丢进终端的 Include 文件夹就能被 EA 直接 #include。原版发布包里依赖终端指标缓冲区的写法被砍了,所有均值类特征都在导出函数体内现算,后续你要加原始特征,必须同步在导出函数里补逻辑,否则线上推理会和训练态脱节。 下面这段是导出函数的关键片段,绿色高亮处就是写进 Include 路径的网格与均线周期变量,以及 fill_arays 里用 CopyClose 取不同周期收盘价、算偏离均值的特征构造。注意 grid_size 决定挂单层数,grid_distances 是各层间距,grid_coefficients 是逐层手数倍率,EA 编译后拿 catboost_model() 吐回 [0,1] 信号。 外汇与贵金属杠杆高,网格加仓在极端跳空时可能扩大浮亏,上 MT5 回测前先确认点差与止损逻辑。
export_model_to_MQL_code(res[-class="num">1][class="num">1]) def export_model_to_MQL_code(model): model.save_model(&class="macro">#x27;catmodel.h&class="macro">#x27;, format="cpp", export_parameters=None, pool=None) # add variables code = &class="macro">#x27;class="macro">#include <Math\Stat\Math.mqh>&class="macro">#x27; code += &class="macro">#x27;\n&class="macro">#x27; code += &class="macro">#x27;class="type">int MAs[&class="macro">#x27; + str(len(MA_PERIODS)) + &class="macro">#x27;] = {&class="macro">#x27; + &class="macro">#x27;,&class="macro">#x27;.join(map(str, MA_PERIODS)) + &class="macro">#x27;};&class="macro">#x27; code += &class="macro">#x27;\n&class="macro">#x27; code += &class="macro">#x27;class="type">int grid_size = &class="macro">#x27; + str(GRID_SIZE) + &class="macro">#x27;;&class="macro">#x27; code += &class="macro">#x27;\n&class="macro">#x27; code += &class="macro">#x27;class="type">class="kw">double grid_distances[&class="macro">#x27; + str(len(GRID_DISTANCES)) + &class="macro">#x27;] = {&class="macro">#x27; + &class="macro">#x27;,&class="macro">#x27;.join(map(str, GRID_DISTANCES)) + &class="macro">#x27;};&class="macro">#x27; code += &class="macro">#x27;\n&class="macro">#x27; code += &class="macro">#x27;class="type">class="kw">double grid_coefficients[&class="macro">#x27; + str(len(GRID_COEFFICIENTS)) + &class="macro">#x27;] = {&class="macro">#x27; + &class="macro">#x27;,&class="macro">#x27;.join(map(str, GRID_COEFFICIENTS)) + &class="macro">#x27;};&class="macro">#x27; code += &class="macro">#x27;\n&class="macro">#x27; # get features code += &class="macro">#x27;class="type">void fill_arays( class="type">class="kw">double &features[]) {\n&class="macro">#x27; code += &class="macro">#x27; class="type">class="kw">double pr[], ret[];\n&class="macro">#x27; code += &class="macro">#x27; ArrayResize(ret, class="num">1);\n&class="macro">#x27; code += &class="macro">#x27; for(class="type">int i=ArraySize(MAs)-class="num">1; i>=class="num">0; i--) {\n&class="macro">#x27; code += &class="macro">#x27; CopyClose(NULL,PERIOD_CURRENT,class="num">1,MAs[i],pr);\n&class="macro">#x27; code += &class="macro">#x27; class="type">class="kw">double mean = MathMean(pr);\n&class="macro">#x27; code += &class="macro">#x27; ret[class="num">0] = pr[MAs[i]-class="num">1] - mean;\n&class="macro">#x27; code += &class="macro">#x27; ArrayInsert(features, ret, ArraySize(features), class="num">0, WHOLE_ARRAY); }\n&class="macro">#x27; code += &class="macro">#x27; ArraySetAsSeries(features, true);\n&class="macro">#x27; code += &class="macro">#x27;}\n\n&class="macro">#x27; # add CatBosst code += &class="macro">#x27;class="type">class="kw">double catboost_model&class="macro">#x27; + &class="macro">#x27;(const class="type">class="kw">double &features[]) { \n&class="macro">#x27; code += &class="macro">#x27; &class="macro">#x27; with open(&class="macro">#x27;catmodel.h&class="macro">#x27;, &class="macro">#x27;r&class="macro">#x27;) as file: data = file.read() code += data[data.find("unsigned class="type">int TreeDepth") :data.find("class="type">class="kw">double Scale = class="num">1;")] code += &class="macro">#x27;\n\n&class="macro">#x27; code += &class="macro">#x27;class="kw">return &class="macro">#x27; + &class="macro">#x27;ApplyCatboostModel(features, TreeDepth, TreeSplits , BorderCounts, Borders, LeafValues); } \n\n&class="macro">#x27; code += &class="macro">#x27;class="type">class="kw">double ApplyCatboostModel(const class="type">class="kw">double &features[],class="type">uint &TreeDepth_[],class="type">uint &TreeSplits_[],class="type">uint &BorderCounts_[],class="type">class="kw">float &Borders_[],class="type">class="kw">double &LeafValues_[]) {\n class="type">uint FloatFeatureCount=ArrayRange(BorderCounts_,class="num">0);\n class="type">uint BinaryFeatureCount=ArrayRange(Borders_,class="num">0);\n
把梯度提升模型落地成 MT5 头文件
训练好的 CatBoost 模型要想在 MT5 里跑推断,得先序列化成 MQL5 的 .mqh 头文件。下面这段 Python 片段把生成的代码直接写到终端 Include 目录,文件名按品种拼接,例如 EURUSD_cat_model_martin.mqh,路径里的 D0E8209F77C8CF37AD8BF550E51FF075 是终端实例哈希,换机器要改。 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') 写盘之后,EA 侧用 #include 把模型拉进来,特征工程靠均线网格。MAs 数组锁了 14 条周期:5 到 500,步长不均,最短 5 根、最长 500 根,覆盖 scalper 到 swing 的视角。 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}; 网格距离从 0.003 到 0.008 等差递增,系数从 1.0 线性爬到 5.0,意味着第 10 层仓位是首层的 5 倍,马丁加仓斜率较陡。fill_arays 里用 CopyClose 取各周期收盘价,MathMean 算均值作为特征输入,贵金属与外汇波动属性不同,直接套这套距离参数可能放大回撤风险。 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); 推断函数先把浮点特征按边界转成二值,再遍历每棵树用位运算拼叶子索引,最后 sigmoid 压缩输出概率。 uint TreeCount=ArrayRange(TreeDepth_,0); bool binaryFeatures[]; ArrayResize(binaryFeatures,BinaryFeatureCount); uint binFeatureIndex=0; for(uint i=0; i<FloatFeatureCount; i++) { for(uint j=0; j<BorderCounts_[i]; j++) { binaryFeatures[binFeatureIndex]=features[i]>Borders_[binFeatureIndex]; binFeatureIndex++; } } double result=0.0; uint treeSplitsPtr=0; uint leafValuesForCurrentTreePtr=0; for(uint treeId=0; treeId<TreeCount; treeId++) { uint currentTreeDepth=TreeDepth_[treeId]; uint index=0; for(uint depth=0; depth<currentTreeDepth; depth++) { index|=(binaryFeatures[TreeSplits_[treeSplitsPtr+depth]]<<depth); } result+=LeafValues_[leafValuesForCurrentTreePtr+index]; treeSplitsPtr+=currentTreeDepth; leafValuesForCurrentTreePtr+=(1<<currentTreeDepth); } return 1.0/(1.0+MathPow(M_E,-result)); 二值化那层循环把连续特征切成 BorderCounts_ 个 0/1 开关;树遍历里 index|=(...<<depth) 是把决策路径压成一个整数,叶子值累加后过 sigmoid,输出 0~1 之间倾向多空的概率。开 MT5 把生成的 .mqh 挂上,用 EURUSD 五分钟跑一遍 fill_arays,看 MathMean 特征维度是否和 BinaryFeatureCount 对得上,不对就会数组越界。
class="type">uint TreeCount=ArrayRange(TreeDepth_,class="num">0); class="type">bool binaryFeatures[]; ArrayResize(binaryFeatures,BinaryFeatureCount); class="type">uint binFeatureIndex=class="num">0; for(class="type">uint i=class="num">0; i<FloatFeatureCount; i++) { for(class="type">uint j=class="num">0; j<BorderCounts_[i]; j++) { binaryFeatures[binFeatureIndex]=features[i]>Borders_[binFeatureIndex]; binFeatureIndex++; } } class="type">class="kw">double result=class="num">0.0; class="type">uint treeSplitsPtr=class="num">0; class="type">uint leafValuesForCurrentTreePtr=class="num">0; for(class="type">uint treeId=class="num">0; treeId<TreeCount; treeId++) { class="type">uint currentTreeDepth=TreeDepth_[treeId]; class="type">uint index=class="num">0; for(class="type">uint depth=class="num">0; depth<currentTreeDepth; depth++) { index|=(binaryFeatures[TreeSplits_[treeSplitsPtr+depth]]<<depth); } result+=LeafValues_[leafValuesForCurrentTreePtr+index]; treeSplitsPtr+=currentTreeDepth; leafValuesForCurrentTreePtr+=(class="num">1<<currentTreeDepth); } class="kw">return class="num">1.0/(class="num">1.0+MathPow(M_E,-result)); } file = open(&class="macro">#x27;C:/Users/dmitrievsky/AppData/Roaming/MetaQuotes/Terminal/D0E8209F77C8CF37AD8BF550E51FF075/MQL5/Include/&class="macro">#x27; + str(SYMBOL) + &class="macro">#x27;_cat_model_martin&class="macro">#x27; + &class="macro">#x27;.mqh&class="macro">#x27;, "w") file.write(code) file.close() print(&class="macro">#x27;The file &class="macro">#x27; + &class="macro">#x27;cat_model&class="macro">#x27; + &class="macro">#x27;.mqh &class="macro">#x27; + &class="macro">#x27;has been written to disc&class="macro">#x27;) class="macro">#include <Math\Stat\Math.mqh> class="type">int MAs[class="num">14] = {class="num">5,class="num">25,class="num">55,class="num">75,class="num">100,class="num">125,class="num">150,class="num">200,class="num">250,class="num">300,class="num">350,class="num">400,class="num">450,class="num">500}; class="type">int grid_size = class="num">10; class="type">class="kw">double grid_distances[class="num">10] = {class="num">0.003,class="num">0.0035555555555555557,class="num">0.004111111111111111,class="num">0.004666666666666666,class="num">0.005222222222222222,class="num">0.0057777777777777775,class="num">0.006333333333333333,class="num">0.006888888888888889,class="num">0.0074444444444444445,class="num">0.008}; class="type">class="kw">double grid_coefficients[class="num">10] = {class="num">1.0,class="num">1.4444444444444444,class="num">1.8888888888888888,class="num">2.333333333333333,class="num">2.7777777777777777,class="num">3.2222222222222223,class="num">3.6666666666666665,class="num">4.111111111111111,class="num">4.555555555555555,class="num">5.0}; class="type">void fill_arays( class="type">class="kw">double &features[]) { class="type">class="kw">double pr[], ret[]; ArrayResize(ret, class="num">1); for(class="type">int i=ArraySize(MAs)-class="num">1; i>=class="num">0; i--) { CopyClose(NULL,PERIOD_CURRENT,class="num">1,MAs[i],pr); class="type">class="kw">double mean = MathMean(pr);