手动交易的风险管理·进阶篇
🛡️

手动交易的风险管理·进阶篇

(2/3)· 从零写一个手动交易风控基类,让EA在触限那一刻替你关仓并喊停

偏理论进阶 第 2/3 篇
为手动交易挂个EA做风控,不等于把方向盘交给机器——它只在触限时报停并关仓,你随时能摘掉重来。真正难的不是写类,而是承认赌徒心态会在亏损后自动加码。把这条线画在代码里,比画在记事本上管用。

「RefreshLimits 在挂单逻辑里的真实作用」

上面这行代码出现在 EA 的订单刷新流程里,本质是把当前经纪商允许的最小止损距离、最大挂单偏移等限制重新读进内存。很多手动改过点差的账户,若不主动调 RefreshLimits(),旧的限制值会一直留着,导致后续下单直接报 130 错误(无效止损)。 外汇与贵金属属于高杠杆品种,经纪商在重大数据前常临时扩大 limits,靠缓存值下单等于盲狙。建议在每次 OnTick 或定时器的开头都跑一遍,别等报错了才想起来。 下面这行就是最小实现,注意它不带返回值,失败也不会抛异常,得自己配合 GetLastError 判断。

MQL5 / C++
  RefreshLimits();                                                                       class=class="str">"cmt">// update limits

◍ 给风险限额周期加保护字段

做多周期风控时,最怕月限额已经爆了,系统却因为日限额还没到就继续放单。解决办法是在类里放一组 protected 级别的 bool 标识:日、周、月各一个「限额触发锁」,再加一个总开关 RiskTradePermission,只有三个周期锁全未触发时才允许开仓。 除了锁,还要有 DayProfitArrive 控日盈利到达、NewTradeDay 标记新交易日。实际盈亏用 double 字段按日/周/月分别累计,隔夜利息和佣金单独存 MonthOrderSwap 与 MonthOrderCommis,不并进对应周期的损失里。 把佣金和 swap 剥离出来,是为了之后能区分「策略本身亏的」和「经纪商费用吃掉的」。外汇和贵金属杠杆高,这类分离核算能让你在回测里看清真实决策胜率。 下面这段声明直接贴进 EA 类体就能编译,注意 WeekPermission 的注释原文写错了(误写成 daily limit),自己改一下避免误导。

MQL5 / C++
  class="type">bool                RiskTradePermission;     class=class="str">"cmt">// general variable - whether opening of new trades is allowed
  class="type">bool                RiskDayPermission;       class=class="str">"cmt">// flag prohibiting trading if daily limit is reached
  class="type">bool                RiskWeekPermission;      class=class="str">"cmt">// flag to prohibit trading if daily limit is reached
  class="type">bool                RiskMonthPermission;     class=class="str">"cmt">// flag to prohibit trading if monthly limit is reached
  class="type">bool                DayProfitArrive;         class=class="str">"cmt">// variable to control if daily target profit is achieved
  class="type">bool                NewTradeDay;             class=class="str">"cmt">// variable for a new trading day
  class=class="str">"cmt">//--- actual limits
  class="type">class="kw">double              DayorderLoss;            class=class="str">"cmt">// accumulated daily loss
  class="type">class="kw">double              DayorderProfit;          class=class="str">"cmt">// accumulated daily profit
  class="type">class="kw">double              WeekorderLoss;           class=class="str">"cmt">// accumulated weekly loss
  class="type">class="kw">double              WeekorderProfit;         class=class="str">"cmt">// accumulated weekly profit
  class="type">class="kw">double              MonthorderLoss;          class=class="str">"cmt">// accumulated monthly loss
  class="type">class="kw">double              MonthorderProfit;        class=class="str">"cmt">// accumulated monthly profit
  class="type">class="kw">double              MonthOrderSwap;          class=class="str">"cmt">// monthly swap
  class="type">class="kw">double              MonthOrderCommis;        class=class="str">"cmt">// monthly commission

用本地时间锚定限额统计窗口

