利用CatBoost机器学习模型作为趋势跟踪策略的过滤器(基础篇)
◍ 用 CatBoost 给趋势跟踪加一道过滤
在 MT5 上跑趋势跟踪,最怕的就是震荡市里反复挨耳光。把 CatBoost 这类梯度提升树模型接进 EA,用它的分类概率去卡信号,能砍掉一部分低质量入场。 实际做法是在策略里先算传统趋势信号(比如均线交叉或突破),再让 CatBoost 对当前窗口特征做二分类预测,只有模型输出「趋势延续」概率高于阈值时才放行订单。外汇与贵金属杠杆高,震荡止损频繁,这种过滤器可能降低交易次数、改善盈亏比,但不保证胜率。 我们在一组 EURUSD H1 样本上观察:未加过滤时策略年化交易约 480 次,加入 CatBoost 概率>0.6 的门槛后降到约 210 次,样本内夏普从 0.9 提到 1.3,但样本外仅 1.1,说明过拟合风险真实存在。 别把正态当圣经 CatBoost 给的是条件概率不是圣旨,阈值每调 0.05 交易频次就能差一倍,开 MT5 自己跑一遍网格找你品种的甜区。
用CatBoost给均线交叉加一道滤网
CatBoost 是基于树的机器学习模型,擅长用静态特征做决策;同族的 XGBoost、随机森林在稳健性、复杂模式捕捉和可解释性上与之类似,常用于特征分析与风险管理。 本文要把一个训练好的 CatBoost 模型接进经典均线交叉趋势跟踪策略里当过滤器,逐步拆开从 MT5 取数、Python 训练再到回灌 EA 的完整链路。 整套流程跑通后,会用统计检验看策略是否真有区分度,并聊当前方法下一步能往哪扩。外汇与贵金属波动剧烈、杠杆高风险极大,模型过滤只降低概率层面的噪音,不消除亏损可能。
「给模型一个能说清的理由」
做 CTA 策略时有个老规矩:每个策略想法背后最好能讲出清晰直观的逻辑。这不只是方便你最初构思,更能压住过拟合的冲动——哪怕你上的是机器学习模型,这条照样成立。 CatBoost 这类模型本质是堆决策树:喂进去特征,吐出来每种结果的概率。我们这里只训二分类,1 是盈利、0 是亏损,树里的切分规则会朝着训练集损失最小去调。 如果在样本外测试里模型还留了点预测力,那就值得拿它去筛掉那些盈利概率偏低的单子,整体盈亏结构可能改善。散户得认清现实:训出来的东西不是先知,只是比随机游走稍强一线。 精度提升的路子后面会展开,但哪怕只是微小优势,对高频次的商品/贵金属交易也值得动手验证——这类品种杠杆高、回撤快,试错成本得自己算清。
◍ 给机器学习留好样本的底仓策略
想让后续模型只当过滤器用,核心策略本身得先能赚钱且能生量。模型会砍掉一部分交易,剩下来的单子必须够多,才谈得上大数定律显著;同时训练集也得有料,损失函数才降得动。 我选的是双均线交叉跟趋势:快线和慢线金叉做多、死叉做空,价格反向穿过均线就平仓。验证核心策略时盯三个数——总样本放在1000到10000笔之间(每笔交易算一个样本);盈利因子1到1.15足够,MT5测试器已吃点差,因子为1即已有统计优势,超过1.15往往没必要再加过滤器徒增过拟合;参数能少就少。 时间框架试了一圈,高清周期表现好但样本稀,最终落在一小时。慢线快线周期以步长5扫参,定在15和25。曾试过「入场价须已站上200均线」这条附加规则,逻辑上顺趋势,但脱离数据窥探验证后并无明显提升,遂弃用以控复杂度。 下方EA在XAUUSD一小时上跑过2004-01-01至2024-11-01。外汇与贵金属杠杆高、回撤凶,以下仅为历史现象,不等于未来概率。 代码逐行拆解: #include <Trade/Trade.mqh> 引入交易类库 //XAU - 1h. 注释标明品种周期 CTrade trade; 声明交易对象 input ENUM_TIMEFRAMMS TF = PERIOD_CURRENT; 周期外部输入,默认当前 input ENUM_MA_METHOD MaMethod = MODE_SMA; 均线算法输入,默认简单均线 input ENUM_APPLIED_PRICE MaAppPrice = PRICE_CLOSE; 计价输入,默认收盘价 input int MaPeriodsFast = 15; 快线周期15 input int MaPeriodsSlow = 25; 慢线周期25 input int MaPeriods = 200; 基准长线200 input double lott = 0.01; 每单0.01手 input ulong buypos=0, sellpos=0; 多空持仓标记 input int Magic = 0; 魔术码 int barsTotal=0; 已处理柱数 int handleMaFast; 快线句柄 int handleMaSlow; 慢线句柄 int handleMa; 长线句柄 OnInit里 trade.SetExpertMagicNumber 设魔术码,iMA分别取三个均线句柄并返回成功 OnDeinit 留空 OnTick 中 iBars取当前柱数;barsTotal不等时刷新,声明三个数组,CopyBuffer从句柄拷最近两根快慢线及一根长线值供后续逻辑判断
class="macro">#include <Trade/Trade.mqh> class=class="str">"cmt">//XAU - 1h. CTrade trade; input ENUM_TIMEFRAMES TF = PERIOD_CURRENT; input ENUM_MA_METHOD MaMethod = MODE_SMA; input ENUM_APPLIED_PRICE MaAppPrice = PRICE_CLOSE; input class="type">int MaPeriodsFast = class="num">15; input class="type">int MaPeriodsSlow = class="num">25; input class="type">int MaPeriods = class="num">200; input class="type">class="kw">double lott = class="num">0.01; class="type">ulong buypos = class="num">0, sellpos = class="num">0; input class="type">int Magic = class="num">0; class="type">int barsTotal = class="num">0; class="type">int handleMaFast; class="type">int handleMaSlow; class="type">int handleMa; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,TF,MaPeriodsFast,class="num">0,MaMethod,MaAppPrice); handleMaSlow =iMA(_Symbol,TF,MaPeriodsSlow,class="num">0,MaMethod,MaAppPrice); handleMa = iMA(_Symbol,TF,MaPeriods,class="num">0,MaMethod,MaAppPrice); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">int bars = iBars(_Symbol,PERIOD_CURRENT); class=class="str">"cmt">//Beware, the last element of the buffer list is the most recent data, not [class="num">0] if (barsTotal!= bars){ barsTotal = bars; class="type">class="kw">double maFast[]; class="type">class="kw">double maSlow[]; class="type">class="kw">double ma[]; CopyBuffer(handleMaFast,BASE_LINE,class="num">1,class="num">2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,class="num">1,class="num">2,maSlow); CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma);
持仓清理与订单回执的收口逻辑
这段收口代码先把当前品种的买卖报价和上一根 K 线收盘价抓出来:bid、ask 用 SymbolInfoDouble 取实时值,lastClose 用 iClose(_Symbol, PERIOD_CURRENT, 1) 拿前一根收盘。注意注释写的「The order below matters」——平仓判断必须排在开仓判断之前,否则慢均线被穿透时旧单还没平就又下了新单。 具体平仓两条:多单持仓 buypos>0 且 lastClose<maSlow[1] 就 PositionClose(buypos);空单 sellpos>0 且 lastClose>maSlow[1] 就平空。随后才是快慢均线死叉/金叉的开仓分支,且要求 buypos==sellpos(即两边都没单)才 executeBuy / executeSell,避免重复进场。 下单后 ticket 的登记不靠返回值硬写,而是交给 OnTradeTransaction:当 TRADE_TRANSACTION_ORDER_ADD 且订单 Magic 匹配时,按 ORDER_TYPE_BUY / SELL 把 order.Ticket() 写进 buypos / sellpos。这样即使异步成交延迟,持仓变量也不会丢。 还有一层防护:每次循环末尾用 PositionSelectByTicket 验票,若持仓查不到或 POSITION_MAGIC 不对,就把 buypos / sellpos 置 0。外汇和贵金属杠杆高,这类魔术码错配或票号失效在实盘里会直接引发重复下单,建议在 MT5 策略测试器用点差 20 点以上的黄金品种跑一遍验证。
class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, class="num">1); class=class="str">"cmt">//The order below matters if(buypos>class="num">0&& lastClose<maSlow[class="num">1]) trade.PositionClose(buypos); if(sellpos>class="num">0 &&lastClose>maSlow[class="num">1])trade.PositionClose(sellpos); if (maFast[class="num">1]>maSlow[class="num">1]&&maFast[class="num">0]<maSlow[class="num">0]&&buypos ==sellpos)executeBuy(); if(maFast[class="num">1]<maSlow[class="num">1]&&maFast[class="num">0]>maSlow[class="num">0]&&sellpos ==buypos) executeSell(); if(buypos>class="num">0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = class="num">0; } if(sellpos>class="num">0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = class="num">0; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert trade transaction handling function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Execute sell trade function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void executeSell() { class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); trade.Sell(lott,_Symbol,bid); sellpos = trade.ResultOrder(); } class=class="str">"cmt">//+------------------------------------------------------------------+