机器学习和交易中的元模型:交易订单的原始时序·综合运用
📘

机器学习和交易中的元模型:交易订单的原始时序·综合运用

第 3/3 篇

「跨平台取数与非对称验证切分」

Mac 和 Linux 环境下,终端自带的报价加载接口直接不可用,硬走官方 API 只会空手而归。绕开的办法是把 MT5 导出的报价先存成文件丢进终端工作目录,再用独立脚本读盘,这样三个系统都能跑同一套模型。 回测和前向验证的边界靠三个日期变量卡死:TSTART_DATE(2017-01-01)之前的数据纯回测,STOP_DATE(2022-01-01)之后的数据不参与训练、只进测试集,中间段才是训练区。这种非对称切分能逼出模型在未见样本上的真实倾向,外汇与贵金属杠杆高,过拟合的代价是实盘直接爆仓。 下面这段 Python 从 EURUSDMT5.csv 读报价并算多周期偏离,注意它和 MQL5 工作目录的衔接点——文件必须先在 MT5 里导出到位。 代码逐行拆解: 第1行 定义 get_prices 函数,返回 pandas 的 DataFrame; 第2行 从工作目录读 EURUSDMT5.csv,空白符分隔; 第3行 建空表 pFixed,列名 time 和 close; 第4行 用原表的 DATE 和 TIME 拼成时间戳字符串; 第5行 把拼接字段转成 datetime 类型; 第6行 把 CLOSE 列塞进 pFixed; 第7行 将 time 设为索引; 第8行 索引转成秒级 datetime; 第9行 删掉缺失行; 第10行 拷贝一份 pFixedC; 第11-15行 遍历 MA_PERIODS,逐周期算价格减均线的偏离并存入新列; 第16行 返回去空后的偏离表。 末尾三行把 START_DATE、TSTART_DATE、STOP_DATE 钉死,训练/测试分流就靠它们。

