经典策略重构(第14部分):多策略分析·进阶篇
(2/3)· 以为投票融合多策略就更稳,结果优化器挑了两套同涨同跌的策略,本文讲怎么破
把几套策略丢进遗传优化器让它们投票,权重自动算出来就以为组合更抗风险,这是多数人的直觉误判。优化器在框架里会抄近路,专挑走势高度相关的策略凑数,表面收益漂亮实则没分散任何风险。接上篇的自优化基础,这一篇直接拆开这个坑。
◍ 把 ONNX 模型接进 EA 的初始化链路
EA 启动的第一件事是把 Python 侧训练好的 ONNX 模型加载进来。模型推理前,必须用 Python 算出的均值与标准差对各输入列做缩放标准化,这两个数组在代码里叫 Z1(均值)和 Z2(标准差),数值直接写死成全局常量,来自遗传优化器挑出的最优参数。 程序开头用 #resource 把模型文件以 uchar 数组形式编进 exe,运行时再从缓冲区实例化 ONNX 模型并校验加载是否成功。全局变量不多,主要是交易类、时间类、RSI 与均线交叉策略的句柄,以及读模型、存预测结果的容器。 OnTick 里先调 NewCandle 判新日线,确认新 K 线后才更新策略参数与指标缓冲,再向 ONNX 跑推理:先减均值除标准差,再传输入。模型只预测“哪个子策略预期收益最大”,只有该策略自身也发出有效信号时才入场——这是双层过滤。 回测设置上,天数对齐之前前向测试的周期,延迟模式选“随机延迟”以贴近实盘。但加入统计模型后资金曲线明显走弱,总净利润与夏普比率双双下降,说明这套统计建模没带来预期增益,反而可能是过拟合或标准化错位。外汇与贵金属属高风险品种,任何模型回测退化都须先在 MT5 用相同参数复跑确认。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MSA Test class="num">1.mq5 | class=class="str">"cmt">//| Gamuchirai Ndawana | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Gamuchirai Ndawana" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX Model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\\Files\\EURUSD NN MSA.onnx" as class="type">uchar onnx_buffer[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX Parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Z1[] = { class="num">1.18932220e+00, class="num">1.19077958e+00, class="num">1.18786462e+00, class="num">1.18931542e+00, class="num">1.18994040e+00, class="num">1.18994674e+00, class="num">4.94395259e+01, -class="num">4.99204879e-04, -class="num">5.00701302e-04, -class="num">4.97575935e-04, -class="num">4.98995739e-04, -class="num">4.70848300e-04, -class="num">4.70289373e-04, -class="num">1.84697724e-02 };
「把特征数组和常驻参数钉死在头里」
这段声明把模型推理和策略运转依赖的静态量直接写死在全局区,先看清 Z2 这组浮点数是什么量级:前 6 个落在 0.109 附近,中间插了 10.045 和 0.0131 左右两段,最后以 12.855 收尾,共 14 个值,正好对应 ONNX_INPUTS 定义的输入维度。 常量区用 #define 固化了移动平均偏移为 0、类型为指数均线(MODE_EMA)、RSI 取收盘价、输入 14 输出 2、预测视野 HORIZON 为 38 根 K 线。这些数改一个,后面网络前处理和平仓周期都会跟着偏。 策略参数里 MA_PERIOD 设 100、RSI_PERIOD 设 24、周期锁 PERIOD_H3、持仓到期 HOLDING_PERIOD 也等于 38,和 HORIZON 对齐。外汇与贵金属杠杆高,参数回测吻合不代表实盘概率不变,上 MT5 前先单变量扫一遍。 依赖包含了 Trade 库和一套自研 VolatilityDoctor 头文件,其中 OpenCloseMACrossover 与 RSIMidPoint 是两个子策略壳,说明主逻辑是把均线交叉和中点 RSI 信号喂给 ONNX 模型做二次过滤。
class="type">class="kw">double Z2[] = {class="num">1.09599015e-01, class="num">1.09698934e-01, class="num">1.09479324e-01, class="num">1.09593123e-01, class="num">1.09413744e-01, class="num">1.09419007e-01, class="num">1.00452009e+01, class="num">1.31269558e-02, class="num">1.31336302e-02, class="num">1.31513465e-02, class="num">1.31174740e-02, class="num">6.88794916e-03, class="num">6.89036979e-03, class="num">1.28550006e+01 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| System constants | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define MA_SHIFT class="num">0 class="macro">#define MA_TYPE MODE_EMA class="macro">#define RSI_PRICE PRICE_CLOSE class="macro">#define ONNX_INPUTS class="num">14 class="macro">#define ONNX_OUTPUTS class="num">2 class="macro">#define HORIZON class="num">38 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Strategy Parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int MA_PERIOD = class="num">100; class=class="str">"cmt">//Moving Average Period class="type">int RSI_PERIOD = class="num">24; class=class="str">"cmt">//RSI Period ENUM_TIMEFRAMES STRATEGY_TIME_FRAME = PERIOD_H3; class=class="str">"cmt">//Strategy Timeframe class="type">int HOLDING_PERIOD = class="num">38; class=class="str">"cmt">//Position Maturity Period class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Dependencies | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Trade\Trade.mqh> class="macro">#include <VolatilityDoctor\Time\Time.mqh> class="macro">#include <VolatilityDoctor\Trade\TradeInfo.mqh> class="macro">#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh> class="macro">#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global Variables |
EA 初始化里的对象与句柄装配
这段初始化代码把策略跑起来前必须就位的几类东西一次性建好:自定义类型的动态实例、ONNX 模型、以及 MT5 内置指标句柄。看清楚它们分别在全局区和 OnInit 里怎么分配的,能少踩很多「句柄为无效值」的坑。 全局区先声明了 CTrade 实例和一堆指针:Time、TradeInfo、RSIMidPoint、OpenCloseMACrossover 都是后续 new 出来的自定义类;long 型的 onnx_model 存模型句柄,vectorf onnx_output 用 Zeros(ONNX_OUTPUTS) 预分配输出容器。指标读数数组 ma_reading[]、rsi_reading[] 等也在这里定义,但注意此时还没绑定长度。 OnInit 里先用 new 把各个自定义类实例化,构造函数都吃 Symbol() 和 STRATEGY_TIME_FRAME,MACross 还额外传了 MA_PERIOD、MA_SHIFT、MA_TYPE;RSIMid 传 RSI_PERIOD、RSI_PRICE。紧接着 OnnxCreateFromBuffer 从内存 buffer 加载模型,失败则返回 INVALID_HANDLE。 指标句柄用 iMA / iRSI 拿:ma_handle 看收盘价、ma_o_handle 看开盘价,周期参数同为 MA_PERIOD;rsi_handle 按 RSI_PERIOD 建。若 onnx_model 有效,代码会设输入 shape 为 {1, ONNX_INPUTS}、输出 shape 为 {ONNX_OUTPUTS, 1},任何一步 OnnxSetInputShape / OnnxSetOutputShape 失败就 INIT_FAILED 退出。 实盘前建议把 ONNX_INPUTS、ONNX_OUTPUTS、MA_PERIOD 这几个宏在头文件里打印一遍,确认和你的模型输入输出维度对得上,否则 EA 加载成功也不会出信号。外汇与贵金属杠杆高,模型维度错配可能让策略在极端波动下频繁误触发。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Custom Types CTrade Trade; Time *TradeTime; TradeInfo *TradeInformation; RSIMidPoint *RSIMid; OpenCloseMACrossover *MACross; class="type">long onnx_model; vectorf onnx_output; class=class="str">"cmt">//--- Our handlers for our indicators class="type">int ma_handle,ma_o_handle,rsi_handle; class=class="str">"cmt">//--- Data structures to store the readings from our indicators class="type">class="kw">double ma_reading[],ma_o_reading[],rsi_reading[]; class=class="str">"cmt">//--- System Types class="type">int position_timer; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Create dynamic instances of our custom types TradeTime = new Time(Symbol(),STRATEGY_TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME); MACross = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE); RSIMid = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE); onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT); onnx_output = vectorf::Zeros(ONNX_OUTPUTS); class=class="str">"cmt">//---Setup our technical indicators ma_handle = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,class="num">0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,class="num">0,MA_TYPE,PRICE_OPEN); rsi_handle = iRSI(_Symbol,STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE); if(onnx_model != INVALID_HANDLE) { Print("Preparing ONNX model"); class="type">ulong input_shape[] = {class="num">1,ONNX_INPUTS}; if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape)) { Print("Failed To Specify ONNX model input shape"); class="kw">return(INIT_FAILED); } class="type">ulong output_shape[] = {ONNX_OUTPUTS,class="num">1}; if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape)) { Print("Failed To Specify ONNX model output shape"); class="kw">return(INIT_FAILED); } } class=class="str">"cmt">//--- Everything was fine Print("Successfully loaded all components for our Expert Advisor"); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//--- End of OnInit Scope class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 日内新烛触发后的持仓与刷新逻辑
EA 在 OnDeinit 里必须手动释放资源:四个 new 出来的对象 TradeTime、TradeInformation、MACross、RSIMid 用 delete 回收,ONNX 模型句柄走 OnnxRelease,三个指标句柄 ma_handle、ma_o_handle、rsi_handle 调 IndicatorRelease 交还系统,漏掉任一个都可能让 MT5 退出时残留内存。 OnTick 的主轴是「新日线蜡烛」判断:TradeTime.NewCandle() 返回真才进分支,先跑 Update() 刷新指标,再按持仓状态分流。无持仓(PositionsTotal()==0)时清零 position_timer 并调 CheckSignal() 找信号;有持仓则比对 position_timer 与 HOLDING_PERIOD,到了就 PositionClose(Symbol()),没到就 position_timer++ 继续扛。 Update() 里 fetch = HORIZON * 2 决定抓取长度,对三条序列各 CopyBuffer 后立刻 ArraySetAsSeries(...,true),把索引 0 钉在最新一根,后续读 ma_reading[0]、rsi_reading[0] 才是当前值。外汇与贵金属杠杆高,HOLDING_PERIOD 设多大直接改持仓暴露时间,建议先开 MT5 用分钟图肉眼核对 NewCandle 的触发频次再定参。
class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- Delete the dynamic objects class="kw">delete TradeTime; class="kw">delete TradeInformation; class="kw">delete MACross; class="kw">delete RSIMid; OnnxRelease(onnx_model); IndicatorRelease(ma_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(rsi_handle); } class=class="str">"cmt">//--- End of Deinit Scope class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- Check if a new daily candle has formed if(TradeTime.NewCandle()) { class=class="str">"cmt">//--- Update strategy Update(); class=class="str">"cmt">//--- If we have no open positions if(PositionsTotal() == class="num">0) { class=class="str">"cmt">//--- Reset the position timer position_timer = class="num">0; class=class="str">"cmt">//--- Check for a trading signal CheckSignal(); } class=class="str">"cmt">//--- Otherwise else { class=class="str">"cmt">//--- The position has reached maturity if(position_timer == HOLDING_PERIOD) Trade.PositionClose(Symbol()); class=class="str">"cmt">//--- Otherwise keep holding else position_timer++; } } } class=class="str">"cmt">//--- End of OnTick Scope class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update our technical indicators | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Update(class="type">void) { class="type">int fetch = (HORIZON * class="num">2); class=class="str">"cmt">//--- Update the strategy RSIMid.Update(); MACross.Update(); class=class="str">"cmt">//---Set the values as series CopyBuffer(ma_handle,class="num">0,class="num">0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,class="num">0,class="num">0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(rsi_handle,class="num">0,class="num">0,fetch,rsi_reading); ArraySetAsSeries(rsi_reading,true); } class=class="str">"cmt">//--- End of Update Scope class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get A Prediction from our ONNX model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnnxPredict(class="type">void) {
「把 ONNX 输出接上均线交叉下单」
预测函数跑完之后,真正决定开仓的还是经典交叉逻辑,只不过前面用 ONNX 二分类过滤了哪套子策略当前更占优。onnx_output[0] 与 onnx_output[1] 是两个 sigmoid 概率,阈值卡在 0.5:当第一路大于 0.5 且第二路小于 0.5,模型认为均线类信号更可能盈利,才允许走 MACross 分支。 代码里 BuySignal 与 SellSignal 是封装好的交叉判定,命中即按最小手数吃单,止损止盈先传 0 交给外层统一管理。若反过来 onnx_output[0]<0.5 且 onnx_output[1]>0.5,则切到 RSIMid 分支,说明模型倾向 RSI 中段反转更靠谱。 这种结构的好处是:单一 MT5 EA 里并存多套逻辑,由轻量神经网络按近期样本动态切换,而不是写死只用一种。外汇与贵金属杠杆高,切换频率过快可能放大滑点与重绘风险,建议在策略测试器里把 HORIZON 从 10 调到 20 观察信号衰减。
vectorf input_variables =
{
iOpen(_Symbol,STRATEGY_TIME_FRAME,class="num">0),
iHigh(_Symbol,STRATEGY_TIME_FRAME,class="num">0),
iLow(_Symbol,STRATEGY_TIME_FRAME,class="num">0),
iClose(_Symbol,STRATEGY_TIME_FRAME,class="num">0),
ma_reading[class="num">0],
ma_o_reading[class="num">0],
rsi_reading[class="num">0],
iOpen(_Symbol,STRATEGY_TIME_FRAME,class="num">0) - iOpen(_Symbol,STRATEGY_TIME_FRAME,(class="num">0 + HORIZON)),
iHigh(_Symbol,STRATEGY_TIME_FRAME,class="num">0) - iHigh(_Symbol,STRATEGY_TIME_FRAME,(class="num">0 + HORIZON)),
iLow(_Symbol,STRATEGY_TIME_FRAME,class="num">0) - iLow(_Symbol,STRATEGY_TIME_FRAME,(class="num">0 + HORIZON)),
iClose(_Symbol,STRATEGY_TIME_FRAME,class="num">0) - iClose(_Symbol,STRATEGY_TIME_FRAME,(class="num">0 + HORIZON)),
ma_reading[class="num">0] - ma_reading[(class="num">0 + HORIZON)],
ma_o_reading[class="num">0] - ma_o_reading[(class="num">0 + HORIZON)],
rsi_reading[class="num">0] - rsi_reading[(class="num">0 + HORIZON)]
};
for(class="type">int i = class="num">0; i < ONNX_INPUTS;i++)
{
input_variables[i] = ((input_variables[i] - Z1[i])/ Z2[i]);
}
OnnxRun(onnx_model,ONNX_DEFAULT,input_variables,onnx_output);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check for a trading signal using our cross-over strategy |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckSignal(class="type">void)
{
OnnxPredict();
class=class="str">"cmt">//--- MA Strategy is profitable
if((onnx_output[class="num">0] > class="num">0.5) && (onnx_output[class="num">1] < class="num">0.5))
{
class=class="str">"cmt">//--- Long positions when the close moving average is above the open
if(MACross.BuySignal())
{
Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),class="num">0,class="num">0,"");
class="kw">return;
}
class=class="str">"cmt">//--- Otherwise class="type">short
else
if(MACross.SellSignal())
{
Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),class="num">0,class="num">0,"");
class="kw">return;
}
}
class=class="str">"cmt">//--- RSI strategy is profitable
else
if((onnx_output[class="num">0] < class="num">0.5) && (onnx_output[class="num">1] > class="num">0.5))
{
if(RSIMid.BuySignal())
{