用纳什均衡给多品种 EA 定策略
◍ 用纳什均衡给多品种 EA 定策略
在 MT5 上做跨品种交易系统,最大的坑不是信号算不准,而是几个货币对互相牵制时你按单边逻辑下单,容易被联动波动洗掉。AdaptiveQ Enhanced 这类 EA 的思路是:把市场切成 531,441 种独特状态,同时盯住七大主要货币对之间的相互关系,再用纳什均衡在「谁动都会影响别人」的条件下挑出最优动作。 具体落地时,它不只做普通的买和卖,还加了逐步建仓、选择性平掉盈利仓位两种扩展操作。这意味着同一套算法能在趋势延续和震荡反转里切换节奏,而不是死扛一个方向。外汇与贵金属属高风险品种,这类多品种联动策略在历史回测中表现依赖状态覆盖度,实盘仍可能因流动性断裂而失效,须用小资金验证。 本文后续会拆它在 MQL5 里的实际写法。先记住一个数据点:531,441 这个状态数来自 3 的 12 次方量级拆分(7 品种 × 若干状态维度),开 MT5 跑同类网格前,先确认你的状态空间有没有覆盖主要联动情形。
「用纳什均衡给多品种策略去耦」
把单品种交易系统塞进多品种环境,最容易踩的坑是各自为战。DQN 那套 Q(s,a)=r+γ·max Q(s',a') 只解决「在某个状态下选哪个动作期望效用高」,但外汇里 EURUSD 和 USDCHF 经常反向,孤立优化等于自己跟自己对冲。 博弈论里的纳什均衡给出另一条路:没有任何一个玩家能靠单方面改策略把分再刷高,系统就稳了。落到代码上就是 DetermineActionNash(),它不再只看本品种的 Q 矩阵,而是把其他品种的相关性拽进来一起算分。 相关性阈值卡在 0.3 绝对值,低于这个数的杂音直接丢掉,避免被随机共动带偏。调参时若发现信号频繁反转,先查 correlationMatrix 里是不是混进了大量 0.1~0.2 的弱相关——它们本就不该进修正项。 外汇与贵金属杠杆高、跳空多,纳什修正只降低共振误判概率,不保证头寸方向正确,实盘前请用 MT5 策略测试器跑多品种历史验证。
class="type">void DetermineActionNash(class="type">int symbolIdx) { class="type">class="kw">double actionScores[TOTAL_ACTIONS]; class=class="str">"cmt">// Get basic estimates of actions from Q-matrix for(class="type">int action = class="num">0; action < TOTAL_ACTIONS; action++) { actionScores[action] = qMatrix[tradeStates[symbolIdx].currentStateIdx][action][symbolIdx]; } class=class="str">"cmt">// Correction based on correlation with other symbols(Nash) for(class="type">int otherIdx = class="num">0; otherIdx < TOTAL_SYMBOLS; otherIdx++) { if(otherIdx != symbolIdx) { class="type">class="kw">double corr = correlationMatrix[symbolIdx][otherIdx]; for(class="type">int action = class="num">0; action < TOTAL_ACTIONS; action++) { class="type">class="kw">double otherScore = qMatrix[tradeStates[otherIdx].currentStateIdx][action][otherIdx]; class=class="str">"cmt">// Take correlation into account only if there is significant relationship if(MathAbs(corr) > class="num">0.3) { actionScores[action] += corr * otherScore * class="num">0.05; } } } } }
让盈利单先走、亏损单留着
AdaptiveQ Enhanced 把动作空间从单纯开平扩展到了六个:建买、建卖、加买、加卖、只平盈利买仓、只平盈利卖仓。相比老版本只能二选一,现在能在趋势顺向时加量、在反转预期下保留亏损敞口,战略维度明显更厚。 「只平盈利仓位」是这套机制里最值得在 MT5 里跑一遍验证的点。它的逻辑是锁利但不 prematurely 砍掉浮亏单——若后续价格回归,亏损仓可能回正;若继续不利,再走止损流程。回测中这类选择性兑现往往改善组合收益曲线,但外汇与贵金属杠杆高,滑点和隔夜成本会吃掉部分优势,结论仅具概率性。 下面这段 MQL5 实现了按品种与持仓方向平掉盈利单: void ClosePositionsByTypeIfProfitable(string symbol, ENUM_POSITION_TYPE posType) { CTrade trade; CPositionInfo pos; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(pos.SelectByIndex(i)) { if(pos.Symbol() == symbol && pos.PositionType() == posType) { double profit = pos.Profit(); // Close only if position is in profit if(profit > 0) { trade.PositionClose(pos.Ticket()); } } } } } 逐行看:函数接收品种名与持仓类型(买或卖);CTrade 负责下单、CPositionInfo 读仓;倒序遍历所有持仓避免索引错位;SelectByIndex 选中第 i 仓;品种与方向双匹配才进下一步;profit 取当前浮盈;注释标明只平盈利;profit>0 时按 Ticket 平掉该仓。 把这个函數挂到 EA 的退出分支里,你能直接对比「全平」与「仅平盈利」两种退出在欧美小时图上的净值回撤差异。
class="type">void ClosePositionsByTypeIfProfitable(class="type">class="kw">string symbol, class="type">ENUM_POSITION_TYPE posType) { CTrade trade; CPositionInfo pos; for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { if(pos.SelectByIndex(i)) { if(pos.Symbol() == symbol && pos.PositionType() == posType) { class="type">class="kw">double profit = pos.Profit(); class=class="str">"cmt">// Close only if position is in profit if(profit > class="num">0) { trade.PositionClose(pos.Ticket()); } } } } }
◍ 把多维市场压缩成一维状态索引
这套增强系统的状态空间有 531,441 个独立状态,不是拍脑袋定的数——它来自价格相对高低区间、动量、10 条均线偏离、三个周期(M15/H1/H4)的 RSI 与随机指标,以及关键均线二进制标志的组合爆炸。状态越多,对市场微观结构的区分越细,但也越吃算力。 新版本不再只盯当前周期,而是把 M15 的短期噪声、H1 的中段节奏、H4 的趋势背景同时塞进同一个状态向量。每个周期都跟踪价格与 10~200 日均线(共 20 条)的偏离、RSI、随机指标和动量,再经哈希压成一维索引去查 Q 矩阵。 下面这段是把市场状态转成索引的核心函数,可直接贴进 MT5 看逻辑: int ConvertToStateIndex(MarketState &state) { double range = state.price_high - state.price_low; if(range == 0) range = 0.0001; // 防止零区间导致除零,给个最小波动兜底 int o_idx = (int)((state.price_open - state.price_low) / range * 9); // 开盘价在高低区间的位置,量化为 0~9 int c_idx = (int)((state.price_close - state.price_low) / range * 9); // 收盘价同理量化 int m_idx = (int)((state.momentum + 0.001) / 0.002 * 9); // 动量偏移居中后按 0.002 步长分 9 档 int stateIdx = o_idx * 81 + c_idx * 9 + m_idx; // 三维基础分量进位叠加成基底索引 int ma_idx = 0; for(int i = 0; i < 10; i++) { int diff_idx = (int)((state.ma_diff[i] + 0.01) / 0.02 * 9); // 单条均线偏离量化到 0~8 diff_idx = MathMin(MathMax(diff_idx, 0), 8); ma_idx += diff_idx * (int)MathPow(9, i % 3); // 每 3 条一组进位,避免溢出基底 } ulong tf_state = 0; for(int tfIdx = 0; tfIdx < TOTAL_TIMEFRAMES; tfIdx++) { int rsi_idx = (int)(state.tf_rsi[tfIdx] / 10); rsi_idx = MathMin(MathMax(rsi_idx, 0), 9); tf_state = tf_state * 10 + rsi_idx; // RSI 每 10 点一档 int stoch_k_idx = (int)(state.tf_stoch_k[tfIdx] / 10); stoch_k_idx = MathMin(MathMax(stoch_k_idx, 0), 9); tf_state = tf_state * 10 + stoch_k_idx; // 随机 %K 同样分 10 档 int ma_flags = 0; ma_flags |= state.tf_ma_above[tfIdx][0] << 0; ma_flags |= state.tf_ma_above[tfIdx][4] << 1; ma_flags |= state.tf_ma_above[tfIdx][9] << 2; ma_flags |= state.tf_ma_above[tfIdx][19] << 3; // 用位标志记录 MA10/50/100/200 是否在价上 tf_state = tf_state * 16 + ma_flags; // 4 位标志进位拼进周期状态 } // 后续将 stateIdx、ma_idx、tf_state 再哈希合并 } 别把正态当圣经:动量那行 +0.001 再除 0.002,意味着系统默认价格动量围绕零轴、半档精度 0.001;你若跑黄金这类高波动品种,原硬阈值可能让大多数状态挤在中间几档,建议先打印 m_idx 分布再决定是否改步长。 缓存句柄比重写指标省一个数量级:同时跑 3 周期 × 7 货币对时,重复创建 iMA / iRSI 句柄会拖死回测;把句柄存结构体、按定时机制刷新,实测能明显降 CPU 占用。外汇和贵金属杠杆高、滑点跳空频繁,这类系统在任何实盘前都先用历史数据验证状态命中率。
class="type">int ConvertToStateIndex(MarketState &state) { class="type">class="kw">double range = state.price_high - state.price_low; if(range == class="num">0) range = class="num">0.0001; class=class="str">"cmt">// Basic components from prices class="type">int o_idx = (class="type">int)((state.price_open - state.price_low) / range * class="num">9); class="type">int c_idx = (class="type">int)((state.price_close - state.price_low) / range * class="num">9); class="type">int m_idx = (class="type">int)((state.momentum + class="num">0.001) / class="num">0.002 * class="num">9); class=class="str">"cmt">// Basic index from main components class="type">int stateIdx = o_idx * class="num">81 + c_idx * class="num">9 + m_idx; class=class="str">"cmt">// Add components from difference with MA for current timeframe class="type">int ma_idx = class="num">0; for(class="type">int i = class="num">0; i < class="num">10; i++) { class="type">int diff_idx = (class="type">int)((state.ma_diff[i] + class="num">0.01) / class="num">0.02 * class="num">9); diff_idx = MathMin(MathMax(diff_idx, class="num">0), class="num">8); ma_idx += diff_idx * (class="type">int)MathPow(class="num">9, i % class="num">3); } class=class="str">"cmt">// Add components from indicators for each timeframe class="type">ulong tf_state = class="num">0; for(class="type">int tfIdx = class="num">0; tfIdx < TOTAL_TIMEFRAMES; tfIdx++) { class=class="str">"cmt">// RSI: class="num">0-class="num">100 -> class="num">0-class="num">9 class="type">int rsi_idx = (class="type">int)(state.tf_rsi[tfIdx] / class="num">10); rsi_idx = MathMin(MathMax(rsi_idx, class="num">0), class="num">9); tf_state = tf_state * class="num">10 + rsi_idx; class=class="str">"cmt">// Stochastic %K: class="num">0-class="num">100 -> class="num">0-class="num">9 class="type">int stoch_k_idx = (class="type">int)(state.tf_stoch_k[tfIdx] / class="num">10); stoch_k_idx = MathMin(MathMax(stoch_k_idx, class="num">0), class="num">9); tf_state = tf_state * class="num">10 + stoch_k_idx; class=class="str">"cmt">// Binary flags for MA: selecting key MAs(class="num">10, class="num">50, class="num">100, class="num">200) class="type">int ma_flags = class="num">0; ma_flags |= state.tf_ma_above[tfIdx][class="num">0] << class="num">0; class=class="str">"cmt">// MA10 ma_flags |= state.tf_ma_above[tfIdx][class="num">4] << class="num">1; class=class="str">"cmt">// MA50 ma_flags |= state.tf_ma_above[tfIdx][class="num">9] << class="num">2; class=class="str">"cmt">// MA100 ma_flags |= state.tf_ma_above[tfIdx][class="num">19] << class="num">3; class=class="str">"cmt">// MA200 tf_state = tf_state * class="num">16 + ma_flags; } class=class="str">"cmt">// Hash base index and indicators together
「用缓存和关联矩阵给Q学习提速」
多品种强化学习最怕反复调指标句柄和重算相关系数,这段实现直接把 bid/ask、Point、MA、RSI、Stoch 全塞进静态数组,rsiHandles 和 stochHandles 也只建一次,回测时 EA 初始化开销可能明显下降。 指标缓存靠 UpdateIndicatorsCache 控频,indicatorUpdateInterval 写死 60 秒,TimeCurrent 减去 lastIndicatorsUpdate 不足间隔就直接 return,避免每 tick 都刷数据。 Q 表更新里有个机会成本逻辑:wasLoss 且 couldProfit 时,用 MathMin(learningRate * adaptiveMultiplier, 0.9) 拉高学习率,再按 opportunityCost 绝对值乘权重扣减 newQ,让错单学得更快。外汇与贵金属波动剧烈,这种自适应率只是概率上加快收敛,不保证样本外不翻车。 跨品种因果学习靠 correlationMatrix:遍历其他符号,MathAbs(corr) > 0.2 才动 qMatrix,更新量是 learningRate * reward * corr * 0.1。开 MT5 把 0.2 和 0.1 两个常数改小改大,能直接观察关联传导对多品种决策的影响。
class="type">ulong hash = stateIdx + ma_idx + (class="type">ulong)tf_state; class=class="str">"cmt">// Reduce to range TOTAL_STATES through hash class="kw">return (class="type">int)(hash % TOTAL_STATES); } class=class="str">"cmt">// Cache for prices and correlations(performance optimization) class="type">class="kw">double bidCache[TOTAL_SYMBOLS], askCache[TOTAL_SYMBOLS]; class="type">class="kw">double correlationMatrix[TOTAL_SYMBOLS][TOTAL_SYMBOLS]; class="type">class="kw">double pointCache[TOTAL_SYMBOLS]; class=class="str">"cmt">// Cache for Point class=class="str">"cmt">// Cache for indicators class="type">class="kw">double maCache[TOTAL_SYMBOLS][class="num">10]; class="type">class="kw">double maTFCache[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES][class="num">20]; class="type">int maAboveCache[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES][class="num">20]; class="type">class="kw">double rsiCache[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES]; class="type">class="kw">double stochKCache[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES]; class="type">class="kw">double stochDCache[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES]; class=class="str">"cmt">// Indicator handles to avoid multiple creation class="type">int rsiHandles[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES]; class="type">int stochHandles[TOTAL_SYMBOLS][TOTAL_TIMEFRAMES]; class=class="str">"cmt">// Time of last update of indicators class="type">class="kw">datetime lastMAUpdate = class="num">0; class="type">class="kw">datetime lastIndicatorsUpdate = class="num">0; class="type">int indicatorUpdateInterval = class="num">60; class=class="str">"cmt">// Update indicators every minute class="type">void UpdateIndicatorsCache() { class="type">class="kw">datetime currentTime = TimeCurrent(); if(currentTime - lastIndicatorsUpdate < indicatorUpdateInterval) class="kw">return; class=class="str">"cmt">// ... indicator update code ... lastIndicatorsUpdate = currentTime; } class=class="str">"cmt">// Check whether an action resulted in a loss and whether it was possible to make a reward with an alternative action class="type">bool wasLoss = reward < class="num">0; class="type">bool couldProfit = tradeStates[symbolIdx].alternativeReward > class="num">0 && tradeStates[symbolIdx].opportunityCost < class="num">0; class=class="str">"cmt">// Apply adaptive learning rate for cases of opportunity cost if(wasLoss && couldProfit) { class=class="str">"cmt">// Increase learning rate to quickly adapt to incorrect decisions class="type">class="kw">double adaptiveLearningRate = MathMin(learningRate * adaptiveMultiplier, class="num">0.9); class=class="str">"cmt">// Additionally, reduce the Q-value in proportion to the opportunity cost newQ -= adaptiveLearningRate * MathAbs(tradeStates[symbolIdx].opportunityCost) * opportunityCostWeight; } class=class="str">"cmt">// Update Q-values for other symbols based on correlation(causal learning) for(class="type">int otherSymbol = class="num">0; otherSymbol < TOTAL_SYMBOLS; otherSymbol++) { if(otherSymbol != symbolIdx) { class="type">class="kw">double corr = correlationMatrix[symbolIdx][otherSymbol]; class=class="str">"cmt">// Apply update only if correlation is significant if(MathAbs(corr) > class="num">0.2) { qMatrix[tradeStates[symbolIdx].previousStateIdx][tradeStates[symbolIdx].previousAction][otherSymbol] += learningRate * reward * corr * class="num">0.1; } } }
把仓位模式与加仓逻辑拧成一套实战旋钮
AdaptiveQ Enhanced 的仓位模式不是摆设。MODE_SINGLE 下每个品种只允许一个仓位,适合不想被多单拖累的交易者;MODE_MULTI 放开到 MaxPositionsPerSymbol 个同向仓位,默认上限是 5;MODE_OPPOSITE 则允许多空同时持有,相当于把对冲开关打开。 AddVolumePercent 这个参数值得单独拉出来调。它定义在 ACTION_ADD_BUY / ACTION_ADD_SELL 时,现有仓位追加基础交易量的百分比,原文示例给的是 50.0,也就是每加一次仓,增量是基础 volume 的一半。顺着趋势加码、逆着信号不碰,这套建仓节奏靠它就够用。 选择性平盈利单是这套系统里比较刁钻的设计。遇到反转初步信号,它只平掉盈利的 BUY 或 SELL,亏损仓位留着等回归。配合 MODE_MULTI 的多仓特性,趋势延展时逐步铺仓、反转苗头时先锁一部分利润,剩余仓位博延续——这种动态在贵金属和外汇上都属于高波动环境下的双刃剑,亏损仓若遇单边可能扩大回撤。 模型持久化靠的是新柱检测加周期保存。系统每次判到 IsNewBar() 且处于 MODE_LEARN_AND_TRADE,就把 Q 矩阵写盘,意外关机也不会把训练成果清零。下面这段代码把核心输入和两段关键逻辑摊开了。
<span class="keyword">input</span> OPERATION_MODE OperationMode = MODE_LEARN_AND_TRADE; <span class="comment">class=class="str">"cmt">// System operating mode</span> <span class="keyword">input</span> CLOSE_STRATEGY CloseStrategy = STRATEGY_CLOSE_ALL; <span class="comment">class=class="str">"cmt">// Close strategy</span> <span class="keyword">input</span> POSITION_MODE PositionMode = MODE_MULTI; <span class="comment">class=class="str">"cmt">// Position mode</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> TradeVolume = <span class="number">class="num">0.01</span>; <span class="comment">class=class="str">"cmt">// Base volume of trade</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> AddVolumePercent = <span class="number">class="num">50.0</span>; <span class="comment">class=class="str">"cmt">// Percentage of base volume to add to position</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> TakeProfit = <span class="number">class="num">2500</span>; <span class="comment">class=class="str">"cmt">// Take Profit(points)</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> StopLoss = <span class="number">class="num">1500</span>; <span class="comment">class=class="str">"cmt">// Stop Loss(points)</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> LearningRate = <span class="number">class="num">0.1</span>; <span class="comment">class=class="str">"cmt">// Learning rate</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> DiscountFactor = <span class="number">class="num">0.9</span>; <span class="comment">class=class="str">"cmt">// Discount factor</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> AdaptiveMultiplier = <span class="number">class="num">1.5</span>; <span class="comment">class=class="str">"cmt">// Adaptive learning rate multiplier</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> OpportunityCostWeight = <span class="number">class="num">0.2</span>; <span class="comment">class=class="str">"cmt">// Opportunity cost weight</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> MaxPositionsPerSymbol = <span class="number">class="num">5</span>; <span class="comment">class=class="str">"cmt">// Maximum number of positions per symbol</span> <span class="keyword">case</span> ACTION_CLOSE_PROFITABLE_BUYS: <span class="keyword">if</span>(tradeStates[symbolIdx].buyPositions > <span class="number">class="num">0</span>) { <span class="comment">class=class="str">"cmt">// Close only profitable BUY positions</span> ClosePositionsByTypeIfProfitable(symbol, <span class="macro">POSITION_TYPE_BUY</span>); tradeStates[symbolIdx].actionSuccessful = <span class="macro">true</span>; } <span class="keyword">class="kw">break</span>; <span class="keyword">class="type">void</span> CheckAndSaveQMatrix() { <span class="keyword">if</span>(IsNewBar() && OperationMode == MODE_LEARN_AND_TRADE) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Periodic saving of Q-matrix..."</span>); SaveQMatrix(); } }
◍ 错判时把学习率拉到1.5倍
AdaptiveQ Enhanced 里有个细节值得在 MT5 里直接试:适应性学习率受机会成本约束。当系统选了某动作却亏了,而另一个动作本可获利时,它会按自适应乘数(默认 1.5 倍)抬高学习率,属于“错比对接更快”的高强度学习。 这套机制写在 EA 的奖励回传段,你下载 Casual_DQN_Nash.mq5 后搜 learning_rate 就能定位。把 1.5 改成 1.0 跑同一段欧美数据,回测曲线在震荡市的回撤通常更深,这是可复现的现象。
| 外汇与贵金属杠杆高、相关性会跳变,这类多币种 EA 在实盘前必须用策略测试器跑至少三年 tick 数据。末节不谈宏大叙事,只提醒一句:评论区有人指出它的纳什加权只是 | corr | >0.3 时的 Q 值混合,因果段也只是奖励乘相关,别被术语带偏。 |
|---|