因果网络分析(CNA)、随机模型最优控制(SMOC)和纳什博弈论结合深度学习的示例·综合运用
(3/3)·把因果网络、随机最优控制与博弈均衡塞进同一个EA,看深度学习如何改写信号质量
◍ 用 ONNX 模型给收盘价打方向标签
这段逻辑把下一根 K 线的时间锚点和当前收盘价的极值跟踪先算好,再丢给 PredictPrice() 跑推理。ExtNextBar 用 TimeCurrent() 减去自身对 PeriodSeconds() 取模的余数,再加一个周期秒数,等于把时间戳对齐到整根 bar 的边界,避免半截 K 线触发预测。 PredictPrice() 里先建两个静态 vectorf:output_data(1) 接模型输出,x_norm(SAMPLE_SIZE) 装归一化收盘价。若 ExtMin>=ExtMax 说明样本里没有波动,直接把 ExtPredictedClass 置 -1 并返回,不浪费一次推理。 归一化做得直白:x_norm 减去 ExtMin 再除以 (ExtMax-ExtMin),把价格压到 0~1 区间。OnnxRun 跑完把 output_data[0] 反归一化回真实价 predicted,再用 last_close-predicted 的 delta 判方向——fabs(delta)<=0.00001 算 PRICE_SAME,delta<0 判 PRICE_UP,否则 PRICE_DOWN。 GenerateSignal 的买卖条件很硬:多单要 prediction>0.00001 且 rsi<30 且趋势强且波动合格且快线在慢线上,并叠加 dlsgnl==PRICE_UP;空单镜像对称。外汇与贵金属杠杆高,信号只是概率倾向,实盘前请在 MT5 用策略测试器按 SAMPLE_SIZE 跑一遍确认延迟。
ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); class=class="str">"cmt">//--- check min and max class="type">class="kw">float close=(class="type">class="kw">float)iClose(_Symbol,_Period,class="num">0); if(ExtMin>close) ExtMin=close; if(ExtMax<close) ExtMax=close; PredictPrice(); class="type">int signal = GenerateSignal(symbol, prediction); class="type">int signal = GenerateSignal(symbol, prediction, ExtPredictedClass); class="type">int GenerateSignal(class="type">class="kw">string symbol, class="type">class="kw">double prediction, class="type">int dlsgnl) class="type">bool buy_condition = prediction > class="num">0.00001 && rsi < class="num">30 && trend_strong && volatility_ok && fastMA > slowMA; class="type">bool sell_condition = prediction < -class="num">0.00001 && rsi > class="num">70 && trend_strong && volatility_ok && fastMA < slowMA; class="type">bool buy_condition = prediction > class="num">0.00001 && rsi < class="num">30 && trend_strong && volatility_ok && fastMA > slowMA && dlsgnl==PRICE_UP; class="type">bool sell_condition = prediction < -class="num">0.00001 && rsi > class="num">70 && trend_strong && volatility_ok && fastMA < slowMA && dlsgnl==PRICE_DOWN; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PredictPrice(class="type">void) { class="kw">static vectorf output_data(class="num">1); class=class="str">"cmt">// vector to get result class="kw">static vectorf x_norm(SAMPLE_SIZE); class=class="str">"cmt">// vector for prices normalize class=class="str">"cmt">//--- check for normalization possibility if(ExtMin>=ExtMax) { Print("ExtMin>=ExtMax"); ExtPredictedClass=-class="num">1; class="kw">return; } class=class="str">"cmt">//--- request last bars if(!x_norm.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,class="num">1,SAMPLE_SIZE)) { Print("CopyRates ",x_norm.Size()); ExtPredictedClass=-class="num">1; class="kw">return; } class="type">class="kw">float last_close=x_norm[SAMPLE_SIZE-class="num">1]; class=class="str">"cmt">//--- normalize prices x_norm-=ExtMin; x_norm/=(ExtMax-ExtMin); class=class="str">"cmt">//--- run the inference if(!OnnxRun(ExtHandle,ONNX_NO_CONVERSION,x_norm,output_data)) { Print("OnnxRun"); ExtPredictedClass=-class="num">1; class="kw">return; } class=class="str">"cmt">//--- denormalize the price from the output value class="type">class="kw">float predicted=output_data[class="num">0]*(ExtMax-ExtMin)+ExtMin; class=class="str">"cmt">//--- classify predicted price movement class="type">class="kw">float delta=last_close-predicted; if(fabs(delta)<=class="num">0.00001) ExtPredictedClass=PRICE_SAME; else { if(delta<class="num">0) ExtPredictedClass=PRICE_UP; else ExtPredictedClass=PRICE_DOWN; } } class="type">void GetMinMax(class="type">void) { vectorf close;
日线收盘采样里的极值抓取
在 MT5 里做日线级别统计,先要把收盘价序列拉进数组再谈别的。上面这段把当前品种 D1 周期的收盘价按 SAMPLE_SIZE 条样本拷进 close 数组,紧跟着用 Min() 和 Max() 直接拿到这段样本的最低收和最高收。 这里有个容易被忽略的点:COPY_RATES_CLOSE 只取收盘价,不连带高低时间,所以 ExtMin、ExtMax 是收盘价的边界,不是日 K 实体的边界。若你想框定真实波幅,得换 COPY_RATES_OHLC 重新采。 实盘里把 SAMPLE_SIZE 设成 60,就能快速看近三个月日线收盘的上下沿;外汇和贵金属杠杆高、跳空频繁,这套边界只反映历史样本,下破或上破都只是概率事件,别当支撑阻力铁律。
close.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,class="num">0,SAMPLE_SIZE); ExtMin=close.Min(); ExtMax=close.Max(); }
「SMOC 改写时砍掉的两处初始化」
把上一版的 SMOC 逻辑搬过来时,输入参数里不用再给 dlsignal 赋初值,也别把 CTrade 对象塞进全局声明。这两样在纳什均衡脚本里由 OnTick 内部的状态变量接手,硬加只会增加编译警告。 OnTick 里先按日线边界刷一次 ExtMin / ExtMax,再用当前收盘价动态更新极值。SAMPLE_SIZE 固定为 120,意味着模型输入窗口是 120 根 K 线的归一化序列,跑 EURUSD_D1.onnx 时少一根都会让 PredictPrice() 吐出越界结果。 ManageOpenOrder() 现在靠 barsOpen 计数平仓:用 iBars 减 iBarShift 拿到持仓经历的根数,超过 maxBarsOpen 就触发退出。ExecuteTrade() 的入场闸门改成 decision!=0 且双向趋势过滤(IsTrendFavorable + IsLongTermTrendFavorable)都过,且 ExtPredictedClass 必须是 PRICE_UP 或 PRICE_DOWN——同价信号直接被拦在门外。 外汇与贵金属杠杆高,这套过滤只是降低噪音,不等于方向锁死,实盘前请在 MT5 策略测试器用 2020–2023 的 D1 数据跑一遍再上。
class="macro">#resource "/Files/EURUSD_D1.onnx" as class="type">uchar ExtModel[] class="macro">#define SAMPLE_SIZE class="num">120 class="type">long ExtHandle=INVALID_HANDLE; class="type">int ExtPredictedClass=-class="num">1; class="type">class="kw">datetime ExtNextBar=class="num">0; class="type">class="kw">datetime ExtNextDay=class="num">0; class="type">class="kw">float ExtMin=class="num">0.0; class="type">class="kw">float ExtMax=class="num">0.0; class=class="str">"cmt">//--- price movement prediction class="macro">#define PRICE_UP class="num">0 class="macro">#define PRICE_SAME class="num">1 class="macro">#define PRICE_DOWN class="num">2 class="type">void OnTick() { class=class="str">"cmt">//--- check new day if(TimeCurrent()>=ExtNextDay) { GetMinMax(); class=class="str">"cmt">//--- set next day time ExtNextDay=TimeCurrent(); ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1); ExtNextDay+=PeriodSeconds(PERIOD_D1); } class=class="str">"cmt">//--- check new bar if(TimeCurrent()<ExtNextBar) class="kw">return; class=class="str">"cmt">//--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); class=class="str">"cmt">//--- check min and max class="type">class="kw">float close=(class="type">class="kw">float)iClose(_Symbol,_Period,class="num">0); if(ExtMin>close) ExtMin=close; if(ExtMax<close) ExtMax=close; PredictPrice(); class="kw">static class="type">class="kw">datetime lastBarTime = class="num">0; class="type">class="kw">datetime currentBarTime = iTime(Symbol(), PERIOD_CURRENT, class="num">0); class=class="str">"cmt">// Only process on new bar if(currentBarTime == lastBarTime) class="kw">return; lastBarTime = currentBarTime; class="type">class="kw">double currentPrice = SymbolInfoDouble(Symbol(), SYMBOL_LAST); class="type">int decision = OptimalControl(currentPrice); class="type">class="kw">string logMessage = StringFormat("New bar: Time=%s, Price=%f, Decision=%d", TimeToString(currentBarTime), currentPrice, decision); LogMessage(logMessage); class=class="str">"cmt">// Manage open order if exists if(orderTicket != class="num">0) { ManageOpenOrder(decision, ExtPredictedClass); } if(orderTicket == class="num">0 && decision != class="num">0 && IsTrendFavorable(decision) && IsLongTermTrendFavorable(decision) && (ExtPredictedClass==PRICE_DOWN || ExtPredictedClass==PRICE_UP)) { ExecuteTrade(decision, ExtPredictedClass); } } class="type">void ManageOpenOrder(class="type">int decision, class="type">int dlsignal) { class="type">int barsOpen = iBars(Symbol(), PERIOD_CURRENT) - iBarShift(Symbol(), PERIOD_CURRENT, orderOpenTime); LogMessage(StringFormat("Bars open: %d for order ticket: %d", barsOpen, orderTicket)); if(barsOpen >= maxBarsOpen ||
◍ 平仓与开仓的执行分支
这段逻辑把持仓管理和新单触发拆成两条路:当已有多单且决策看空、日线信号也向下,或已有空单且决策看多、日线信号向上时,直接调用 CloseOrder 清掉当前票,并把 orderTicket 与 orderOpenTime 归零,避免后续误判持仓状态。 若不满足反向平仓条件,则走 UpdateSLTP 按最新 decision 移动止损止盈,等于把已有仓位的风控线动态跟着信号走。外汇与贵金属杠杆高,SL/TP 被拉太宽可能瞬间吞掉本金,调参时先在小周期回测。 ExecuteTrade 里先取 SYMBOL_ASK 或 SYMBOL_BID 作为市价,decision 与 dlsignal 同向才进场:1 配 PRICE_UP 做多,-1 配 PRICE_DOWN 做空。SL/TP 由 CalculateDynamicSL / CalculateDynamicTP 按价格与方向算出,volume 走 AdjustLotSizeForDrawdown 做回撤自适应,不是写死 lotSize。 request.deviation 设 10 点、magic=123456 便于在 MT5 订单池里筛出本 EA 的痕迹;OrderSend 成功后用 iTime 抓当前 K 线时间写入 orderOpenTime,失败则 Print 错误码。把这段代码贴进 MT5 的 EA 模板,改 magic 与 deviation 就能跑通基础闭环。
(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && decision == -class="num">1 && dlsignal == PRICE_DOWN) || (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && decision == class="num">1 && dlsignal == PRICE_UP)) { CloseOrder(orderTicket); orderTicket = class="num">0; orderOpenTime = class="num">0; } else { UpdateSLTP(orderTicket, decision); } } class="type">void ExecuteTrade(class="type">int decision, class="type">int dlsignal) { class="type">MqlTradeRequest request = {}; class="type">MqlTradeResult result = {}; class="type">class="kw">double price; class="type">int decision1; if(decision == class="num">1 && dlsignal == PRICE_UP) { price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); request.type = ORDER_TYPE_BUY; decision1 = class="num">1; } else if(decision == -class="num">1 && dlsignal == PRICE_DOWN) { price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.type = ORDER_TYPE_SELL; decision1 = -class="num">1; } class="type">class="kw">double sl = CalculateDynamicSL(price, decision1); class="type">class="kw">double tp = CalculateDynamicTP(price, decision1); class="type">class="kw">double adjustedLotSize = AdjustLotSizeForDrawdown(); request.volume = adjustedLotSize; request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); class=class="str">"cmt">//request.volume = lotSize; class=class="str">"cmt">//request.type = (decision == class="num">1 && dlsignal == PRICE_UP) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; request.price = price; request.sl = sl; request.tp = tp; request.deviation = class="num">10; request.magic = class="num">123456; class=class="str">"cmt">// Magic number to identify orders from this EA request.comment = (decision == class="num">1) ? "Buy Order" : "Sell Order"; if(OrderSend(request, result)) { Print((decision1 == class="num">1 ? "Buy" : "Sell"), " Order Executed Successfully. Ticket: ", result.order); orderOpenTime = iTime(Symbol(), PERIOD_CURRENT, class="num">0); orderTicket = result.order; } else { Print("Error executing Order: ", GetLastError()); } }
把纳什均衡塞进 OnTick 的改法
这一版只动 OnTick() 一个函数,其余逻辑和先前保持一致。核心变化是:每根新_bar 上先做市场状态识别(HMM 与对数似然两套 regime),再让 CalculateStrategySignals 算出各策略信号,最后单独拎出 strategies[3] 这个纳什均衡策略做条件开仓。 代码前半段仍是日界与_bar界的时间戳刷新:TimeCurrent() 超过 ExtNextDay 就重抓高低点并推进到次日 0 点;小于 ExtNextBar 直接 return,避免重复计算。注意 ExtNextBar 用 PeriodSeconds() 无参调用,依赖当前图表周期。 中段把当前收盘价转 float 后持续刷新 ExtMin / ExtMax,随后调 PredictPrice() 得到 ExtPredictedClass(PRICE_UP / PRICE_DOWN)。若 IsNewBar() 不成立直接退出,保证信号只在_bar开盘处理一次。 真正的新增在尾部:DetectMarketRegime 填充 hmmRegime 与 logLikelihoodRegime,传入信号计算函数。当 strategies[3].enabled 且 signal 非零,并且预测方向与 signal 同向(买看 PRICE_UP、卖看 PRICE_DOWN)才触发 OpenBuyOrder / OpenSellOrder。最后若 UseTrailingStop 为真则更新跟踪止损。 外汇与贵金属杠杆高、滑点跳空频繁,这套条件单在实盘可能因点差扩大而错失触发,建议在 MT5 用 EURUSD 15 分钟图先跑复盘,把 strategies[3].enabled 从 false 改 true 看信号密度。
class="type">void OnTick() { class=class="str">"cmt">//--- check new day if(TimeCurrent()>=ExtNextDay) { GetMinMax(); class=class="str">"cmt">//--- set next day time ExtNextDay=TimeCurrent(); ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1); ExtNextDay+=PeriodSeconds(PERIOD_D1); } class=class="str">"cmt">//--- check new bar if(TimeCurrent()<ExtNextBar) class="kw">return; class=class="str">"cmt">//--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); class=class="str">"cmt">//--- check min and max class="type">class="kw">float close=(class="type">class="kw">float)iClose(_Symbol,_Period,class="num">0); if(ExtMin>close) ExtMin=close; if(ExtMax<close) ExtMax=close; PredictPrice(); if(!IsNewBar()) class="kw">return; class=class="str">"cmt">// Solo procesar en nueva barra MarketRegime hmmRegime = NOT_PRESENT; MarketRegime logLikelihoodRegime = NOT_PRESENT; DetectMarketRegime(hmmRegime, logLikelihoodRegime); class=class="str">"cmt">// Calcular señales para cada estrategia CalculateStrategySignals(_Symbol, TimeCurrent(), hmmRegime, logLikelihoodRegime); class=class="str">"cmt">// Verificar si la estrategia de Nash Equilibrium ha generado una señal if(strategies[class="num">3].enabled && strategies[class="num">3].signal != class="num">0) { if(strategies[class="num">3].signal > class="num">0 && ExtPredictedClass==PRICE_UP) { OpenBuyOrder(strategies[class="num">3].name); } else if(strategies[class="num">3].signal < class="num">0 && ExtPredictedClass==PRICE_DOWN) { OpenSellOrder(strategies[class="num">3].name); } } class=class="str">"cmt">// Actualizar stops de seguimiento si es necesario if(UseTrailingStop) { UpdateTrailingStops(); } }
「保守模型与裸因果网络的实盘反差」
同一套因果网络分析(CNA)框架,接不接深度学习层,跑出来的脾气完全两样。裸 CNA 的 EA 在回测里做了 1292 笔交易,带深度学习的只做了 58 笔——频率差出二十多倍,前者明显靠刷量吃行情,后者基本在等高置信信号。 裸模型的盈利能力数字更好看,但回撤也更大,属于典型的高换手换收益、敞口不收手。带深度学习的版本回撤压得更低,连续盈利或亏损的 streaks 更短,看起来更像在刻意控制暴露,而不是赌单边。 胜率上两者都没过 50%:深度学习版略高一点,裸版更低,说明 CNA 本身不是靠胜率吃饭,而是靠盈亏比和频率错位。裸版明显偏做空,深度学习版多空更平衡,这对贵金属和外汇直盘的反转窗口意义不一样。 外汇与贵金属杠杆高、跳空频繁,这种回撤差异不是纸面指标。开 MT5 把同一周期数据分别跑两版,重点看 58 笔那版是不是在你常做的时段根本不出手——若信号稀缺,实盘滑点会先把低频优势磨掉。
◍ SMOC_DL 与 SMOC 的回测硬指标对照
把同一品种上跑出的两份 SMOC 报告摊开看,差异直接落在数字里。策略 2(SMOC_DL)净利润 4.32,策略 1(SMOC)是 -7.33,一个转正一个吃亏。夏普比率更刺眼:策略 2 为 1.69,策略 1 是 -3.77,风险调整后回报谁优谁劣不用多说。 胜率上策略 2 干到 59.38%,比策略 1 的 54% 高出五点多个百分点;交易次数也更多,64 对 50,说明带深度学习的版本可能挖到了更密集的入场窗口。盈利因子一道分水岭:策略 2 是 1.13(大于 1 即净赚),策略 1 仅 0.74,亏钱概率占优。 两边回撤都压在账户 0.01%,风控动作算克制。方向偏好上二者都爱做空,但策略 2 多空分布更匀,不像策略 1 一边倒。单看这组数,SMOC_DL 是更稳的选择,换时间框架喂给模型也值得一试。 不挂深度学习的 NASH 版本另算一笔账:总盈利和风险调整回报反而更好,代价是最大回撤偏高,交易更频繁、多头处理更利索;DL 版则保守,回撤小但整体收益薄。这套对照只来自一个回测周期,外汇与贵金属属高风险品种,换时段和多行情重跑前,别把结论当必然。
收束
把深度学习模型塞进 CNA、SMOC 和纳什博弈这三套 EA 里,结果并不整齐:CNA 的 DL 版收益降了但回撤同步缩窄,交易一致性反而更好;SMOC 的 DL 版胜率和风险调整回报都压过原版;纳什博弈原版总盈利更高,DL 版只是更保守却没赚到更多。 这种分化说明 DL 不是万能插件,它能不能提效高度依赖底层策略和当时市场。回测只跑过特定周期与品种,换时间框架或行情就得重测,否则容易把局部结论当普适规律。 外汇与贵金属属高风险品种,任何策略回测优绩都不代表实盘必然复现,上 MT5 前先用作者给的 ONNX 与 mq5 文件做跨周期验证再谈参数微调。