网格和马丁格尔交易系统中的机器学习。 您敢为其打赌吗?·进阶篇
📘

网格和马丁格尔交易系统中的机器学习。 您敢为其打赌吗?·进阶篇

第 2/3 篇

◍ 把回测器改造成能逐笔清算网格盈亏

网格策略回测里最容易被忽略的是:多数人会盯着余额曲线,却把净值曲线丢在一边。我们沿用这个习惯,测试器只画余额图形,净值留在 MT5 终端里看,避免把回测框架搞重。 要让网格利润算得准,测试器得像 MT5 那样顺序吃历史报价、按真实逻辑开平。核心循环把每个 close 价塞进 all_pr 数组,遇到反向信号才结算当前网格——不是等整组平仓,而是每平一单就往 report 里加一次,余额图反映的是已平仓位的累积,不是组合总利润。 下面这段是测试器主体。last_deal=2 表示尚无持仓方向;pred<=0.5 判为空头、否则多头。反向信号触发时,up_range 存价格穿越区间,首仓利润扣掉 markup 点差后入账,再遍历 distances 看挂单是否被打到,命中就按 coefficients 加权累加。卖出侧逻辑对称。 外汇与贵金属网格本身带高杠杆与连亏扩张风险,回测余额好看不代表实盘能扛住滑点与断连。 别把余额曲线当圣杯 只画余额会掩盖浮亏暴露。开 MT5 对照终端净值,若两者背离过大,说明测试器漏了某类挂单触发,优先查 distances 与 coefficients 的索引对齐。

MQL5 / C++
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 即可跑通。

MQL5 / C++
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 主体结构。外汇和贵金属波动剧烈,这类网格策略实盘前务必在测试器跑足够样本,高风险不是吓唬人。

MQL5 / C++
# 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 回测前先确认点差与止损逻辑。

MQL5 / C++
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;&nbsp;&nbsp; class="type">class="kw">double pr[], ret[];\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp; ArrayResize(ret, class="num">1);\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp; for(class="type">int i=ArraySize(MAs)-class="num">1; i>=class="num">0; i--) {\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CopyClose(NULL,PERIOD_CURRENT,class="num">1,MAs[i],pr);\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="type">class="kw">double mean = MathMean(pr);\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret[class="num">0] = pr[MAs[i]-class="num">1] - mean;\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ArrayInsert(features, ret, ArraySize(features), class="num">0, WHOLE_ARRAY); }\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp; 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;&nbsp;&nbsp;&nbsp;&nbsp;&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 对得上,不对就会数组越界。

MQL5 / C++
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);

常见问题

把回测器改成逐笔清算逻辑,每成交一单就结算该单盈亏并累加,这样能直接看到下行网格的累计曲线和斜率,判断策略是否越亏越深。
在多种行情合成样本上跑回测,挑那些最大回撤可控、逐单累计斜率不持续为负、清算后仍能复利的模型,而不是只看总收益最高的。
小布可以接入你的成交记录,自动画出逐单累计盈亏和斜率,标出下行网格的危险段,你不用自己改回测器也能看清风险。
把模型导出为头文件放进软件的 Include 目录,主程序直接引用函数得到预测信号,不必每次重训,注意浮点精度要和训练时一致。
没有万能值,建议用历史样本回测找分位:当累计盈亏斜率连续 N 根低于某负值时预警,N 可先试 20~50 根,再按品种波动微调。