算法交易中的风险管理器·进阶篇
(2/3)·从手动风控到算法封装,解决近距离止损与滑点吞噬收益的隐性漏洞
遍历持仓把每单风险算清楚
做批量风控前,得先把当前账户里每一张持仓的关键字段抓出来。下面这段逻辑用 r_position.SelectByIndex(i) 按序号定位,再比对 Symbol() 是否等于当前图表品种,只处理同品种仓位,避免跨品种误算。 抓到目标仓位后,止损价、开仓价、浮动盈利、手数、票据号、持仓方向分别存进变量。买仓按 Bid 平仓、卖仓按 Ask 平仓,这个细节不写对,后续测算的平仓点就会偏一个 spread。 风险额不是拍脑袋。用 NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0) 把止损距离换算成整数点数,再乘 TickValueLoss() 得到的单点价值和手数,得出 potentionLossOnDeal——也就是触止损时这单可能亏多少。外汇和贵金属杠杆高,这套数算错一格点,账户回撤可能就超你容忍线。 让小布替你跑这套 把 lot_cost 和 potentionLossOnDeal 打进 Print(),挂 EA 里跑一轮,看和你手动算的止损风险是否一致,不一致就查 Point 和合约大小。
class="type">class="kw">double lot_cost = r_symbol.TickValueLoss(); class=class="str">"cmt">// 获取当前品种的每点价值(以账户币种计) class="type">bool ticket_sc = class="num">0; class=class="str">"cmt">// 标记平仓是否成功的布尔变量 r_position.SelectByIndex(i) Symbl = r_position.Symbol(); class=class="str">"cmt">// 取出该持仓的交易品种 if(Symbl==Symbol()) class=class="str">"cmt">// 判断是否为当前图表品种 PriceStopLoss = r_position.StopLoss(); class=class="str">"cmt">// 记录其止损价 PriceOpen = r_position.PriceOpen(); class=class="str">"cmt">// 记录开仓价 ProfitCur = r_position.Profit(); class=class="str">"cmt">// 记录当前浮动盈亏 LotsOrder = r_position.Volume(); class=class="str">"cmt">// 记录持仓手数 Ticket = r_position.Ticket(); class="type">int dir = r_position.Type(); class=class="str">"cmt">// 判定持仓类型(买或卖) if(dir == POSITION_TYPE_BUY) class=class="str">"cmt">// 若是买入持仓 { PriceClose = r_symbol.Bid(); class=class="str">"cmt">// 以买价平仓 } if(dir == POSITION_TYPE_SELL) class=class="str">"cmt">// 若是卖出持仓 { PriceClose = r_symbol.Ask(); class=class="str">"cmt">// 以卖价平仓 } class="type">int curr_sl_ord = (class="type">int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),class="num">0); class=class="str">"cmt">// 将止损距离规范化为整数点数 class="type">class="kw">double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,class="num">2); class=class="str">"cmt">// 计算触及止损时的潜在亏损金额
「滑点超标时如何强制平仓」
风险管理的实战里,光设止损不够,还得防流动性吃掉的滑点。下面这段逻辑判断:当潜在亏损超过单笔风险阈值,且持仓已经浮亏、又挂了止损,就直接市价平掉。
核心判定行 potentionLossOnDeal > NormalizeDouble(riskPerDeal*slippfits, 0) 里,riskPerDeal 是计划单笔风险,slippfits 是可容忍滑点系数,NormalizeDouble(..., 0) 把结果四舍五入到整数金额,避免浮点误差误触。
SlippageCheck 函数开头先 r_symbol.Refresh() 拉最新报价,再声明一组局部变量:收盘价、止损价、开仓价、手数、当前利润、订单号、品种名。这些在后续循环里会被逐仓填充,任一为 0 通常意味着数据未就绪,函数会跳过该仓。
外汇与贵金属杠杆高,滑点可能在数据行情瞬间扩大数倍,这类硬平机制能截断超出预期的小概率亏损,但无法消除跳空风险。
potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,class="num">0) && class=class="str">"cmt">// if the resulting stop exceeds risk per trade given the threshold value ProfitCur<class="num">0 && class=class="str">"cmt">// and the order is at a loss PriceStopLoss != class="num">0 class=class="str">"cmt">// if stop loss is not set, don&class="macro">#x27;t touch ) ticket_sc = r_trade.PositionClose(Ticket); class=class="str">"cmt">// close order class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SlippageCheck | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool RiskManagerAlgo::SlippageCheck(class="type">void) class="kw">override { r_symbol.Refresh(); class=class="str">"cmt">// update symbol data class="type">class="kw">double PriceClose = class="num">0, class=class="str">"cmt">// close price for the order PriceStopLoss = class="num">0, class=class="str">"cmt">// stop loss price for the order PriceOpen = class="num">0, class=class="str">"cmt">// open price for the order LotsOrder = class="num">0, class=class="str">"cmt">// order lot volume ProfitCur = class="num">0; class=class="str">"cmt">// current order profit class="type">ulong Ticket = class="num">0; class=class="str">"cmt">// order ticket class="type">class="kw">string Symbl; class=class="str">"cmt">// symbol
◍ 遍历持仓并抓取平仓要素
在 MT5 的 EA 逻辑里,先拿到当前品种的 TickValueLoss() 才能算清每点价值,否则后续盈亏判断只是空转。用一个 bool 变量记录平仓是否成功,比直接依赖 OrderSend 返回值更方便做循环内的状态追踪。 下面这段从 PositionsTotal() 倒序扫持仓,SelectByIndex(i) 选中后只处理 Symbol() 与当前图表品种一致的单子,避免跨品种误平。对外汇与贵金属来说,点差和滑点在高危时段可能瞬间扩大,只平自己品种是基本防守。 选中后把止损、开仓价、浮动盈利、手数、票据号、持仓方向全部暂存到局部变量。买仓按 Bid 平、卖仓按 Ask 平,这个细节若写反,回测和实盘都会出现隐性点差损耗。 代码逐行拆解: double lot_cost = r_symbol.TickValueLoss(); // 取当前品种每tick亏损金额 bool ticket_sc = 0; // 平仓成功标志,初始为假 for(int i = PositionsTotal(); i>=0; i--) // 从持仓总数倒序循环到0 { if(r_position.SelectByIndex(i)) // 按序号选中第i个持仓 { Symbl = r_position.Symbol(); // 读出该持仓交易品种 if(Symbl==Symbol()) // 仅当品种与当前图表一致才继续 { PriceStopLoss = r_position.StopLoss(); // 记录止损价 PriceOpen = r_position.PriceOpen(); // 记录开仓价 ProfitCur = r_position.Profit(); // 记录当前浮动盈亏 LotsOrder = r_position.Volume(); // 记录手数 Ticket = r_position.Ticket(); // 记录持仓票据号 int dir = r_position.Type(); // 判断持仓方向 if(dir == POSITION_TYPE_BUY) // 若为多单 { PriceClose = r_symbol.Bid(); // 多单以买价平仓 } if(dir == POSITION_TYPE_SELL) // 若为空单 { PriceClose = r_symbol.Ask(); // 空单以卖价平仓 }
| if(dir == POSITION_TYPE_BUY | dir == POSITION_TYPE_SELL) // 确属双向持仓之一 |
|---|
{
class="type">class="kw">double lot_cost = r_symbol.TickValueLoss(); class=class="str">"cmt">// get tick value class="type">bool ticket_sc = class="num">0; class=class="str">"cmt">// variable for successful closing for(class="type">int i = PositionsTotal(); i>=class="num">0; i--) class=class="str">"cmt">// start loop through orders { if(r_position.SelectByIndex(i)) { Symbl = r_position.Symbol(); class=class="str">"cmt">// get the symbol if(Symbl==Symbol()) class=class="str">"cmt">// check if it&class="macro">#x27;s the right symbol { PriceStopLoss = r_position.StopLoss(); class=class="str">"cmt">// remember its stop loss PriceOpen = r_position.PriceOpen(); class=class="str">"cmt">// remember its open price ProfitCur = r_position.Profit(); class=class="str">"cmt">// remember financial result LotsOrder = r_position.Volume(); class=class="str">"cmt">// remember order lot volume Ticket = r_position.Ticket(); class="type">int dir = r_position.Type(); class=class="str">"cmt">// define order type if(dir == POSITION_TYPE_BUY) class=class="str">"cmt">// if it is Buy { PriceClose = r_symbol.Bid(); class=class="str">"cmt">// close at Bid } if(dir == POSITION_TYPE_SELL) class=class="str">"cmt">// if it is Sell { PriceClose = r_symbol.Ask(); class=class="str">"cmt">// close at Ask } if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL)
用止损点数卡死单笔风险
EA 在持仓监控里先算止损距离:把(止损价-开仓价)的绝对值除以品种 Point,再 NormalizeDouble 到整数点,得到 curr_sl_ord。这一步直接把价格差翻译成「离场还要扛多少点」,是后面风控的分母。 潜在亏损额用 curr_sl_ord × 每点成本 lot_cost × 手数 LotsOrder 算出,保留两位小数。假设 EURUSD 的 lot_cost 约 10 美元/标准手每点,0.1 手扛 50 点止损,potentionLossOnDeal 就是 50×1×0.1=5 美元,数字可在 MT5 策略测试器里逐项 Print 核对。 只有当三个条件同时成立才强平:算出的潜在亏损大于「单笔风险上限 riskPerDeal × 滑点系数 slippfits」、当前浮亏 ProfitCur<0、且止损价非零(没设止损就不动它)。外汇与贵金属杠杆高,这种硬砍仓逻辑能避免亏损单在极端波动里吃掉账户缓冲,但也可能因滑点导致 Close 失败,需监听 GetLastError 做日志。
class="type">int curr_sl_ord = (class="type">int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),class="num">0); class=class="str">"cmt">// check the resulting stop class="type">class="kw">double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,class="num">2); class=class="str">"cmt">// calculate risk upon reaching the stop level if( potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,class="num">0) && class=class="str">"cmt">// if the resulting stop exceeds risk per trade given the threshold value ProfitCur<class="num">0 && class=class="str">"cmt">// and the order is at a loss PriceStopLoss != class="num">0 class=class="str">"cmt">// if stop loss is not set, don&class="macro">#x27;t touch ) { ticket_sc = r_trade.PositionClose(Ticket); class=class="str">"cmt">// close order Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal)); class=class="str">"cmt">// Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits)); class=class="str">"cmt">// Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal)); class=class="str">"cmt">// Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder)); class=class="str">"cmt">// Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord)); class=class="str">"cmt">// if(!ticket_sc) { Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); class=class="str">"cmt">// output to log } else
「滑点导致的订单关闭日志怎么打」
在 MQL5 的订单处理循环里,当某张订单因滑点被关闭,需要在日志里留下可追溯记录,否则复盘时根本分不清是正常平仓还是异常滑点砍单。 下面这段代码片段嵌在订单遍历逻辑中,作用是把发生滑点关闭的订单号输出到专家日志: Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // output to log continue; 其中 __FUNCTION__ 自动返回当前函数名,IntegerToString(ticket_sc) 把订单 ticket 转成字符串拼接,最后 continue 跳过后续处理直接进入下一轮循环。 在 MT5 按 F8 打开策略测试器跑一遍含滑点环境的回测,控制台里搜 'closed by slippage' 就能看到具体哪些单子受影响了,外汇和贵金属点差跳变频繁,这类滑点风险需重点核查。
{
Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); class=class="str">"cmt">// output to log
}
class="kw">continue;
}
}
}
}
class="kw">return(ticket_sc);
}
class=class="str">"cmt">//+------------------------------------------------------------------+◍ 开仓前的点差闸门怎么设
EA 在发单之前先跑一遍点差校验,逻辑并不复杂:取当前品种点差,和用户传进来的止损点数乘一个系数做比较。系数通常设在 2 以上,也就是点差一旦超过止损距离的一半,这单就先按住不开。 具体实现里用 CSymbolInfo 的 Spread() 拿到实时点差,塞进 int 型变量,再和 intSL*spreadfits 比大小。超出就返回 false,仓位被拦到下一个 tick 再来评估。外汇和贵金属杠杆高,点差吃掉短止损的概率本来就大,这种拦截能少挨几刀。 别把系数拧得太死 系数若卡在 1 附近,EA 会频繁因点差略超止损而空转,日志刷得凶却不成交。实盘里把 spreadfits 调到 2~3,要么等点差缩回来,要么直接放弃过窄止损的入场,比硬吃亏损更划算。 下面这段是核心判断,开 MT5 把 spreadfits 当成输入参数暴露出来,跑两周看拦截次数和成交质量的取舍。
class="type">bool SpreadAllowed = true; class="type">int SpreadCurrent = r_symbol.Spread(); if(SpreadCurrent>intSL*spreadfits) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SpreadMonitor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool RiskManagerAlgo::SpreadMonitor(class="type">int intSL) { class=class="str">"cmt">//--- spread control class="type">bool SpreadAllowed = true; class=class="str">"cmt">// allow spread trading and check ratio further class="type">int SpreadCurrent = r_symbol.Spread(); class=class="str">"cmt">// current spread values if(SpreadCurrent>intSL*spreadfits) class=class="str">"cmt">// if the current spread is greater than the stop and the coefficient { SpreadAllowed = false; class=class="str">"cmt">// prohibit trading Print(__FUNCTION__+IntegerToString(__LINE__)+ ". 点差太大了!Spread:"+ IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));class=class="str">"cmt">// notify } class="kw">return SpreadAllowed; class=class="str">"cmt">// class="kw">return result } class=class="str">"cmt">//+------------------------------------------------------------------+