手动交易的风险管理·综合运用
(3/3)· 用 21 节拆解一个 2019 年实战沉淀的 MQL5 风控类,把情绪化爆仓挡在代码外
◍ 按周拉取成交并逐笔剥离盈亏结构
做周维度统计前,先得把指定时间窗内的历史成交一次性选进内存。下面这段逻辑用 HistorySelect 框住从本周起点到当前本地时间的区间,再靠 HistoryDealsTotal 拿到命中条数,外汇和贵金属品种在此区间内的持仓过夜成本与点差损耗都可能显著改变净收益,属高风险品类,统计时不能只看表面利润。 循环里对每笔成交调用 HistoryDealGetTicket 取编号,再用 HistoryDealGetDouble 分别抓 DEAL_PROFIT、DEAL_COMMISSION、DEAL_SWAP,用 HistoryDealGetInteger 取 DEAL_TYPE。只保留 DEAL_TYPE_BUY 与 DEAL_TYPE_SELL 的真实买卖成交,过滤掉余额操作类记录,才能算出可对照的实战胜率。 判断 profit>0 即记为盈利笔数,这一步没有含手续费与 swap 的净口径修正,若直接拿去评估 EA 实盘表现会偏乐观,建议在统计前先把 commis 与 swap 从 profit 中扣掉再判正负。
HistorySelect(StructToTime(start_week),StructToTime(local)); class=class="str">"cmt">// select the required history class=class="str">"cmt">//--- check total = HistoryDealsTotal(); class=class="str">"cmt">// number number of selected deals ticket = class="num">0; class=class="str">"cmt">// order number profit = class="num">0; class=class="str">"cmt">// order profit commis = class="num">0; class=class="str">"cmt">// order commission swap = class="num">0; class=class="str">"cmt">// order swap class=class="str">"cmt">//--- for all deals for(class="type">uint i=class="num">0; i<total; i++) class=class="str">"cmt">// loop through all selected orders { class=class="str">"cmt">//--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>class="num">0) class=class="str">"cmt">// get the number of each in order { class=class="str">"cmt">//--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); class=class="str">"cmt">// get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); class=class="str">"cmt">// get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); class=class="str">"cmt">// get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); class=class="str">"cmt">// get data on operation type if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) class=class="str">"cmt">// if the deal is form a trading operatoin { if(profit>class="num">0) class=class="str">"cmt">// if financial result of current order is greater than class="num">0, {
「切到月维度重算逐笔盈亏」
周维度统计跑完后,代码紧接着把视野切到月。先用 HistorySelect 按 StructToTime(start_month) 到本地时间拉取整月成交,这一步决定了后面循环的样本边界,选错时间结构就会把跨月持仓算漏。 total = HistoryDealsTotal() 拿到选中成交笔数,ticket、profit、commis、swap 四个变量清零,准备在 for(uint i=0; i<total; i++) 里逐笔重写。注意这里用的是 Deal 而非 Order,已平仓且已结算的才算数。 循环内先 if((ticket=HistoryDealGetTicket(i))>0) 取出逐笔票号,再分别用 HistoryDealGetDouble 抓 DEAL_PROFIT、DEAL_COMMISSION、DEAL_SWAP。外汇与贵金属品种隔夜 swap 可能吃掉小周期策略大半利润,这部分必须单列,不能并到 profit 里糊弄。 打开 MT5 按这段代码改个 Print,把月度 commis+swap 占 profit 比打出来,你可能会发现某些金属品种持仓超过 3 天就倾向亏损。
WeekorderProfit += profit; class=class="str">"cmt">// add to profit } else { WeekorderLoss += MathAbs(profit); class=class="str">"cmt">// if loss, add up } } } } class=class="str">"cmt">//--- now select data by --==MONTH==-- HistorySelect(StructToTime(start_month),StructToTime(local)); class=class="str">"cmt">// select the required history class=class="str">"cmt">//--- check total = HistoryDealsTotal(); class=class="str">"cmt">// number number of selected deals ticket = class="num">0; class=class="str">"cmt">// order number profit = class="num">0; class=class="str">"cmt">// order profit commis = class="num">0; class=class="str">"cmt">// order commission swap = class="num">0; class=class="str">"cmt">// order swap class=class="str">"cmt">//--- for all deals for(class="type">uint i=class="num">0; i<total; i++) class=class="str">"cmt">// loop through all selected orders { class=class="str">"cmt">//--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>class="num">0) class=class="str">"cmt">// get the number of each in order { class=class="str">"cmt">//--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); class=class="str">"cmt">// get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); class=class="str">"cmt">// get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); class=class="str">"cmt">// get swap data
逐笔拆出当月持仓成本与盈亏
在 MT5 的历史成交回放里,先把每笔 ticket 的操作类型捞出来,再决定这笔该归到哪一类统计池。下面这段逻辑直接按成交属性分流: swap 和 commis 无条件累加进月度隔夜费与手续费,买卖成交才进盈亏统计。 type = HistoryDealGetInteger(ticket, DEAL_TYPE); // 取该笔成交类型 MonthOrderSwap += swap; // 累加隔夜利息 MonthOrderCommis += commis; // 累加佣金
| if(type == DEAL_TYPE_BUY | type == DEAL_TYPE_SELL) // 仅处理买卖类成交 |
|---|
{ if(profit > 0) // 该笔净盈利为正 { MonthorderProfit += profit; // 盈利累加 } else { MonthorderLoss += MathAbs(profit); // 亏损取绝对值累加 } } 盈利和亏损分开建池很关键:后续算盈亏比时直接用 MonthorderProfit / MonthorderLoss,不用再遍历。外汇与贵金属杠杆高,swap 在周三、周五会跳点,月度统计若忽略这几天的异常值,成本模型会偏轻。 开 MT5 按 F4 把这段塞进历史扫描循环,跑一个月 tick 级回测,重点看 MonthOrderSwap 占净值回撤的比例,多数账户超 15% 才暴露费用盲区。
type = HistoryDealGetInteger(ticket, DEAL_TYPE); class=class="str">"cmt">// get data on operation type MonthOrderSwap += swap; class=class="str">"cmt">// sum up swaps MonthOrderCommis += commis; class=class="str">"cmt">// sum up commissions if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) class=class="str">"cmt">// if the deal is form a trading operatoin { if(profit>class="num">0) class=class="str">"cmt">// if financial result of current order is greater than class="num">0, { MonthorderProfit += profit; class=class="str">"cmt">// add to profit } else { MonthorderLoss += MathAbs(profit); class=class="str">"cmt">// if loss, sum up } }
◍ 用周期切换事件重置交易限额
在 ContoEvents() 这个 protected 内部方法里,核心任务是捕捉日、周、月三个周期的开端。做法是用两个 datetime 数组 Periods_old[3] 和 Periods_new[3],下标 0/1/2 分别存日、周、月;构造函数里只初始化 Periods_new,故意留空 Periods_old,这样类一加载就会触发“新周期开始”的事件,把所有交易许可标识先打开,后续再由额度逻辑决定关不关。 判定逻辑很直白:每次跑 ContoEvents() 先把旧值搬过来,再用 iTime(_Symbol, PERIOD_D1, 0) 取当前周期起点时间,只要 new 和 old 不等,就说明换周期了。此时把 NewTradeDay 之类标识置 true,并调用 RefreshLimits() 重算输入限额。 主标识位 RiskTradePermission 用“与”逻辑汇总各周期权限:只有日、周、月三个子标识全为真,才允许开仓;任一个禁交易,总开关就断。把它做成 getter 暴露给 EA,就能直接当下单条件用。外汇与贵金属波动剧烈、杠杆风险高,这类周期重置逻辑在实盘前务必用 MT5 策略测试器跑通。 下面这段是构造函数与事件方法的骨架,注意 iTime 第二参数用 PERIOD_D1/W1/MN1,偏移 1 是取上一根已完成 K 线的起点时间,偏移 0 是当前未完成周期起点。
class=class="str">"cmt">//--- additional auxiliary arrays class="type">class="kw">datetime Periods_old[class="num">3]; class=class="str">"cmt">// class="num">0-day,class="num">1-week,class="num">2-mn class="type">class="kw">datetime Periods_new[class="num">3]; class=class="str">"cmt">// class="num">0-day,class="num">1-week,class="num">2-mn Periods_new[class="num">0] = iTime(_Symbol, PERIOD_D1, class="num">1); class=class="str">"cmt">// initialize the current day with the previous period Periods_new[class="num">1] = iTime(_Symbol, PERIOD_W1, class="num">1); class=class="str">"cmt">// initialize the current week with the previous period Periods_new[class="num">2] = iTime(_Symbol, PERIOD_MN1, class="num">1); class=class="str">"cmt">// initialize the current month with the previous period class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void RiskManagerBase::ContoEvents() { class=class="str">"cmt">// check the start of a new trading day NewTradeDay = false; class=class="str">"cmt">// variable for new trading day set to false Periods_old[class="num">0] = Periods_new[class="num">0]; class=class="str">"cmt">// copy to old, new Periods_new[class="num">0] = iTime(_Symbol, PERIOD_D1, class="num">0); class=class="str">"cmt">// update new for day if(Periods_new[class="num">0]!=Periods_old[class="num">0]) class=class="str">"cmt">// if do not match, it&class="macro">#x27;s a new day { Print(__FUNCTION__+", New trade day!"); class=class="str">"cmt">// inform NewTradeDay = true; class=class="str">"cmt">// variable to true }
「按日周切分重置风控与盈亏计数器」
EA 在每根 tick 里先比对日线起始时间,一旦发现 Periods_new[0] 与旧值不同,就判定进入新交易日。此时把 DayProfitArrive 置 false、RiskDayPermission 置 true,并调用 RefreshLimits() 重写当日额度,同时将 DayorderLoss 与 DayorderProfit 两个浮点变量清零,保证当日的盈亏统计从零开始。 周维度用 PERIOD_W1 的 iTime(_Symbol,PERIOD_W1,0) 取值做同样的比对。新周触发后除了打印 "New trade week!" 日志,还会把 RiskWeekPermission 放开,WeekorderLoss 与 WeekorderProfit 归零,相当于把周级别的风控闸门重新拉满。 这套切分逻辑的实际意义在于:日内策略若设了「单日盈利达标就停手」的开关,必须靠 DayProfitArrive 重置才能次日继续跑;而周/月级别的亏损上限也只有归零后才会重新生效。外汇与贵金属杠杆高,这类计数器若漏重置,可能让上周亏损额度悄悄延续到本周,放大穿仓概率。 下面这段是日与周边界判定的核心片段,逐行看逻辑很直接:DayProfitArrive=false 是清掉「已达目标」标记;RiskDayPermission=true 重新允许开仓;RefreshLimits() 刷新限额;两个 Dayorder 变量归零。周段里 iTime 取周线开盘时间,不匹配即新周,随后 Weekorder 系列清零。
DayProfitArrive = false; class=class="str">"cmt">// reset flag of reaching target profit after a new day started RiskDayPermission = true; class=class="str">"cmt">// allow opening new positions RefreshLimits(); class=class="str">"cmt">// update limits DayorderLoss = class="num">0; class=class="str">"cmt">// reset daily financial result DayorderProfit = class="num">0; class=class="str">"cmt">// reset daily financial result } class=class="str">"cmt">// check the start of a new trading week Periods_old[class="num">1] = Periods_new[class="num">1]; class=class="str">"cmt">// copy data to old period Periods_new[class="num">1] = iTime(_Symbol, PERIOD_W1, class="num">0); class=class="str">"cmt">// fill new period for week if(Periods_new[class="num">1]!= Periods_old[class="num">1]) class=class="str">"cmt">// if periods do not match, it&class="macro">#x27;s a new week { Print(__FUNCTION__+", line"+IntegerToString(__LINE__)+", New trade week!"); class=class="str">"cmt">// inform RiskWeekPermission = true; class=class="str">"cmt">// allow opening new positions RefreshLimits(); class=class="str">"cmt">// update limits WeekorderLoss = class="num">0; class=class="str">"cmt">// reset weekly losses WeekorderProfit = class="num">0; class=class="str">"cmt">// reset weekly profits } class=class="str">"cmt">// check the start of a new trading month Periods_old[class="num">2] = Periods_new[class="num">2]; class=class="str">"cmt">// copy the period to the old one
月线切换时重置风控计数器
在 MT5 的 EA 主循环里,用 iTime 抓取当前月线开盘时间,一旦和上一根月线时间不同,就判定进入了新月份。此时把 RiskMonthPermission 置 true,并调用 RefreshLimits() 刷新限额,同时将 MonthorderLoss 与 MonthorderProfit 清零,相当于每月重新给账户做一次风险归零。 真正下单许可不是单看月线,而是日、周、月三层权限全为 true 时才把 RiskTradePermission 设为 true;只要任意一层为 false,整体交易许可就失效。这种级联判断能避免某周期亏损超标后还在别的周期继续开仓,外汇与贵金属波动大,多层熔断可显著降低穿仓概率。 下面这段逻辑直接贴进 OnTick 或定时函数就能跑,重点看 Periods_new[2] 的比较与三段 if 的级联关系。
Periods_new[class="num">2] = iTime(_Symbol, PERIOD_MN1, class="num">0); class=class="str">"cmt">// update new period for month if(Periods_new[class="num">2]!= Periods_old[class="num">2]) class=class="str">"cmt">// if do not match, it&class="macro">#x27;s a new month { Print(__FUNCTION__+", New trade Month!"); class=class="str">"cmt">// inform RiskMonthPermission = true; class=class="str">"cmt">// allow opening new positions RefreshLimits(); class=class="str">"cmt">// update limits MonthorderLoss = class="num">0; class=class="str">"cmt">// reset the month&class="macro">#x27;s loss MonthorderProfit = class="num">0; class=class="str">"cmt">// reset the month&class="macro">#x27;s profit } class=class="str">"cmt">// set the permission to open new positions true only if everything is true class=class="str">"cmt">// set to true if(RiskDayPermission == true && class=class="str">"cmt">// if there is a daily limit available RiskWeekPermission == true && class=class="str">"cmt">// if there is a weekly limit available RiskMonthPermission == true class=class="str">"cmt">// if there is a monthly limit available ) { RiskTradePermission=true; class=class="str">"cmt">// if all are allowed, trading is allowed } class=class="str">"cmt">// set to false if at least one of them is false if(RiskDayPermission == false || class=class="str">"cmt">// no daily limit available RiskWeekPermission == false || class=class="str">"cmt">// or no weekly limit available
◍ 月度风控与日内止盈的互斥闸门
这段逻辑只做一件事:把「月度风控未生效」和「当日利润已达标」并成一条禁止交易的硬开关。只要二者满足其一,就直接把 RiskTradePermission 置为 false,后续订单不会被发送。
| RiskMonthPermission == false 代表本月没挂月度回撤限额,或限额功能不可用;DayProfitArrive == true 代表当日目标利润已经触碰。用 | 连接,意味着任意一条为真就触发封锁,不是「且」的关系。 |
|---|
实盘里这种写法常见于 EA 的日内收手机制:比如设了日盈利 2% 就停手,到达后即便月度风控没开,也不再开新仓。外汇与贵金属杠杆高,这类开关能降低连亏与过度交易概率,但是否生效仍依赖上游变量被正确赋值。 逐行看这段条件:第一行判断月度权限缺失或不可用;第二行判断当日利润到达;第三行收尾括号标注 then;第四行起进入代码块把交易许可关掉。改参数时重点确认 DayProfitArrive 的赋值点,否则会出现「到了利润却没停」的漏拦。
RiskMonthPermission == false || class=class="str">"cmt">// or no monthly limit available DayProfitArrive == true class=class="str">"cmt">// or target profit is reached ) class=class="str">"cmt">// then { RiskTradePermission=false; class=class="str">"cmt">// prohibit trading } }
「用内部类接管日利润平仓」
在风险管理器里,日目标利润一旦被总浮盈触达,就要把账户上所有头寸清空。与其手搓平仓逻辑,不如直接复用 MT5 终端自带的 CPositionInfo 和 CTrade 两个标准类,前者负责遍历与选中持仓,后者负责发平仓指令。 这两个类实例用 protected 级别定义在 RiskManagerBase 里即可,构造函数无需额外初始化——当前功能框架用不到附加配置。 AllOrdersClose() 是 public 方法,从 PositionsTotal() 倒序扫持仓,用 SelectByIndex 选中后取 ticket,再交给 PositionClose 按单号平。任何一笔失败就 Print 报错并返回 false,成功则打印并 continue。 触达目标利润或风控限制时都会调用它,同时用 SendNotification 推一条通知给手机。外汇与贵金属杠杆高,这类自动全平逻辑可能在某些滑点行情下未完全成交,需在 MT5 里实测确认。
CTrade r_trade; class=class="str">"cmt">// instance CPositionInfo r_position; class=class="str">"cmt">// instance class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| AllOrdersClose | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool RiskManagerBase::AllOrdersClose() class=class="str">"cmt">// closing market positions { class="type">ulong ticket = class="num">0; class=class="str">"cmt">// order ticket class="type">class="kw">string symb; for(class="type">int i = PositionsTotal(); i>=class="num">0; i--) class=class="str">"cmt">// loop through open positoins { if(r_position.SelectByIndex(i)) class=class="str">"cmt">// if a position selected { ticket = r_position.Ticket(); class=class="str">"cmt">// remember position ticket if(!r_trade.PositionClose(ticket)) class=class="str">"cmt">// close by ticket { Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); class=class="str">"cmt">// if not, inform class="kw">return(false); class=class="str">"cmt">// class="kw">return false } else { Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); class=class="str">"cmt">// if not, inform class="kw">continue; class=class="str">"cmt">// if everything is ok, class="kw">continue } } }
正文
<span class="keyword">return</span>(<span class="macro">true</span>); <span class="comment">// return true</span> } <span class="comment">//--- daily</span> <span class="keyword">if</span>(dayProfitControl) // check if functionality is enabled by the user { <span class="keyword">if</span>(CurrentEquity >= (StartEquity+PlanDayProfit)) <span class="comment">// if equity exceeds or equals start + target profit,</span> { DayProfitArrive = <span class="macro">true</span>; <span class="comment">// set flag that target profit is reached</span> <span class="functions
◍ 给EA接上风险监控的公共方法
在风险管理类里开一个 public 级别的 ContoMonitor() 方法,是把限额监控塞进 EA 的主入口。它先把之前写好的事件处理(ForOnTrade、ContoEvents)聚到一起,每个 tick 刷新一次账户状态,再拿实际亏损和用户在输入参数里填的 RiskPerDay、RiskPerWeek 等限额做比对。 月和周维度的逻辑很直白:只要实际损失冲破用户限额,对应时间段的交易许可标志直接翻成禁止,后续订单不会再发。日限额多一层——还得看有没有未平仓头寸,所以用 OR 把「当前浮亏+当日已亏 ≥ 日限额且利润为负的浮亏」和「纯当日已亏 ≥ 日限额」两种情况都罩住,触发后调平仓方法并打印日志。 光禁交易不够,得让用户看见。最省事的办法是调用终端自带的 Comment() 把字符串怼到图表左上角;类里再配一个 public 的 Message() 返回拼好的变量串,格式可参考原图「数据输出格式」那一栏。Comment() 只传一个 string 参数,你从类里把 DayorderLoss、RiskPerDay 这些抓出来拼进去就行。 别把监控当黑盒 Comment() 输出的是裸文本,真跑起来黄金和外汇这种高杠杆品种跳空时,日限额可能在几秒内被击穿,图表文字不会响铃。想更保险就自己加 Alert() 或推送到小布这类 AIGC 面板,至少把「限额触发」做成可回溯事件。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ContoMonitor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void RiskManagerBase::ContoMonitor() class=class="str">"cmt">// monitoring { ForOnTrade(); class=class="str">"cmt">// update at each tick ContoEvents(); class=class="str">"cmt">// event block class=class="str">"cmt">//--- class="type">class="kw">double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT); if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay && class=class="str">"cmt">// if equity is less than or equal to the start balance minus the daily risk currentProfit<class="num">0 && class=class="str">"cmt">// profit below zero RiskDayPermission==true) class=class="str">"cmt">// day trading is allowed || class=class="str">"cmt">// OR (RiskDayPermission==true && class=class="str">"cmt">// day trading is allowed MathAbs(DayorderLoss) >= RiskPerDay) class=class="str">"cmt">// loss exceed daily risk ) { Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = " +DoubleToString(currentProfit));class=class="str">"cmt">// notify
「日周月三层止损熔断的执行逻辑」
这段控制块在 EA 主循环里依次卡三道闸:日亏损、周亏损、月亏损。任意一层达到预设风险额度且对应权限开关还是 true,就立刻翻成 false,并强平所有持仓。 日层先动:当 MathAbs(DayorderLoss) 大于等于 RiskPerDay 且 RiskDayPermission 为 true,打印当前风险值与当日已亏,然后把 RiskDayPermission 置 false,调用 AllOrdersClose() 清仓,当天不再开新单。 周层与月层复用同一套判定骨架,只是把 DayorderLoss / RiskPerDay / RiskDayPermission 换成 WeekorderLoss / RiskPerWeek / RiskWeekPermission 和 MonthorderLoss / RiskPerMonth / RiskMonthPermission。三层彼此独立,任一层触发都会全平,但只锁自己那段时间维度的新开权限。 把这段代码直接塞进你的 EquityControl 函数,用 MT5 策略测试器跑一段含连续回撤的历史,观察日志里 Print 输出的 RiskPerDay 与 DayorderLoss 数值,能验证熔断是否在阈值交点准时翻牌。外汇与贵金属杠杆高,这类硬止损只能压住单维度暴露,不意味账户免亏。
Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = "+DoubleToString(RiskPerDay)); class=class="str">"cmt">// notify Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = "+DoubleToString(DayorderLoss)); class=class="str">"cmt">// notify RiskDayPermission=false; class=class="str">"cmt">// prohibit opening new orders during the day AllOrdersClose(); class=class="str">"cmt">// close all open positions } class=class="str">"cmt">// check if there is a WEEK limit available for opening a new position if there are no open ones if( MathAbs(WeekorderLoss)>=RiskPerWeek && class=class="str">"cmt">// if weekly loss is greater than or equal to the weekly risk RiskWeekPermission==true) class=class="str">"cmt">// and we traded { RiskWeekPermission=false; class=class="str">"cmt">// prohibit opening of new orders during the day AllOrdersClose(); class=class="str">"cmt">// close all open positions Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss)); class=class="str">"cmt">// notify Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek)); class=class="str">"cmt">// notify } class=class="str">"cmt">// check if there is a MONTH limit available for opening a new position if there are no open ones if( MathAbs(MonthorderLoss)>=RiskPerMonth && class=class="str">"cmt">// if monthly loss is greater than or equal to the monthly risk RiskMonthPermission==true) class=class="str">"cmt">// we traded { RiskMonthPermission=false; class=class="str">"cmt">// prohibit opening of new orders during the day AllOrdersClose(); class=class="str">"cmt">// close all open positions
把风控状态打印成可读面板
风险管理器跑完日/周/月维度的校验后,需要把权限与盈亏实况抛到日志里,否则你根本不知道 EA 是卡在哪一档风控上不开仓。上面这段 Message() 方法就是把内部变量拼成一段带分隔线的纯文本报告。 它先拼出『----------Risk-Manager----------』总标题,再列四类权限布尔值:RiskTradePer(单笔是否放行)、RiskDayPer / RiskWeekPer / RiskMonthPer(对应周期风险额度是否还有余量)。这几行直接告诉你当前账户处于『能交易』还是『被风控冻结』状态。 第二部分打印限额与已用:RiskPerDay / RiskPerWeek / RiskPerMonth 用 DoubleToString(...,2) 保留两位小数,单位是账户净值币种(多为 USD)。假设你设 RiskPerMonth=500.00,而 Monthly 段显示 MonthorderLoss 已逼近 480.00,那月内剩余容错就只剩 20 美金,外汇与贵金属的高波动下很可能一次止损就触顶。 第三部分输出各周期已实现盈亏:DayLoss / DayProfit / WeekLoss / WeekProfit 同样保留两位小数。把这些数拉出来对照限额,就能判断周亏损是不是已吃掉月限额的大头。开盘前在 MT5 策略测试器跑一遍,看日志里 Message() 的输出格式,比盲调参数靠谱得多。
Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss)); class=class="str">"cmt">// notify Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth)); class=class="str">"cmt">// notify } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RiskManagerBase::Message(class="type">void) { class="type">class="kw">string msg; class=class="str">"cmt">// message msg += "\n"+" ----------Risk-Manager---------- "; class=class="str">"cmt">// common class=class="str">"cmt">//--- msg += "\n"+"RiskTradePer = "+(class="type">class="kw">string)RiskTradePermission; class=class="str">"cmt">// final trade permission msg += "\n"+"RiskDayPer = "+(class="type">class="kw">string)RiskDayPermission; class=class="str">"cmt">// daily risk available msg += "\n"+"RiskWeekPer = "+(class="type">class="kw">string)RiskWeekPermission; class=class="str">"cmt">// weekly risk available msg += "\n"+"RiskMonthPer = "+(class="type">class="kw">string)RiskMonthPermission; class=class="str">"cmt">// monthly risk available class=class="str">"cmt">//---limits and inputs msg += "\n"+" -------------------------------- "; class=class="str">"cmt">// msg += "\n"+"RiskPerDay = "+DoubleToString(RiskPerDay,class="num">2); class=class="str">"cmt">// daily risk in usd msg += "\n"+"RiskPerWeek = "+DoubleToString(RiskPerWeek,class="num">2); class=class="str">"cmt">// weekly risk in usd msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,class="num">2); class=class="str">"cmt">// monthly risk usd class=class="str">"cmt">//--- current profits and losses for periods msg += "\n"+" -------------------------------- "; class=class="str">"cmt">// msg += "\n"+"DayLoss = "+DoubleToString(DayorderLoss,class="num">2); class=class="str">"cmt">// daily loss msg += "\n"+"DayProfit = "+DoubleToString(DayorderProfit,class="num">2); class=class="str">"cmt">// daily profit msg += "\n"+"WeekLoss = "+DoubleToString(WeekorderLoss,class="num">2); class=class="str">"cmt">// weekly loss msg += "\n"+"WeekProfit = "+DoubleToString(WeekorderProfit,class="num">2); class=class="str">"cmt">// weekly profit
◍ 把月度与当日风控数据拼进监控字符串
这段逻辑出现在某个监控函数的收尾处,负责把已经算好的月度盈亏、手续费、库存费,以及可选的当日利润控制状态,拼成一段可读的 msg 字符串返回给调用方。 月度部分固定输出四项:MonthLoss 与 MonthProfit 用 DoubleToString(...,2) 保留两位小数,MonthCommis 和 MonthSwap 同理,分别对应当月手续费与库存费。若 dayProfitControl 开关为真,才追加当日区块,打出 DayProfitArrive 布尔值、StartBalance、PlanDayProfit、PlanDayEquity 四个字段。 在 MT5 里直接把这段贴进你的 EA 监控函数,用 Print(msg) 或发送到手机通知,就能在日志里看到类似「MonthProfit = 123.45 / PlanDayEquity = 5000.00」的实时剖面。外汇与贵金属品种波动剧烈,这类当日权益计划值仅作概率参考,实际可能偏离。
msg += "\n"+"MonthLoss = "+DoubleToString(MonthorderLoss,class="num">2); class=class="str">"cmt">// monthly loss msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,class="num">2); class=class="str">"cmt">// monthly profit msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,class="num">2); class=class="str">"cmt">// monthly commissions msg += "\n"+"MonthSwap = "+DoubleToString(MonthOrderSwap,class="num">2); class=class="str">"cmt">// monthly swaps class=class="str">"cmt">//--- for current monitoring if(dayProfitControl) class=class="str">"cmt">// if control daily profit { msg += "\n"+" ---------dayProfitControl-------- "; msg += "\n"+"DayProfitArrive = "+(class="type">class="kw">string)DayProfitArrive; class=class="str">"cmt">// daily profit achieved msg += "\n"+"StartBallance = "+DoubleToString(StartBalance,class="num">2); class=class="str">"cmt">// starting balance msg += "\n"+"PlanDayProfit = "+DoubleToString(PlanDayProfit,class="num">2); class=class="str">"cmt">// target profit msg += "\n"+"PlanDayEquity = "+DoubleToString(PlanDayEquity,class="num">2); class=class="str">"cmt">// target equity } class="kw">return(msg); class=class="str">"cmt">// class="kw">return value }
「把风控类接进EA的实操落点」
基础风控类只是骨架,真正跑在盘面上还得挂进EA。除了日损拦截,短线止损策略里最该补的是点差监控:写个 SpreadMonitor(int intSL),把理论止损点数和实时点差比一比,比例失控就禁单,能躲开点差暴涨扫损的高风险局面。 滑点也不能放任。SlippageCheck() 在经纪商成交价偏离挂单太远时直接平掉这笔,保住整体统计样本不被单笔坏交易污染。固定盈亏比策略里,滑点会啃掉比例优势,宁可小亏走人。 月收益目标控制可以复用日利逻辑,适合长线EA。类本身已具备手动日内交易所需功能,用 #include <RiskManagerBase.mqh> 接进来即可,EA和手动单挂同一图表互不冲突。 全局层声明 RiskManagerBase *RMB 指针;OnInit 里 new 分配内存,OnDeinit 里 delete 释放,否则MT5退出时漏内存。顺手 Comment(" ") 清掉图表文字。OnTick 调 ContoMonitor() 跑主监控。下面这段是最小可编译骨架,直接贴进 ManualRiskManager.mq5 能跑。 外汇与贵金属杠杆高,这类拦截只降概率不挡黑天鹅,上真实账户前请用策略测试器先跑。
class="macro">#include <RiskManagerBase.mqh> RiskManagerBase *RMB; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { RMB = new RiskManagerBase(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- class="kw">delete RMB; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { RMB.ContoMonitor(); Comment(RMB.Message()); } class=class="str">"cmt">//+------------------------------------------------------------------+
同一套分形信号,风控改了盈亏结构
用 USDJPY 的 M15 图表做对照:2024.01.01 至 2024.03.18,初始本金 10000 美元、杠杆 1:100、Every Tick 模拟、无延迟理想环境。入场信号直接取自日线分形突破,不写策略逻辑,只把现成信号塞进 EA 跑一遍。 无风控版本(ManualRiskManager UniTest1)净盈利 41.1 美元,最大余额回撤 0.74%,净值回撤 1.13%,期望收益 10.28,夏普 0.12。接上风控类(UniTest2,日风险 0.25%、周 0.75%、月 2.25%、日利目标 0.78、开启日利控制)后,净盈利跑到 144.48 美元,余额回撤压到 0.25%,期望收益 36.12,夏普 0.67。 平均亏损从 -74.47 缩到 -25.46,平均盈利从 38.52 抬到 56.65,风险回报比由 0.52 翻到 2.23。外汇与贵金属属高风险品种,回测为理想环境,实盘滑点可能吃掉部分优势。 下面的结构体是信号载体的最小定义,改 symbol / direction / price / tradedate 就能换自己的手动信号做单元测试。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TradeInputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct TradeInputs { class="type">class="kw">string symbol; class=class="str">"cmt">// 交易品种,如 "USDJPY" class="type">ENUM_POSITION_TYPE direction; class=class="str">"cmt">// 持仓方向:买入或卖出 class="type">class="kw">double price; class=class="str">"cmt">// 触发下单的价格水平 class="type">class="kw">datetime tradedate; class=class="str">"cmt">// 信号对应的时间,用于按 tick 比对是否到点 class="type">bool done; class=class="str">"cmt">// 该条信号是否已处理过的标记 };
◍ 正文
<span class="keyword">class</span> TradeModel { <span class="keyword">protected</span>: CTrade *cTrade; <span class="comment">// to trade</span> TradeInputs container[]; <span class="comment">// container of entries</span> <span class="keyword">int</span> size; <span class="comment">// container size</span> <span class="keyword">public</span>: TradeModel(<span class="keyword">con
「交易实例的回收与定时派单循环」
在 CTrade 封装类析构时,先用 CheckPointer(cTrade)!=POINTER_INVALID 判断指针有效性,再 delete 释放对象,避免 EA 重载时内存泄漏。 订单参数在初始化阶段就固定:偏离容差设 1000 点(SetDeviationInPoints(1000)),魔术码 123(SetExpertMagicNumber(123)),同步下单模式(SetAsyncMode(false))。这套配置在回测和实盘里都直接生效,改一个数就会影响成交判定。 Processing() 是轮询核心:取 TimeCurrent() 和当前 Symbol 的 BID/ASK,再遍历预设的 container 数组。每条记录带 tradedate 和 done 标记,只有「未成交且时间已到」才进入 switch 按 direction 分发。 外汇与贵金属杠杆高,这种定时派单逻辑若 tradedate 误差超过服务器执行延迟,可能直接错过入场窗口,建议先在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍确认时间戳对齐。
cTrade.SetDeviationInPoints(class="num">1000); class=class="str">"cmt">// deviation cTrade.SetExpertMagicNumber(class="num">123); class=class="str">"cmt">// magic number cTrade.SetAsyncMode(false); class=class="str">"cmt">// asynchronous method } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ~TradeModel | class=class="str">"cmt">//+------------------------------------------------------------------+ TradeModel::~TradeModel(class="type">void) { if(CheckPointer(cTrade)!=POINTER_INVALID) class=class="str">"cmt">// if there is an instance, { class="kw">delete cTrade; class=class="str">"cmt">// class="kw">delete } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Processing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void TradeModel::Processing(class="type">void) { class="type">class="kw">datetime timeCurr = TimeCurrent(); class=class="str">"cmt">// request current time class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); class=class="str">"cmt">// take bid class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); class=class="str">"cmt">// take ask for(class="type">int i=class="num">0; i<size; i++) class=class="str">"cmt">// loop through inputs { if(container[i].done==false && class=class="str">"cmt">// if we haven&class="macro">#x27;t traded yet AND container[i].tradedate <= timeCurr) class=class="str">"cmt">// date is correct { class="kw">switch(container[i].direction) class=class="str">"cmt">// check trade direction { class=class="str">"cmt">//---
挂单价触发后的双向下单分支
在挂单容器遍历逻辑里,多空两路要用 case 分开处理,否则 ask/bid 比较方向会反掉。下面这段就是 Buy 与 Sell 各自触发时的标准写法。
[CODE]
case POSITION_TYPE_BUY: // 识别为买入挂单
if(container[i].price >= ask) // 当前卖价刺穿挂单价才追
{
if(cTrade.Buy(0.1)) // 以0.1手市价买入
{
container[i].done = true; // 标记该挂单已成交
Print("Buy has been done"); // 终端打印通知
}
else
{
Print("Error: buy"); // 下单失败报错
}
}
break; // 结束Buy分支
//---
case POSITION_TYPE_SELL: // 识别为卖出挂单
if(container[i].price <= bid) // 当前买价跌破挂单价才追
{
if(cTrade.Sell(0.1)) // 以0.1手市价卖出
{
[/CODE]
逐行看:Buy 分支要求 container[i].price >= ask,也就是挂单价在卖价之上才算触及,避免用 bid 比错边。Sell 分支反过来用 <= bid,挂单价在买价之下才触发。
两处下单手数都写死 0.1,实盘记得换成变量或仓位管理函数;外汇与贵金属杠杆高,错边触发可能连续开仓,先在策略测试器跑一遍再上真仓。
case POSITION_TYPE_BUY: class=class="str">"cmt">// if Buy, if(container[i].price >= ask) class=class="str">"cmt">// check if price has reached and { if(cTrade.Buy(class="num">0.1)) class=class="str">"cmt">// by the same lot { container[i].done = true; class=class="str">"cmt">// if time has passed, put a flag Print("Buy has been done"); class=class="str">"cmt">// notify } else class=class="str">"cmt">// if hasn&class="macro">#x27;t passed, { Print("Error: buy"); class=class="str">"cmt">// notify } } break; class=class="str">"cmt">// complete the case class=class="str">"cmt">//--- case POSITION_TYPE_SELL: class=class="str">"cmt">// if Sell if(container[i].price <= bid) class=class="str">"cmt">// check if price has reached and { if(cTrade.Sell(class="num">0.1)) class=class="str">"cmt">// sell the same lot {
◍ 挂单触发后的状态标记与回测样本
这段片段承接前面的开关结构:当卖出条件被触发且时间已越过限定点,代码会把容器里的 done 标记为 true,并用 Print 抛出 "Sell has been done";若时间未到则走 else 分支报 "Error: sell"。这种 done 标志本质是防止同一笔计划被重复处理的闸门,实盘里漏掉它可能导致同价位挂单被多次推送。 switch 的 default 分支只做一件事:打印 "Wrong inputs" 然后直接 return,说明外部传入的枚举或品种不在预期内时整套模型不启动。注意这里 return 在 default 内,若前面 case 没 break 也不会穿透到此处。 尾部给出了可直接塞进 MT5 的回测样本:USDJPYz 上 4 笔计划,卖单分别挂在 146.636(2024-01-31)与 149.672(2024-02-08),买单挂在 148.794(2024-02-05)与 148.882(2024-02-08),初始 done 全为 false。外汇与贵金属杠杆高,这类历史价位仅用于验证逻辑,实盘触发概率随波动而变化。 把 TradeModel 实例化后调一次 Processing() 即按表下单;MqlDateTime time_curr 留作后续比对当前时间用,你可以自己在 OnTick 里填值看 done 翻转节奏。
container[i].done = true; class=class="str">"cmt">// if time has passed, put a flag Print("Sell has been done"); class=class="str">"cmt">// notify } else class=class="str">"cmt">// if hasn&class="macro">#x27;t passed, { Print("Error: sell"); class=class="str">"cmt">// notify } } break; class=class="str">"cmt">// complete the case class=class="str">"cmt">//--- class="kw">default: Print("Wrong inputs"); class=class="str">"cmt">// notify class="kw">return; break; } } } class="macro">#include <TradeModel.mqh> class=class="str">"cmt">//--- TradeInputs modelInputs[] = { {"USDJPYz", POSITION_TYPE_SELL, class="num">146.636, D&class="macro">#x27;class="num">2024-class="num">01-class="num">31&class="macro">#x27;,false}, {"USDJPYz", POSITION_TYPE_BUY, class="num">148.794, D&class="macro">#x27;class="num">2024-class="num">02-class="num">05&class="macro">#x27;,false}, {"USDJPYz", POSITION_TYPE_BUY, class="num">148.882, D&class="macro">#x27;class="num">2024-class="num">02-class="num">08&class="macro">#x27;,false}, {"USDJPYz", POSITION_TYPE_SELL, class="num">149.672, D&class="macro">#x27;class="num">2024-class="num">02-class="num">08&class="macro">#x27;,false} }; class=class="str">"cmt">//--- tModel = new TradeModel(modelInputs); tModel.Processing(); class=class="str">"cmt">// place orders class="type">MqlDateTime time_curr; class=class="str">"cmt">// current time structure
「用服务器时间卡掉隔夜持仓」
MT5 的 TimeCurrent() 取的是经纪商服务器时间而非本地时钟,这在跨时区做贵金属或外汇时容易被忽略。上面的片段先拉取当前时间,再判断小时数是否越过 23 点。 若 hour >= 23 即视为当日收尾,直接调用 RMB.AllOrdersClose() 平掉所有未平仓。逻辑上能避开隔夜跳空与掉期费,但 23 点这个阈值要按你经纪商每日结算规则核对,欧美盘夏令时可能偏移一小时。 外汇与贵金属属高杠杆品种,强制平仓虽降低隔夜风险,也可能在尾盘波动中错过延续行情,实盘前请在策略测试器用历史数据验证触发频率。
TimeCurrent(time_curr); class=class="str">"cmt">// request current time if(time_curr.hour >= class="num">23) class=class="str">"cmt">// if end of day { RMB.AllOrdersClose(); class=class="str">"cmt">// close all positions }
把工具请下神坛
手动交易里挂一个风险管理器,已经验证能抬升策略的存活率——连原本就盈利的打法也会被它滤掉情绪单,亏损策略则被硬性截断在可承受区间内。外汇与贵金属杠杆高,这种保护不是收益加成,只是不让你一次归零的概率稍大一点。 限制期不是让你干瞪眼的空档。去翻那几笔被掐掉的亏损单,看是点位错了还是仓位飘了,等窗口过了再无情绪地重开。附件里 ManualRiskManager.mq5 只有 3.04 KB,丢进 MT5 就能跑,先拿 1 分钟图回测一遍再说。 真要把这类逻辑塞进纯算法 EA,原作者也还在犹豫。但至少现在,它只是个帮你管手贱的脚本,不是圣杯。