手动交易的风险管理·进阶篇
(2/3)· 从零写一个手动交易风控基类,让EA在触限那一刻替你关仓并喊停
「RefreshLimits 在挂单逻辑里的真实作用」
上面这行代码出现在 EA 的订单刷新流程里,本质是把当前经纪商允许的最小止损距离、最大挂单偏移等限制重新读进内存。很多手动改过点差的账户,若不主动调 RefreshLimits(),旧的限制值会一直留着,导致后续下单直接报 130 错误(无效止损)。 外汇与贵金属属于高杠杆品种,经纪商在重大数据前常临时扩大 limits,靠缓存值下单等于盲狙。建议在每次 OnTick 或定时器的开头都跑一遍,别等报错了才想起来。 下面这行就是最小实现,注意它不带返回值,失败也不会抛异常,得自己配合 GetLastError 判断。
RefreshLimits(); class=class="str">"cmt">// update limits
◍ 给风险限额周期加保护字段
做多周期风控时,最怕月限额已经爆了,系统却因为日限额还没到就继续放单。解决办法是在类里放一组 protected 级别的 bool 标识:日、周、月各一个「限额触发锁」,再加一个总开关 RiskTradePermission,只有三个周期锁全未触发时才允许开仓。 除了锁,还要有 DayProfitArrive 控日盈利到达、NewTradeDay 标记新交易日。实际盈亏用 double 字段按日/周/月分别累计,隔夜利息和佣金单独存 MonthOrderSwap 与 MonthOrderCommis,不并进对应周期的损失里。 把佣金和 swap 剥离出来,是为了之后能区分「策略本身亏的」和「经纪商费用吃掉的」。外汇和贵金属杠杆高,这类分离核算能让你在回测里看清真实决策胜率。 下面这段声明直接贴进 EA 类体就能编译,注意 WeekPermission 的注释原文写错了(误写成 daily limit),自己改一下避免误导。
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() 对比输出,差异一眼就能看到。
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 遍历的载体。外汇与贵金属杠杆高,回测边界差一天,样本量可能偏差数倍,锚点必须手算确认。
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% 以上。
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==-- 的切口,说明同一套遍历框架直接换时间过滤就能切到周维度,不用重写取数逻辑。
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==--