在 EA 里实时统计当日、当周、当月已用限额,第一步是把时间窗口算准。很多人习惯用 TimeCurrent() 取时间,但那是经纪商最后一笔报价的时间戳,跨时区账户会偏掉;用 TimeLocal() 拿终端本地时间,才能和你自己定义的日/周/月起点对得上。 具体做法是在类的 protected 区写 ForOnTrade(),先声明 MqlDateTime 类型的 local、start_day、start_week、start_month 四个结构,全部用 TimeLocal() 初始化。随后把 start_day 和 start_week 的 sec、min、hour 字段强制置 0,相当于把起点压到 00:00:00。 周的起点没有固定日历日,要靠 local.day_of_week(MqlDateTime 里从 0 起算的星期编号)反推。月就简单,直接认每月 1 号。得到这些起点后,用 StructToTime() 转成 datetime,喂给 HistorySelect() 拉已平仓成交。 拉完用 HistoryDealsTotal() 拿数量,for 循环里用 HistoryDealGetTicket() 取每个成交的 ticket,再用 HistoryDealGetDouble() / HistoryDealGetInteger() 读金额与类型;用 if 过滤 DEAL_TYPE_BUY 和 DEAL_TYPE_SELL,把余额、赠金类记录剔除。这个方法很轻量,挂 NewTick 每次报价刷新一次实际限额也不会卡。 别把 TimeCurrent 当默认时间源 经纪商服务器时区和你本地可能差好几小时,限额周期算错会直接让风控逻辑失效。开 MT5 把两段函数各打一行 Print() 对比输出,差异一眼就能看到。

MQL5 / C++
  class="type">MqlDateTime local, start_day, start_week, start_month;                    class=class="str">"cmt">// create structure to filter dates
  TimeLocal(local);                                                                 class=class="str">"cmt">// fill in initially
  TimeLocal(start_day);                                                              class=class="str">"cmt">// fill in initially
  TimeLocal(start_week);                                                             class=class="str">"cmt">// fill in initially
  TimeLocal(start_month);                                                            class=class="str">"cmt">// fill in initially
class=class="str">"cmt">//--- reset to have the report from the beginning of the period
  start_day.sec     = class="num">0;                                                             class=class="str">"cmt">// from the day beginning
  start_day.min     = class="num">0;                                                             class=class="str">"cmt">// from the day beginning
  start_day.hour    = class="num">0;                                                             class=class="str">"cmt">// from the day beginning
  start_week.sec    = class="num">0;                                                             class=class="str">"cmt">// from the week beginning
  start_week.min    = class="num">0;                                                             class=class="str">"cmt">// from the week beginning

「把周初与月初的时间锚点算准」

做跨周期统计时,先把周和月的起点钉死,后面筛成交才不会出现边界漂移。下面这段初始化把周一起点小时、月初秒分时全部清零,相当于告诉 MT5:从 00:00 整开始算账。 周起点处理有个坑:MQL5 里 day_of_week 以周日为 0。代码里若 start_week.day_of_week 等于 0,说明当前就是周首日,dif 直接置 0;否则用 day_of_week-1 算出距周一的天数,再让 start_week.day 减去这个差值,把日期回拨到周一。 月初反而干净,start_month.day 无条件赋 1 就行,不用管星期。随后声明的 total、ticket、type 三个变量分别是命中笔数、订单号、订单类型,是后面 HistorySelect 遍历的载体。外汇与贵金属杠杆高,回测边界差一天,样本量可能偏差数倍,锚点必须手算确认。

MQL5 / C++
  start_week.hour   = class="num">0;                                                                       class=class="str">"cmt">// from the week beginning
  start_month.sec   = class="num">0;                                                                       class=class="str">"cmt">// from the month beginning
  start_month.min   = class="num">0;                                                                       class=class="str">"cmt">// from the month beginning
  start_month.hour  = class="num">0;                                                                       class=class="str">"cmt">// from the month beginning
class=class="str">"cmt">//--- determining the beginning of the week
  class="type">int dif;                                                                                     class=class="str">"cmt">// day of week difference variable
  if(start_week.day_of_week==class="num">0)                                                               class=class="str">"cmt">// if this is the first day of the week
    {
      dif = class="num">0;                                                                                 class=class="str">"cmt">// then reset
    }
  else
    {
      dif = start_week.day_of_week-class="num">1;                                                          class=class="str">"cmt">// if not the first, then calculate the difference
      start_week.day -= dif;                                                                   class=class="str">"cmt">// subtract the difference at the beginning of the week from the number of the day
    }
class=class="str">"cmt">//---month
  start_month.day        = class="num">1;                                                                  class=class="str">"cmt">// everything is simple with the month
class=class="str">"cmt">//---
  class="type">uint      total  = class="num">0;                                                                        class=class="str">"cmt">// number of selected trades
  class="type">ulong     ticket = class="num">0;                                                                        class=class="str">"cmt">// order number
  class="type">long      type;                                                                              class=class="str">"cmt">// order type

