机器学习和交易中的元模型:交易订单的原始时序·进阶篇
(2/3)· 当单一分类器在选择性交易里反复误触发,元模型如何自我校正训练集
◍ 双层模型预测与坏样本回收逻辑
堆叠模型跑完基础层和元层后,先各自输出概率再做二分类截断:p2 与 p2_meta 都把 predict_proba 的第 0 列小于 0.5 判为负类,这一步直接决定后续标签流向。 代码里用全局 BAD_SAMPLES_BOOK 收集元标签为 0.0 的索引,相当于把「这轮被元模型否掉的样本」记进黑名单,供后续迭代复用。 坏样本回标时有个阈值判断:若某索引在 BAD_SAMPLES_BOOK 中出现次数超过均值乘以 bad_samples_fraction,才把它在 pr2 的 meta_labels 强制置 0.0;否则只要进过黑名单就置 0.0。这个 fraction 调大,模型对历史坏样本会更宽容,调小则更激进地剔除。 最后用 full_pr 跑一次 tester 拿 R2 返回,说明评估是在未截断时间窗的全样本上做的,而标签修正只作用在 pr2 子窗。外汇与贵金属行情下用这套重标注,过拟合风险偏高,建议先在 MT5 用历史数据跑通再上实盘。
# predict the learned models(base and meta) p = model.predict_proba(X) p_meta = meta_model.predict_proba(X_meta) p2 = [x[class="num">0] < class="num">0.5 for x in p] p2_meta = [x[class="num">0] < class="num">0.5 for x in p_meta] pr2 = pr_tst.iloc[:len(p2)].copy() pr2[&class="macro">#x27;labels&class="macro">#x27;] = p2 pr2[&class="macro">#x27;meta_labels&class="macro">#x27;] = p2_meta pr2[&class="macro">#x27;labels&class="macro">#x27;] = pr2[&class="macro">#x27;labels&class="macro">#x27;].astype(class="type">float) pr2[&class="macro">#x27;meta_labels&class="macro">#x27;] = pr2[&class="macro">#x27;meta_labels&class="macro">#x27;].astype(class="type">float) full_pr = pr2.copy() pr2 = pr2[pr2.index >= TSTART_DATE] pr2 = pr2[pr2.index <= STOP_DATE] # add bad samples of this iteratin(bad meta labels) global BAD_SAMPLES_BOOK BAD_SAMPLES_BOOK = BAD_SAMPLES_BOOK.append(pr2[pr2[&class="macro">#x27;meta_labels&class="macro">#x27;]==class="num">0.0].index) # test mdels and resample meta labels R2, meta_labels = tester(pr2, MARKUP, use_meta=True, plot=False) pr2[&class="macro">#x27;meta_labels&class="macro">#x27;] = meta_labels # resample labels based on meta labels pr2 = labelling_relabeling(pr2, relabeling=True) pr2[&class="macro">#x27;labels&class="macro">#x27;] = pr2[&class="macro">#x27;labels&class="macro">#x27;].astype(class="type">float) pr2[&class="macro">#x27;meta_labels&class="macro">#x27;] = pr2[&class="macro">#x27;meta_labels&class="macro">#x27;].astype(class="type">float) # mark bad labels from bad_samples_book if BAD_SAMPLES_BOOK.value_counts().max() > class="num">1: to_mark = BAD_SAMPLES_BOOK.value_counts() mean = to_mark.mean() marked_idx = to_mark[to_mark > mean*bad_samples_fraction].index pr2.loc[pr2.index.isin(marked_idx), &class="macro">#x27;meta_labels&class="macro">#x27;] = class="num">0.0 else: pr2.loc[pr2.index.isin(BAD_SAMPLES_BOOK), &class="macro">#x27;meta_labels&class="macro">#x27;] = class="num">0.0 R2, _ = tester(full_pr, MARKUP, use_meta=True, plot=False) class="kw">return [R2, model, meta_model, pr2] marked_idx = to_mark[to_mark > mean*bad_samples_fraction].index
「迭代训练里怎么把亏钱信号洗掉」
元模型打标靠的是 tester() 返回的 R^2 和带标记成交帧。把这个结果帧挂到原始数据上,再用第二个标签 meta_labels 对主标签 labels 做二次重标,本质就是从数据集里剔掉被验证无利可图的成交。外汇与贵金属波动剧烈,这种清洗只能降低样本偏差,不承诺实盘胜率。 重训练循环先清空上一轮留下的 bad_samples_book,再跑指定迭代次数。我本地跑 25 轮、bad_samples_fraction=0.7 时,每一轮 brute_force() 把模型和返回数据写进 res[],并打印当轮 R^2。第一次迭代通常 R^2 偏低,后续演算让模型自我修正,测试时成交数减少就是演变的特征信号。 pr 变量承载当轮转换后的数据,喂给下一轮。理论建议扩展基准模型训练区间:把训练起始日按天数后移,但窗口不能超过元模型 TSTART_DATE 的跨度。启用 use_GMM_resampling 思路相近,不过我后来从函数库删了重采样——它多耗训练时间,结果却没明显改观。 从新数据回测看,靠后的第五个模型成交更多,第七个模型成交数翻倍且点数总利润更高。具体选哪个,取决于你更看重样本量还是单边点数,两者都只是概率倾向。
# make dataset pr = get_prices() pr = labelling_relabeling(pr, relabeling=False) a, b = tester(pr, MARKUP, use_meta=False, plot=False) pr[&class="macro">#x27;meta_labels&class="macro">#x27;] = b pr = pr.dropna() pr = labelling_relabeling(pr, relabeling=True) # iterative learning res = [] BAD_SAMPLES_BOOK = pd.DatetimeIndex([]) for i in range(class="num">25): res.append(brute_force(pr[pr.columns[class="num">1:]], bad_samples_fraction=class="num">0.7)) 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] >>> pr = get_prices(START_DATE, STOP_DATE) >>> pr = labelling_relabeling(pr, relabeling=False) >>> pr
从小时序列看标签对齐的实况
上面这组 EURUSD 小时数据截取自 2021-04-13 23:00 至次日凌晨 03:00,close 从 1.19474 走到 1.19543,单根小时 K 最大正向变动约 0.000356(04-14 03:00)。第 0 至第 7 列是不同滞后期收益率,最远一列 0.004888→0.005286 反映 8 小时前的滚动收益,说明近端动量偏弱、远端残留更明显。 用 tester 跑完不含元特征的版本后,把返回的 b 直接挂到 pr['meta_labels'],再 dropna 得到可用样本。2020-05-06 20:00 起连续 5 根 close 从 1.08086 跌到 1.07968,原始 labels 全为 1.0,而 meta_labels 也给出 1.0,说明该时段下跌段被元标签一致确认。 整表原始规模 5670 行 × 9 列,dropna 后尾行出现在 2021-04-13 18:00,close 1.19385、原始 label 为 0.0 但 meta_labels 为 1.0——这种原始标签与元标签背离的格子,才是你开 MT5 对照裸 K 时该重点翻的地方。外汇和贵金属杠杆高,标签背离只代表历史统计倾向,不等于后续必反转。
◍ 迭代重标注把拟合度拉过 0.9
上面那张 5665 行的小时级 EURUSD 片段,最后一列是类标(1.0 表示触发方向信号),前面 8 列是波动率类特征。直接拿原始标签跑模型,R² 往往卡在 0.3 附近,说明噪声样本把边界搅浑了。 用 relabeling=True 做标签重标定后,再跑 25 轮 brute_force,每轮丢弃 70% 的坏样本(bad_samples_fraction=0.7)。迭代 6 出现 R²=0.9168,迭代 14 冲到 0.9413,说明越往后留存的样本越「干净」,模型解释力倾向抬升。 外汇与贵金属属高风险品种,这种重标注只是提高历史样本可学习性,对未来行情只能给概率性参考,不构成方向保证。 别把 0.94 当天花板 迭代 1 的 R² 只有 0.74,迭代 3 跳到 0.889,迭代 8 又掉回 0.827,说明丢弃比例固定时,随机子集差异会让分数抖动量级到 0.1 以上。实盘前建议把迭代次数提到 50 轮,看 R² 分布的中位数而非单点峰值。
class="num">2021-class="num">04-class="num">13 class="num">19:class="num">00:class="num">00 class="num">1.19379 class="num">0.000546 class="num">0.003121 class="num">0.003015 ... class="num">0.003522 class="num">0.004166 class="num">0.0 class="num">1.0 class="num">2021-class="num">04-class="num">13 class="num">20:class="num">00:class="num">00 class="num">1.19423 class="num">0.000622 class="num">0.003269 class="num">0.003349 ... class="num">0.003904 class="num">0.004555 class="num">0.0 class="num">1.0 class="num">2021-class="num">04-class="num">13 class="num">21:class="num">00:class="num">00 class="num">1.19465 class="num">0.000820 class="num">0.003315 class="num">0.003640 ... class="num">0.004267 class="num">0.004929 class="num">0.0 class="num">1.0 class="num">2021-class="num">04-class="num">13 class="num">22:class="num">00:class="num">00 class="num">1.19552 class="num">0.001112 class="num">0.003733 class="num">0.004311 ... class="num">0.005092 class="num">0.005733 class="num">1.0 class="num">1.0 [class="num">5665 rows x class="num">10 columns] pr = labelling_relabeling(pr, relabeling=True) # iterative learning res = [] BAD_SAMPLES_BOOK = pd.DatetimeIndex([]) for i in range(class="num">25): res.append(brute_force(pr[pr.columns[class="num">1:]], bad_samples_fraction=class="num">0.7)) 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] Iteration: class="num">0, R^class="num">2: class="num">0.30121038659012245 Iteration: class="num">1, R^class="num">2: class="num">0.7400055934041012 Iteration: class="num">2, R^class="num">2: class="num">0.6221261327516192 Iteration: class="num">3, R^class="num">2: class="num">0.8892813889403367 Iteration: class="num">4, R^class="num">2: class="num">0.787251984980149 Iteration: class="num">5, R^class="num">2: class="num">0.794241109825588 Iteration: class="num">6, R^class="num">2: class="num">0.9167876214355855 Iteration: class="num">7, R^class="num">2: class="num">0.903399695678254 Iteration: class="num">8, R^class="num">2: class="num">0.8273236332747745 Iteration: class="num">9, R^class="num">2: class="num">0.8646088124681762 Iteration: class="num">10, R^class="num">2: class="num">0.8614746864767437 Iteration: class="num">11, R^class="num">2: class="num">0.7900599001415054 Iteration: class="num">12, R^class="num">2: class="num">0.8837049280116869 Iteration: class="num">13, R^class="num">2: class="num">0.784793801426211 Iteration: class="num">14, R^class="num">2: class="num">0.941340102099874 Iteration: class="num">15, R^class="num">2: class="num">0.8715065229034792 Iteration: class="num">16, R^class="num">2: class="num">0.8104990158946458 Iteration: class="num">17, R^class="num">2: class="num">0.8542444489379808 Iteration: class="num">18, R^class="num">2: class="num">0.8307365677342298 Iteration: class="num">19, R^class="num">2: class="num">0.9092509787525882
「双模型落地到 EA 的编译要点」
把训练好的两个 CatBoost 模型都导成头文件再塞进 EA:基准模型负责买卖方向,元模型做开关——只有它输出大于 0.5 时才放行交易。这套机制直接改写了传统单模型 EA 的逻辑,避免在不该出手的时间段被迫成交。 代码里用 catboost_model() 和 catboost_meta_model() 分别读取 catmodel.h 与 meta_catmodel.h,两者都靠 ApplyCatboostModel 做推理。OnTick 中先判新 K 线,再填特征数组;若特征维度与 MAs 不一致就打印缺失历史并退出,防止数组越界。 被高亮的 meta_sig > 0.5 是实盘过滤核心:它包住了平仓与开仓两块。基准模型 sig > 0.5 倾向做多、sig < 0.5 倾向做空,但前提是元模型已授权。外汇与贵金属杠杆高,模型误判可能在分钟级扩大浮亏,建议先在策略测试器用 2020—2023 年 tick 数据跑通再上模拟盘。 编译时若报 TreeDepth 未声明,通常是头文件截取区间卡在 Scale=1 之前漏了常量,手动核对 data.find 的起止位置即可。
# add CatBosst base model code += &class="macro">#x27;<span class="keyword">class="type">class="kw">double</span> catboost_model&class="macro">#x27; + &class="macro">#x27;(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &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;) <span class="keyword">as</span> file: data = file.read() code += data[data.find("<span class="keyword">unsigned</span> <span class="keyword">class="type">int</span> TreeDepth") :data.find("<span class="keyword">class="type">class="kw">double</span> Scale = <span class="number">class="num">1</span>;")] code += &class="macro">#x27;\n\n&class="macro">#x27; code += &class="macro">#x27;<span class="keyword">class="kw">return</span> &class="macro">#x27; + \ &class="macro">#x27;ApplyCatboostModel(features, TreeDepth, TreeSplits , BorderCounts, Borders, LeafValues); } \n\n&class="macro">#x27; # add CatBosst meta model code += &class="macro">#x27;<span class="keyword">class="type">class="kw">double</span> catboost_meta_model&class="macro">#x27; + &class="macro">#x27;(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &features[]) { \n&class="macro">#x27; code += &class="macro">#x27; &class="macro">#x27; with open(&class="macro">#x27;meta_catmodel.h&class="macro">#x27;, &class="macro">#x27;r&class="macro">#x27;) <span class="keyword">as</span> file: data = file.read() code += data[data.find("<span class="keyword">unsigned</span> <span class="keyword">class="type">int</span> TreeDepth") :data.find("<span class="keyword">class="type">class="kw">double</span> Scale = <span class="number">class="num">1</span>;")] code += &class="macro">#x27;\n\n&class="macro">#x27; code += &class="macro">#x27;<span class="keyword">class="kw">return</span> &class="macro">#x27; + \ &class="macro">#x27;ApplyCatboostModel(features, TreeDepth, TreeSplits , BorderCounts, Borders, LeafValues); } \n\n&class="macro">#x27; <span class="keyword">class="type">void</span> <span class="functions">OnTick</span>() { <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">if</span>(!isNewBar()) <span class="keyword">class="kw">return</span>; <span class="functions">TimeToStruct</span>(<span class="functions">TimeCurrent</span>(), hours); <span class="keyword">class="type">class="kw">double</span> features[]; fill_arays(features); <span class="keyword">if</span>(<span class="functions">ArraySize</span>(features) !=<span class="functions">ArraySize</span>(MAs)) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"No history availible, will try again on next signal!"</span>); <span class="keyword">class="kw">return</span>; } <span class="keyword">class="type">class="kw">double</span> sig = catboost_model(features); <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">class="type">class="kw">double</span> meta_sig = catboost_meta_model(features);</span> <span class="comment">class=class="str">"cmt">// close positions by an opposite signal</span> <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">if</span>(meta_sig > <span class="number">class="num">0.5</span>)</span> <span class="keyword">if</span>(count_market_orders(<span class="number">class="num">0</span>) || count_market_orders(<span class="number">class="num">1</span>)) <span class="keyword">for</span>(<span class="keyword">class="type">int</span> b = <span class="functions">OrdersTotal</span>() - <span class="number">class="num">1</span>; b >= <span class="number">class="num">0</span>; b--) <span class="keyword">if</span>(<span class="functions">OrderSelect</span>(b, SELECT_BY_POS) == <span class="macro">true</span>) { <span class="keyword">if</span>(OrderType() == <span class="number">class="num">0</span> && OrderSymbol() == <span class="predefines">_Symbol</span> && OrderMagicNumber() == OrderMagic && sig > <span class="number">class="num">0.5</span>) <span class="keyword">if</span>(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), <span class="number">class="num">0</span>, Red)) { } <span class="keyword">if</span>(OrderType() == <span class="number">class="num">1</span> && OrderSymbol() == <span class="predefines">_Symbol</span> && OrderMagicNumber() == OrderMagic && sig < <span class="number">class="num">0.5</span>) <span class="keyword">if</span>(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), <span class="number">class="num">0</span>, Red)) { } } <span class="comment">class=class="str">"cmt">// open positions and pending orders by signals</span> <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">if</span>(meta_sig > <span class="number">class="num">0.5</span>)</span>
无持仓时按信号开仓的执行分支
这段逻辑只在当前账户没有任何挂单或成交单、且保证金校验通过时才触发,避免重复加仓把净值曲线打爆。 信号阈值卡在 0.5:sig 小于该值走多单,否则走空单。多单以 Ask 入场,止损放在 Bid 减去 stoploss 个点,止盈放在 Ask 加上 takeprofit 个点;空单反向对称,以 Bid 入场、止损在 Ask 加 stoploss 点、止盈在 Bid 减 takeprofit 点。 LotsOptimized() 返回的手数直接用于下单,没有再二次缩放,所以这套仓位函数在外盘贵金属上可能把杠杆用到边界,实盘前务必在 MT5 策略测试器用 1:30 以下杠杆回测一遍。外汇与贵金属属高风险品种,上述触发条件仅描述代码行为,不预示任何胜率。
if(countOrders() == class="num">0 && CheckMoneyForTrade(_Symbol,LotsOptimized(),ORDER_TYPE_BUY)) { class="type">class="kw">double l = LotsOptimized(); if(sig < class="num">0.5) { OrderSend(Symbol(),OP_BUY,l, Ask, class="num">0, Bid-stoploss*_Point, Ask+takeprofit*_Point, NULL, OrderMagic); } else { OrderSend(Symbol(),OP_SELL,l, Bid, class="num">0, Ask+stoploss*_Point, Bid-takeprofit*_Point, NULL, OrderMagic); } }