机器学习和交易中的元模型:交易订单的原始时序·综合运用
「跨平台取数与非对称验证切分」
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 钉死,训练/测试分流就靠它们。
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 报价,复刻下面参数自行核对。
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>): 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>)) print(&class="macro">#x27;Iteration: {}, R^class="num">2: {}&class="macro">#x27;.format(i, res[-class="num">1][class="num">0])) 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 策略测试器跑,能验证样本外表现。外汇和贵金属波动大、杠杆高,样本内得分高不代表实盘概率一定延续,先用测试器看滑点和点差下的衰减。
export_model_to_MQL_code(res[-class="num">1], str(class="num">1))
把这条线请下神坛
这套元模型思路最值得记住的一点,是它能在训练里自动把难以分类的历史片段丢出去,不用人工去挑时段或星期。评论区里有人拿 EURUSD 跑下来,发现 CatBoost 在样本上平衡点低于原文配图,说明不是挂上模型就出魔法,多次迭代后才可能看到文章里那种渐进变好的倾向。 维度也是个坑,作者自己提到通常放 5–10 个特征,太高维反而难找模式。外汇和贵金属杠杆高、滑点跳空频繁,这类模型顶多算概率优势,实盘前务必在 MT5 用附带的 Meta_bot.mq5 跑一遍历史校验。 所以别把它当圣杯,它只是把整理历史这件事交给了机器,省下的人工该拿去盯自己的交易假设成不成立。