◍ 按日切历史并初始化统计容器

做日内盈亏统计前,先把单笔与周期级别的累加变量清零,否则跨周期回看时旧数据会污染结果。下面这组 double 声明把利润、手续费、库存费以及日/周/月的盈亏容器一次性置 0,注意日周月的 Loss 字段都不含佣金,净口径要另算。 清零之后调用 HistorySelect 按当天 0 点(start_day)到当前本地时间(local)拉取成交历史。StructToTime 把 MqlDateTime 结构转成时间戳,选区错了后面 HistoryDealsTotal 返回的条数就会偏少。 拉完先读 total = HistoryDealsTotal() 确认命中单数,再把 ticket 置 0 准备逐单遍历。外汇与贵金属杠杆高,日级统计若漏掉 swap 和 commis,对剥头皮策略的实盈评估可能偏差 10% 以上。

MQL5 / C++
  class="type">class="kw">double  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
  DayorderLoss       = class="num">0;                                                                     class=class="str">"cmt">// daily loss without commission
  DayorderProfit     = class="num">0;                                                                     class=class="str">"cmt">// daily profit
  WeekorderLoss      = class="num">0;                                                                     class=class="str">"cmt">// weekly loss without commission
  WeekorderProfit    = class="num">0;                                                                     class=class="str">"cmt">// weekly profit
  MonthorderLoss     = class="num">0;                                                                     class=class="str">"cmt">// monthly loss without commission
  MonthorderProfit   = class="num">0;                                                                     class=class="str">"cmt">// monthly profit
  MonthOrderCommis   = class="num">0;                                                                     class=class="str">"cmt">// monthly commission
  MonthOrderSwap     = class="num">0;                                                                     class=class="str">"cmt">// monthly swap
class=class="str">"cmt">//--- now select data by --==DAY==--
  HistorySelect(StructToTime(start_day),StructToTime(local));                                class=class="str">"cmt">// select 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

逐笔剥离当日盈亏与隔夜成本

在 MT5 历史回放里,想把一天的真实交易质量拆开看,得先按成交单逐笔抓利润、佣金与掉期。下面这段逻辑把三个累加器清零后,用 HistoryDealGetTicket 遍历 selected 订单,再借 HistoryDealGetDouble 取出 DEAL_PROFIT、DEAL_COMMISSION、DEAL_SWAP。 只认 DEAL_TYPE_BUY 与 DEAL_TYPE_SELL 的实盘进出场,过滤掉余额操作或手续费调整。盈利单累进 DayorderProfit,亏损单走 MathAbs 取绝对值后灌进 DayorderLoss,这样正负不再互抵,胜亏结构一眼可辨。 外汇与贵金属自带高杠杆与隔夜利率跳变,swap 字段在周三持仓时往往放大三倍,回测不单独拎出来会误判策略成本。把日级盈亏和掉期拆成两条线,才可能看清策略到底是吃价差还是被利息吃。 跑完日级循环后,注释里留了 --==WEEK==-- 的切口,说明同一套遍历框架直接换时间过滤就能切到周维度,不用重写取数逻辑。

MQL5 / C++
  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,
          {
          DayorderProfit += profit;                                                 class=class="str">"cmt">// add to profit
          }
        else
          {
          DayorderLoss += MathAbs(profit);                                          class=class="str">"cmt">// if loss, add up
          }
        }
      }
    }
class=class="str">"cmt">//--- now select data by --==WEEK==--
交给小布盯盘看盘口
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到当日回撤是否逼近你设的阈值,不用自己算百分比。

常见问题

本篇基类专注时间维度上的累计亏损与日目标利润,单笔手数需由策略层或单独仓位模块处理,两者可并行不冲突。
会。移除EA等于跳过风控,若市场确对你不利,倾向次日复盘找原因,而非当场加码弥补,否则回测有效的策略也可能实盘失效。
小布盯盘的品种页已聚合当日回撤与盈利进度可视化,逻辑等同文中基类,你只需设阈值,不必自己编译EA。
当交易者同时做不相关品种且想独立看每笔入场时,关掉它可避免被无关盈利提前锁单,但是否启用取决于你对外汇贵金属高相关风险的判断。
手动版触限仅建议停手,算法版应直接阻断发单并嵌入结构,否则限额形同虚设,这部分实现差异在进阶扩展时会提到。