MQL5 / C++
def get_prices() -> pd.DataFrame:
    p = pd.read_csv(&class="macro">#x27;EURUSDMT5.csv&class="macro">#x27;, delim_whitespace=True)
    pFixed = pd.DataFrame(columns=[&class="macro">#x27;time&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;])
    pFixed[&class="macro">#x27;time&class="macro">#x27;] = p[&class="macro">#x27;<DATE>&class="macro">#x27;] + &class="macro">#x27; &class="macro">#x27; + p[&class="macro">#x27;<TIME>&class="macro">#x27;]
    pFixed[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(pFixed[&class="macro">#x27;time&class="macro">#x27;], infer_datetime_format=True)
    pFixed[&class="macro">#x27;close&class="macro">#x27;] = p[&class="macro">#x27;<CLOSE>&class="macro">#x27;]
    pFixed.set_index(&class="macro">#x27;time&class="macro">#x27;, inplace=True)
    pFixed.index = pd.to_datetime(pFixed.index, unit=&class="macro">#x27;s&class="macro">#x27;)
    pFixed = pFixed.dropna()
    pFixedC = pFixed.copy()
    count = class="num">0
    for i in MA_PERIODS:
        pFixed[str(count)] = pFixedC - pFixedC.rolling(i).mean()
        count += class="num">1
    class="kw">return pFixed.dropna()
START_DATE = class="type">class="kw">datetime(class="num">2021, class="num">1, class="num">1)
TSTART_DATE = class="type">class="kw">datetime(class="num">2017, class="num">1, class="num">1)
STOP_DATE = class="type">class="kw">datetime(class="num">2022, class="num">1, class="num">1)

训练窗口与验证数据的切分边界

做元模型回测时,先认清楚两段训练区间的差别:基准模型只在 START_DATE 到 STOP_DATE 这段里训练,而元模型吃的是 TSTART_DATE 到 STOP_DATE 的数据。两者停止日期一致,但元模型的起始点更靠后,意味着它看不见基准模型早期那部分样本。 文件里除了上述训练区间之外的其余数据,不会进训练,只用于回测和前向验证测试。实操时若你在 MT5 里改了 TSTART_DATE,要同步检查验证集是否因此被截断,否则前向测试结果可能虚高。 外汇与贵金属品种波动受宏观事件驱动,这种切分方式仅降低过拟合概率,不保证样本外表现,上线前务必用真实点差重跑验证。

◍ GBPJPY H1 上的交叉盘验证

把训练框架搬到 GBPJPY H1 上跑了一遍,报价取自 2010 年全年的历史数据。基准模型用 2021 至 2022 年初训练,元模型拉长到 2018–2022 年,剩下的 2010 至 2022 年 6 月 15 日全部留作样本外测试。 交易采样的随机持续时间限定在 15–35 根 BAR 之间,训练迭代固定 25 轮,不良样本簿的乘数设为 0.5。这套设定下,R^2 在迭代中波动明显:第 11 轮冲到 0.9759,第 23 轮掉到 0.6541,说明模型对交叉盘的样本敏感度高,过拟合风险并不低。 外汇与贵金属属高风险品种,上述 R^2 仅反映历史样本内拟合,实盘表现可能显著走弱。建议直接在 MT5 导一份 GBPJPY H1 的 2010 报价,复刻下面参数自行核对。

MQL5 / C++
MA_PERIODS = [i <span class="keyword">for</span> i in range(<span class="number">class="num">15</span>, <span class="number">class="num">500</span>, <span class="number">class="num">15</span>)]
MARKUP = <span class="number">class="num">0.00002</span>
START_DATE = <span class="keyword">class="type">class="kw">datetime</span>(<span class="number">class="num">2021</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>)
TSTART_DATE = <span class="keyword">class="type">class="kw">datetime</span>(<span class="number">class="num">2018</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>)
STOP_DATE = <span class="keyword">class="type">class="kw">datetime</span>(<span class="number">class="num">2022</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>)
def labelling_relabeling(dataset, min=<span class="number">class="num">15</span>, max=<span class="number">class="num">35</span>, relabeling=False):
# iterative learning
res = []
BAD_SAMPLES_BOOK = pd.DatetimeIndex([])
for i in range(<span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);">class="num">25</span>):
&nbsp;&nbsp;&nbsp;&nbsp;res.append(brute_force(pr[pr.columns[class="num">1:]], <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);">bad_samples_fraction=class="num">0.5</span>))
&nbsp;&nbsp;&nbsp;&nbsp;print(&class="macro">#x27;Iteration: {}, R^class="num">2: {}&class="macro">#x27;.format(i, res[-class="num">1][class="num">0]))
&nbsp;&nbsp;&nbsp;&nbsp;pr = res[-class="num">1][class="num">3] 
# test best model
res.sort()
p = test_model(res[-class="num">1])
Iteration: <span class="number">class="num">0</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8364212812476872</span>
Iteration: <span class="number">class="num">1</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8265960950867208</span>
Iteration: <span class="number">class="num">2</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8710535097094494</span>
Iteration: <span class="number">class="num">3</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.820894300254345</span>
Iteration: <span class="number">class="num">4</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.7271704621597865</span>
Iteration: <span class="number">class="num">5</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8746302835797399</span>
Iteration: <span class="number">class="num">6</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.7746283871087961</span>
Iteration: <span class="number">class="num">7</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.870806543378866</span>
Iteration: <span class="number">class="num">8</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8651222653557956</span>
Iteration: <span class="number">class="num">9</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9452164577256995</span>
Iteration: <span class="number">class="num">10</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.867541289963404</span>
Iteration: <span class="number">class="num">11</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9759544230548619</span>
Iteration: <span class="number">class="num">12</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9063804006221455</span>
Iteration: <span class="number">class="num">13</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9609701853129079</span>
Iteration: <span class="number">class="num">14</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9666262255426672</span>
Iteration: <span class="number">class="num">15</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.7046628448822643</span>
Iteration: <span class="number">class="num">16</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.7750941894554821</span>
Iteration: <span class="number">class="num">17</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9436968900331276</span>
Iteration: <span class="number">class="num">18</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8961403809578388</span>
Iteration: <span class="number">class="num">19</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9627553719743711</span>
Iteration: <span class="number">class="num">20</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9559809326980575</span>
Iteration: <span class="number">class="num">21</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.9578579606050637</span>
Iteration: <span class="number">class="num">22</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.8095556721129047</span>
Iteration: <span class="number">class="num">23</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.654147043077418</span>
Iteration: <span class="number">class="num">24</span>, R^<span class="number">class="num">2</span>: <span class="number">class="num">0.7538928969905255</span>

「把高分模型导出到 MT5 实测」

模型训练完后按 R^2 从高到低排,我们取靠前的几个看表现。自 2010 年以来的回测里,这些形态整体曲线还算稳,但不是每条都平滑,中间也有回落段。 导出时函数吃两个参数:一个是模型对象(这里用 res 列表倒数第一个,即末尾最佳),另一个是编号字符串改文件名,方便你一次存多个不覆盖。 编译完的机器人直接丢进 MT5 策略测试器跑,能验证样本外表现。外汇和贵金属波动大、杠杆高,样本内得分高不代表实盘概率一定延续,先用测试器看滑点和点差下的衰减。

MQL5 / C++
export_model_to_MQL_code(res[-class="num">1], str(class="num">1))

把这条线请下神坛

这套元模型思路最值得记住的一点,是它能在训练里自动把难以分类的历史片段丢出去,不用人工去挑时段或星期。评论区里有人拿 EURUSD 跑下来,发现 CatBoost 在样本上平衡点低于原文配图,说明不是挂上模型就出魔法,多次迭代后才可能看到文章里那种渐进变好的倾向。 维度也是个坑,作者自己提到通常放 5–10 个特征,太高维反而难找模式。外汇和贵金属杠杆高、滑点跳空频繁,这类模型顶多算概率优势,实盘前务必在 MT5 用附带的 Meta_bot.mq5 跑一遍历史校验。 所以别把它当圣杯,它只是把整理历史这件事交给了机器,省下的人工该拿去盯自己的交易假设成不成立。

常见问题

按时间严格切分,训练窗口用早段、验证用晚段且互不重叠;交叉盘验证时只用未参与训练的品种数据看泛化。
倾向取含多种波段形态的 6~12 个月,验证数据单独留最近 1~3 个月做非对称切分,别用随机打乱。
可以,小布能按你设的时间边界自动拉多平台原始时序、做非对称切分,并标出验证集命中率供你筛模型。
查验证集上的盈亏分布和最大回撤,贵金属外汇高风险,单看分数可能漏掉极端行情下的裸奔。
掉点说明泛化弱,倾向只做原品种轻仓实测,并把这条线请下神坛——它只是概率工具不是圣杯。