随机优化和最优控制示例·进阶篇
(2/3)· 从 SMOC EA 的 OptimalControl 函数拆解,看模型预测控制如何落地中期趋势交易
◍ 用回撤和连亏给仓位踩刹车
AdjustLotSizeForDrawdown 这套逻辑只干一件事:让仓位随账户状态收缩,而不是固定手数硬扛。它盯两个硬指标——当前账户的回撤幅度,以及连续亏损的次数,两者任一越线就动手数。 内部用静态变量 consecutiveLosses 记连亏笔数,maxBalance 存历史最高余额;每次进来先抓 currentBalance 和 currentEquity。回撤不是拿余额算,而是用 (maxBalance - currentEquity) / maxBalance 得出百分比落差,这样浮亏没平仓也会被计入风险。 基础手数交给 CalculateDynamicLotSize() 先算个底(本段未给实现),之后才做两层裁剪:回撤破 10% 就降仓,连亏超 3 次也降仓。外汇与贵金属杠杆高,这种双条件收缩能压住尾部风险,但是否触发取决于实盘波动,不保证避免亏损。 要在 MT5 里验证,先把这两个阈值(10%、3次)打到全局常量,跑一段 EURUSD 的回测看最大回撤是否比固定 0.1 手更浅,再决定搬进实盘。
按回撤和连败动态砍仓的手数逻辑
这套手数控制先把最大余额盯住:只要当前余额冲破历史峰值就刷新 maxBalance,它是后续回撤的分母。回撤率用 (maxBalance - 当前权益) / maxBalance 算,属于账户级浮亏参考,不是单笔止损比例。 基础手数由独立的 CalculateDynamicLotSize() 给出,之后才进风控判断。若回撤 > 10%,直接把手数减半;若连续亏损 > 3 次(未达回撤线时),手数乘 0.75,即砍掉 25%。两者都不触发就返基础值。 外汇与贵金属杠杆高、跳空频繁,10% 回撤在镑系或黄金行情里可能一两日就到。把这段代码丢进 MT5 的 EA 回测,调 0.1 这个阈值到 0.07 或 0.15,能看到权益曲线尾部斜率明显变化。 别把正态当圣经 连亏 3 次就降仓是经验值不是统计必然,震荡市里 5 连亏也常见,真要硬扛可能错过反扑段。
if (currentBalance > maxBalance) maxBalance = currentBalance; class="type">class="kw">double drawdown = (maxBalance - currentEquity) / maxBalance; class="type">class="kw">double baseLotSize = CalculateDynamicLotSize(); if (drawdown > class="num">0.1 ) class=class="str">"cmt">// class="num">10% drawdown { class="kw">return baseLotSize * class="num">0.5 ; class=class="str">"cmt">// Reduce lot size by half } else if (consecutiveLosses > class="num">3 ) { class="kw">return baseLotSize * class="num">0.75 ; class=class="str">"cmt">// Reduce lot size by class="num">25% }
「回撤控仓与波动自适应止损的改造空间」
原版的风险管理只做了固定回撤减仓,实战里可以把梯度做细:按回撤深度分档降手数,而不是一刀切。账户从深回撤爬出来时,再写一段恢复逻辑逐步加仓,能避免刚回血就被重仓再坑一次。 连胜跟踪也值得接进来——连续获利时市场倾向延续的概率偏高,可适度放大手数;同时务必给 EA 挂一个硬上限最大手数,外汇和贵金属杠杆高,这句不是建议而是保命。 指标层这块 EA 用了四条线:SMA(50)、Parabolic SAR(0.02,0.2)、RSI(14)、ATR(14)。波动由市价 Close 计算,决策依赖它们交叉。 动态止损止盈由两个函数算出来,核心是把 ATR 当波动尺。SL 多头取价减 ATR*multi*1.2、空头加 ATR*multi*0.8;TP 多头加 ATR*multi*1.8、空头减 ATR*multi*2.2。multi 是你自己调的乘数,改它比改周期更直接影响风控松紧。 别把正态当圣经:ATR 只是历史波动快照,重大数据行情里可能瞬间失效,开 MT5 把 multi 从 1 调到 2 跑一周回测就能看见 SL 被扫频率的变化。
class=class="str">"cmt">// Initialize indicator handles smaHandle = iMA( Symbol() , PERIOD_CURRENT , class="num">50 , class="num">0 , MODE_SMA , PRICE_CLOSE ); psarHandle = iSAR( Symbol() , PERIOD_CURRENT , class="num">0.02 , class="num">0.2 ); rsiHandle = iRSI( Symbol() , PERIOD_CURRENT , class="num">14 , PRICE_CLOSE ); atrHandle = iATR( Symbol() , PERIOD_CURRENT , class="num">14 ); class="type">class="kw">double CalculateDynamicSL( class="type">class="kw">double price, class="type">int decision) { class="type">class="kw">double atrValue[]; if ( CopyBuffer(atrHandle, class="num">0 , class="num">0 , class="num">1 , atrValue) <= class="num">0 ) { LogMessage( StringFormat( "Error getting ATR values: %d" , GetLastError())); class="kw">return class="num">0.0 ; } class="type">class="kw">double volatility = atrValue[ class="num">0 ]; class="type">class="kw">double dynamicSL = (decision == class="num">1 ) ? price - (volatility * multi * class="num">1.2 ) : price + (volatility * multi * class="num">0.8 ); class="kw">return NormalizeDouble(dynamicSL, _Digits ); } class="type">class="kw">double CalculateDynamicTP( class="type">class="kw">double price, class="type">int decision) { class="type">class="kw">double atrValue[]; if ( CopyBuffer(atrHandle, class="num">0 , class="num">0 , class="num">1 , atrValue) <= class="num">0 ) { LogMessage( StringFormat( "Error getting ATR values: %d" , GetLastError())); class="kw">return class="num">0.0 ; } class="type">class="kw">double volatility = atrValue[ class="num">0 ]; class="type">class="kw">double dynamicTP = (decision == class="num">1 ) ? price + (volatility * multi * class="num">1.8 ) : price - (volatility * multi * class="num">2.2 ); class="kw">return NormalizeDouble(dynamicTP, _Digits ); }
◍ 波动自适应止损止盈的两个核心函数
这两个函数都依赖 ATR 指标刻画市场波动,再结合当前价格与交易方向(1 为买入,-1 为卖出)去反推动态 SL 与 TP。也就是说,同样一笔单子,ATR 放大时止损和获利都会同步拉开,波动收敛时则收紧,避免用固定点数硬扛不同行情。 函数输入只有三个关键量:price 是标的实时报价,decision 用整数标定方向,multi 作为全局乘数控制波动影响的权重。调大 multi,止损止盈对 ATR 的敏感度就更高,回撤空间与盈利目标会一起膨胀。 下方代码片段列出了两个函数的签名骨架,实际计算逻辑就是拿 ATR 值乘 multi 后按 decision 符号偏移 price。外汇与贵金属杠杆高、跳空频繁,这类动态位数在极端波动下仍可能不足以覆盖滑点,仅作概率上的风控辅助。
class="type">class="kw">double CalculateDynamicSL( class="type">class="kw">double price, class="type">int decision) class="type">class="kw">double CalculateDynamicTP( class="type">class="kw">double price, class="type">int decision)
用ATR系数拉开止损止盈的非对称距离
动态止损止盈的核心是把 ATR 当期波动值乘上可调倍数,再按多空方向挂到市价另一侧。买入时 SL 放在 price 下方,卖出时放在 price 上方;TP 则反向,买上卖下。 原文作者故意把系数做成非对称:买 SL 用 1.2、卖 SL 用 0.8,买 TP 用 1.8、卖 TP 用 2.2,目的只是演示差异,实盘你完全可以按品种波动习性改。外汇与贵金属杠杆高,ATR 跳变时这类动态距离可能瞬间拉大仓位回撤,参数须先在 MT5 策略测试器跑一遍。 下面这段 MQL5 展示了从 atrHandle 取最新值再到归一化返回的过程,注意 decision==1 代表买入信号: [CODE] <span class="keyword">double</span> atrValue[]; <span class="keyword">if</span> ( <span class="functions">CopyBuffer</span> (atrHandle, <span class="number">0</span> , <span class="number">0</span> , <span class="number">1</span> , atrValue) <= <span class="number">0</span> ) { LogMessage( <span class="functions">StringFormat</span> ( <span class="string">"Error getting ATR values: %d"</span> , <span class="functions">GetLastError</span> ())); <span class="keyword">return</span> <span class="number">0.0</span> ; } <span class="keyword">double</span> volatility = atrValue[ <span class="number">0</span> ]; <span class="keyword">double</span> dynamicSL = (decision == <span class="number">1</span> ) ? price - (volatility * multi * <span class="number">1.2</span> ) : price + (volatility * multi * <span class="number">0.8</span> ); <span class="keyword">return</span> <span class="functions">NormalizeDouble</span> (dynamicSL, <span class="predefines">_Digits</span> ); <span class="keyword">double</span> dynamicTP = (decision == <span class="number">1</span> ) ? price + (volatility * multi * <span class="number">1.8</span> ) : price - (volatility * multi * <span class="number">2.2</span> ); [/CODE] 逐行看:atrValue 数组接 CopyBuffer 取 0 号柱的 ATR;若返回值 ≤0 打日志并返 0.0 避免脏数据。volatility 就是 atrValue[0]。dynamicSL 那行用三元表达式,买则价减波动乘 multi 乘 1.2,卖则价加波动乘 multi 乘 0.8。NormalizeDouble 按 _Digits 对齐报价小数位。dynamicTP 同理,买加 1.8 倍、卖减 2.2 倍——把 multi 设成 2 时,欧美 15M 上 ATR 若 12 点,买 TP 就约距 43 点。
<span class="keyword">class="type">class="kw">double</span> atrValue[]; <span class="keyword">if</span> ( <span class="functions">CopyBuffer</span> (atrHandle, <span class="number">class="num">0</span> , <span class="number">class="num">0</span> , <span class="number">class="num">1</span> , atrValue) <= <span class="number">class="num">0</span> ) { LogMessage( <span class="functions">StringFormat</span> ( <span class="class="type">class="kw">string">"Error getting ATR values: %d"</span> , <span class="functions">GetLastError</span> ())); <span class="keyword">class="kw">return</span> <span class="number">class="num">0.0</span> ; } <span class="keyword">class="type">class="kw">double</span> volatility = atrValue[ <span class="number">class="num">0</span> ]; <span class="keyword">class="type">class="kw">double</span> dynamicSL = (decision == <span class="number">class="num">1</span> ) ? price - (volatility * multi * <span class="number">class="num">1.2</span> ) : price + (volatility * multi * <span class="number">class="num">0.8</span> ); <span class="keyword">class="kw">return</span> <span class="functions">NormalizeDouble</span> (dynamicSL, <span class="predefines">_Digits</span> ); <span class="keyword">class="type">class="kw">double</span> dynamicTP = (decision == <span class="number">class="num">1</span> ) ? price + (volatility * multi * <span class="number">class="num">1.8</span> ) : price - (volatility * multi * <span class="number">class="num">2.2</span> );
「回撤与连亏双触发下的手数收缩逻辑」
把止损止盈挂单和仓位规模绑成一套自适应系统,核心是用 ATR 做波动对齐、用账户回撤做仓位收缩。前几节聊了 SL/TP 随波动率拉开不对称距离,这一节落点在「钱怎么下」:连续亏损或净值回撤时,算法交易里的手数应当自动变小,而不是靠人手动改。 AdjustLotSizeForDrawdown() 用两个静态变量盯住「连续亏损次数」和「历史最大余额」。当 (maxBalance - 当前净值) / maxBalance 算出的回撤大于 10%,基础手数直接减半;若连续亏损超过 3 次,手数乘 0.75 收缩 25%。基础手数来自 CalculateDynamicLotSize(),未触发条件时原样返回。 CalculateDynamicLotSize() 取账户余额与净值中的较小值做保守基底,固定每笔风险 1%。它假设 1 点 = 10 ticks,用 SYMBOL_TRADE_TICK_VALUE / TICK_SIZE * 10 算点值,再以「风险金额 ÷ (50 点 × 点值)」得出手数并归一化到两位小数。这两段函数组合后,能在外汇与贵金属这种高杠杆、高波动市场里,把账户下行速度按概率压住——但市场仍可能单边跳空,本金有快速亏损风险。 代码里 stopLossPips 写死 50 点、riskPercentage 写死 0.01,实盘前建议改成输入参数。打开 MT5 把下面两段贴进 EA,把 50 点止损换成你策略的真实止损,跑一次回测看净值曲线的最大回撤是否比固定手数更平。
class=class="str">"cmt">// Function to adjust the lot size based on drawdown class="type">class="kw">double AdjustLotSizeForDrawdown() { class=class="str">"cmt">// Static variables to keep track of consecutive losses and maximum balance class="kw">static class="type">int consecutiveLosses = class="num">0; class="kw">static class="type">class="kw">double maxBalance = class="num">0; class=class="str">"cmt">// Get current account balance and equity class="type">class="kw">double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); class="type">class="kw">double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">// Update the maximum balance if current balance is higher if(currentBalance > maxBalance) maxBalance = currentBalance; class=class="str">"cmt">// Calculate the current drawdown as a percentage class="type">class="kw">double drawdown = (maxBalance - currentEquity) / maxBalance; class=class="str">"cmt">// Calculate the base lot size using a separate function class="type">class="kw">double baseLotSize = CalculateDynamicLotSize(); class=class="str">"cmt">// If drawdown is greater than class="num">10%, reduce lot size by half if(drawdown > class="num">0.1) class=class="str">"cmt">// class="num">10% drawdown { class="kw">return baseLotSize * class="num">0.5; class=class="str">"cmt">// Reduce lot size by half } else if(consecutiveLosses > class="num">3) { class="kw">return baseLotSize * class="num">0.75; class=class="str">"cmt">// Reduce lot size by class="num">25% after class="num">3 consecutive losses } class=class="str">"cmt">// Return the base lot size if no adjustments are needed class="kw">return baseLotSize; } class=class="str">"cmt">// Function to dynamically calculate the lot size class="type">class="kw">double CalculateDynamicLotSize() { class=class="str">"cmt">// Get current account balance and equity class="type">class="kw">double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); class="type">class="kw">double equity = AccountInfoDouble(ACCOUNT_EQUITY); class="type">class="kw">double riskPercentage = class="num">0.01; class=class="str">"cmt">// class="num">1% risk per trade class=class="str">"cmt">// Use the lower value between balance and equity to be conservative class="type">class="kw">double baseAmount = MathMin(accountBalance, equity); class=class="str">"cmt">// Calculate the value of a pip for the current symbol class="type">class="kw">double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); class="type">class="kw">double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); class="type">class="kw">double pipValue = (tickValue / tickSize) * class="num">10; class=class="str">"cmt">// Assuming a pip is class="num">10 ticks class=class="str">"cmt">// Calculate lot size based on desired risk class="type">class="kw">double riskAmount = baseAmount * riskPercentage; class="type">class="kw">double stopLossPips = class="num">50; class=class="str">"cmt">// Adjust according to your strategy class="type">class="kw">double lotSize1 = NormalizeDouble(riskAmount / (stopLossPips * pipValue), class="num">2);