算法交易中的风险管理器·综合运用
🛡️

算法交易中的风险管理器·综合运用

(3/3)·从接口到集成测试,用一套类把日内与投资的回撤关进笼子里

进阶 第 3/3 篇
很多交易者把风控逻辑散落在 EA 各个函数里,一改参数就满屏报错。把风险规则收拢进独立类,才是后续能安稳迭代的底子。

用接口搭起风险管理类的骨架

MQL5 不支持多重继承,所以接口得作为基类 RiskManagerBase 的第一个父类来写,这样能把项目压成一条连贯的继承链,不会卡死扩展。 让 RiskManagerBase 去继承前面定义的 IShortStopLoss 接口,纯虚函数没有字段也没有构造/析构,继承时用 private 还是 public 都无所谓,功能往后甩给 RiskManagerAlgo 子类就行。 最终 RiskManagerAlgo 的继承结构靠封装公共方法给出完整能力,图里能看清层次。在拼算法前,只需补上决策工具去跑一遍风控计算逻辑做验证。 下面这段是基类声明的核心一行,注意冒号后直接接接口名:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//|                 RiskManagerBase                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class RiskManagerBase:IShortStopLoss              class=class="str">"cmt">// the purpose of the class is to control risk in terminal

◍ 用日线分形驱动日内开仓信号

把之前手动风控里的分形逻辑搬进 EA,核心不变:日线图上破上分形则买,下破下分形则卖,仓位只在当日开闭。差别在于信号不再手工填,而是由 CFractalsSignal 类从 CiFractals 指标直接读,顺带能在长历史里回测,不用人工造输入。 类里用 ENUM_TIMEFRAMES 存分析周期,和 EA 运行周期解耦;指标交互交给终端自带的 CiFractals 指针,不必重写分形算法。买卖信号各存一份 TradeInputs 结构,因为上下分形谁先被触没法预知,得同时盯着。 构造时把周期锁在 PERIOD_D1——日线波动够大、出形比周月线频繁,给日内盈利留出空间。Upper() 取分形值传索引 3,即前天的前一天,避开当天刚画又消失的假突破。FrUp 不等于 EMPTY_VALUE 才更新 price,空值不覆写,保证结构里永远是有效分形价。 买入判定三条件:fract_Up.price 初值非零、ask >= fract_Up.price、且该分形当天未被标记 done。日内持仓不加仓,所以同一分形每天只发一次信号。Broker 回执成功后才调 BuyDone() 把 done 置 true。卖出对称,比的是 bid < fract_Dn.price。 Process() 每个 tick 刷一次指标,其实可改为新日线柱事件触发更省资源,但实测额外负担有限就先留着。外汇与贵金属杠杆高,这类信号在历史中胜率随品种和时段波动,实盘前务必在 MT5 策略测试器跑通再上。

MQL5 / C++
ENUM_TIMEFRAMES   TF;                                                     class=class="str">"cmt">// timeframe used
   CiFractals        *cFractals;                                              class=class="str">"cmt">// fractals
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">// symbol
   class="type">ENUM_POSITION_TYPE direction;                                                                             class=class="str">"cmt">// direction
   class="type">class="kw">double            price;                                                                                  class=class="str">"cmt">// price
   class="type">class="kw">datetime          tradedate;                                                                              class=class="str">"cmt">// date
   class="type">bool              done;                                                                                   class=class="str">"cmt">// trigger flag
  };
   TradeInputs       fract_Up, fract_Dn;   class=class="str">"cmt">// current signal

「把分形信号封装成可复用类」

在 MT5 里做多周期信号耦合,最怕把分形取值散落在 EA 主逻辑里。把上下分形和买卖判定收进一个 CFractalsSignal 类,后面接别的过滤器时只需调 BuySignal() / SellSignal(),主循环能干净不少。 类里 protected 区挂了 ENUM_TIMEFRAMES TF 和 CiFractals* cFractals 两个核心成员:前者决定取哪根周期的裸分形,后者是标准库的分形指标实例。FrUp、FrDn 两个 double 存最近一次识别到的上/下分形价格,fract_Up、fract_Dn 则记录当前触发状态。 构造函数默认把 TF 写死成 PERIOD_D1,也就是说这个类出厂即看日线分形。若你要做 XAUUSD 的 4H 突破回踩,得把 PERIOD_D1 改成 PERIOD_H4 再编译,否则信号周期不对,黄金的高波动特性会让日线分形滞后非常明显。 new CiFractals() 之后必须过 CheckPointer 判空,并且 cFractals.Create(Symbol(),TF) 要返回 true 才算绑定成功。外汇和贵金属杠杆高、滑点跳空频繁,指标实例创建失败若不被捕获,实盘可能静默漏信号。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CFractalsSignal                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFractalsSignal
  {
class="kw">protected:
   ENUM_TIMEFRAMES    TF;                  class=class="str">"cmt">// 使用的图表周期
   CiFractals        *cFractals;           class=class="str">"cmt">// 分形指标实例
   TradeInputs        fract_Up, fract_Dn;  class=class="str">"cmt">// 当前买卖信号状态
   class="type">class="kw">double             FrUp;                class=class="str">"cmt">// 上方分形价格
   class="type">class="kw">double             FrDn;                class=class="str">"cmt">// 下方分形价格
class="kw">public:
                     CFractalsSignal(class="type">void); class=class="str">"cmt">// 构造函数
                    ~CFractalsSignal(class="type">void); class=class="str">"cmt">// 析构函数
   class="type">void              Process();            class=class="str">"cmt">// 启动更新的方法
   class="type">bool              BuySignal();          class=class="str">"cmt">// 买入信号
   class="type">bool              SellSignal();         class=class="str">"cmt">// 卖出信号
   class="type">void              BuyDone();            class=class="str">"cmt">// 买入完成
   class="type">void              SellDone();           class=class="str">"cmt">// 卖出完成
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CFractalsSignal                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CFractalsSignal::CFractalsSignal(class="type">void)
  {
   TF  =  PERIOD_D1;                       class=class="str">"cmt">// 使用的图表周期
class=class="str">"cmt">//--- fractal class
   cFractals=new CiFractals();             class=class="str">"cmt">// 创建分形指标实例
   if(CheckPointer(cFractals)==POINTER_INVALID ||   class=class="str">"cmt">// 若实例未创建 或
      !cFractals.Create(Symbol(),TF))       class=class="str">"cmt">// 变体未创建

分形信号类的初始化与回收细节

在自定义分形信号类里,初始化失败时要立刻打印 INIT_FAILED 并中止后续流程,避免空指针读写。紧接着对分形缓冲区做 BufferResize(4),把容量定为 4 根 K 线,再调 Refresh() 拉一次最新数据。 起始状态把 FrUp、FrDn 都置为 EMPTY_VALUE,上下分形价位先留空;fract_Up 与 fract_Dn 的 done 标记设 true、price 设 EMPTY_VALUE,相当于告诉主循环“这一轮还没拿到有效分形”。 析构函数里用 CheckPointer(cFractals)!=POINTER_INVALID 判断实例是否真被建出来,只有合法才 delete,防止重复释放炸 EA。Process() 每次被调也先 Refresh() 再算,保证分形数组不是上一 tick 的残影。 外汇与贵金属波动跳空频繁,这类缓冲区若忘了 Resize 或析构漏删,MT5 日志里可能反复报对象泄漏,实盘前务必在策略测试器跑至少 200 根历史棒验证。

MQL5 / C++
Print("INIT_FAILED");                                                     class=class="str">"cmt">// don&class="macro">#x27;t proceed
   cFractals.BufferResize(class="num">4);                                                    class=class="str">"cmt">// resize fractal buffer
   cFractals.Refresh();                                                          class=class="str">"cmt">// update
class=class="str">"cmt">//---
   FrUp = EMPTY_VALUE;                                                           class=class="str">"cmt">// leveled upper at start
   FrDn = EMPTY_VALUE;                                                           class=class="str">"cmt">// leveled lower at start
   fract_Up.done   = true;                                                       class=class="str">"cmt">//
   fract_Up.price = EMPTY_VALUE;                                                 class=class="str">"cmt">//
   fract_Dn.done   = true;                                                       class=class="str">"cmt">//
   fract_Dn.price = EMPTY_VALUE;                                                 class=class="str">"cmt">//
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ~CFractalsSignal                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CFractalsSignal::~CFractalsSignal(class="type">void)
   {
class=class="str">"cmt">//---
   if(CheckPointer(cFractals)!=POINTER_INVALID)                                 class=class="str">"cmt">// if instance was created,
      class="kw">delete cFractals;                                                          class=class="str">"cmt">// class="kw">delete
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  Process                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFractalsSignal::Process(class="type">void)
   {
class=class="str">"cmt">//---
   cFractals.Refresh();                                                          class=class="str">"cmt">// update fractals

◍ 上破分形时的买单信号判定

在 MT5 里用分形做突破信号,核心是先拿到实时卖一价,再比对上方分形位。下面这段逻辑直接取当前品种 SYMBOL_ASK,并调用 cFractals.Upper(3) 读取第三根 K 线的上方分形值。 判定条件分两层:第一层检查分形值是否更新且非 EMPTY_VALUE,避免拿到无效数据;第二层才看 ask 是否大于等于分形价,同时 fract_Up.done 仍为 false,确保同一分形只触发一次。 满足条件即 return true,交给后续下单模块处理。外汇与贵金属杠杆高,分形假突破概率不低,实盘前务必在策略测试器用至少 3 个月 tick 数据回测。 代码逐行拆解: // 取当前品种卖一价(买单成交价) double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); // 取第三根 K 线的上方分形值 FrUp=cFractals.Upper(3); // 若分形值变化且不是空值,说明有新分形 if(FrUp != fract_Up.price && FrUp != EMPTY_VALUE) // 若旧分形有效、卖一价≥分形、且未处理过 if(fract_Up.price != EMPTY_VALUE && ask >= fract_Up.price && fract_Up.done == false) // 返回 true 生成信号 return true;

MQL5 / C++
class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);              class=class="str">"cmt">// Buy price
FrUp=cFractals.Upper(class="num">3);                                            class=class="str">"cmt">// request the current value
if(FrUp != fract_Up.price        &&                                 class=class="str">"cmt">// if the data has been updated
   FrUp != EMPTY_VALUE)                                             class=class="str">"cmt">// skip empty value
if(fract_Up.price != EMPTY_VALUE   &&                               class=class="str">"cmt">// skip zero values
   ask                 >= fract_Up.price &&                         class=class="str">"cmt">// if the buy price is greater than or equal to the fractal
   fract_Up.done     == false)                                      class=class="str">"cmt">// the signal has not been processed yet
   {
      class="kw">return true;                                                  class=class="str">"cmt">// generate a signal to process
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                    BuySignal                                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFractalsSignal::BuySignal(class="type">void)
  {
  class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);               class=class="str">"cmt">// Buy price
class=class="str">"cmt">//--- check fractals update
  FrUp=cFractals.Upper(class="num">3);                                          class=class="str">"cmt">// request the current value
  if(FrUp != fract_Up.price        &&                               class=class="str">"cmt">// if the data has been updated
     FrUp != EMPTY_VALUE)                                           class=class="str">"cmt">// skip empty value
    {
     fract_Up.price = FrUp;                                         class=class="str">"cmt">// process the new fractal

「向上分形触发后的信号锁与向下分形读取」

向上分形的成交确认,靠的是一个布尔锁。fract_Up.done 在被 BuyDone() 置为 true 之前,BuySignal() 每次 tick 都会重新评估,避免同一分形被反复发单。 BuySignal() 的核心判断只有三条件并列:分形价不是 EMPTY_VALUE、ask 大于等于分形价、且 done 仍为 false。三者同时满足才 return true,否则直接 false,逻辑极薄但够用。 向下方向走的是另一套:SellSignal() 先取 SYMBOL_BID,再用 cFractals.Lower(3) 抓第 3 根 K 线以下的向下分形。这里 Lower(3) 的参数是偏移量,实战里改这个数就能控制回看深度,外汇与贵金属波动大,参数乱调可能放大假信号风险。 把两段拼起来看,类里用 done 标志位隔离已处理信号,是防止 EA 在 MT5 历史回测里重复触发的常见写法。开 MT5 把 Lower(3) 改成 Lower(5) 跑一遍 EURUSD 的 M15,能直接看到触发频率的变化。

MQL5 / C++
fract_Up.done = false;                     class=class="str">"cmt">// not processed
class=class="str">"cmt">//--- check the signal
if(fract_Up.price != EMPTY_VALUE      class=class="str">"cmt">// skip zero values
   ask              >= fract_Up.price &&    class=class="str">"cmt">// if the buy price is greater than or equal to the fractal
   fract_Up.done   == false)                class=class="str">"cmt">// the signal has not been processed yet
   {
   class="kw">return true;                             class=class="str">"cmt">// generate a signal to process
   }
class="kw">return false;                              class=class="str">"cmt">// otherwise false
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                  BuyDone                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFractalsSignal::BuyDone(class="type">void)
  {
  fract_Up.done = true;                     class=class="str">"cmt">// processed
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                  SellSignal                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFractalsSignal::SellSignal(class="type">void)
  {
  class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);   class=class="str">"cmt">// bid price
class=class="str">"cmt">//--- check fractals update
  FrDn=cFractals.Lower(class="num">3);                            class=class="str">"cmt">// request the current value
  if(FrDn != EMPTY_VALUE                             class=class="str">"cmt">// skip empty value

下破分形后的卖出信号判定

这段逻辑只做一件事:当下行分形(fract_Dn)的价格被刷新,且当前 bid 价小于等于该分形位、且这个信号还没被处理过,就返回 true 触发卖出信号。外汇与贵金属杠杆高,信号失败概率不低,实盘须自行验证。 代码里先比对 FrDn 与已存 fract_Dn.price,不等说明分形数据更新了,于是写入新价格并把 done 标为 false,代表待处理。随后用 bid <= fract_Dn.price 做穿透判断,三者同时满足才 return true。 SellDone() 函数极简,仅把 fract_Dn.done 置 true,作用是通知系统该信号已成交或已忽略,避免同一条分形在下一 tick 重复发信号。开 MT5 把这段嵌进 EA,用 EURUSD 的 M15 跑一周,观察重复信号次数是否归零。

MQL5 / C++
   FrDn != fract_Dn.price)                                                                      class=class="str">"cmt">// if the data has been updated
   {
   fract_Dn.price = FrDn;                                                                       class=class="str">"cmt">// process the new fractal
   fract_Dn.done = false;                                                                       class=class="str">"cmt">// not processed
   }
class=class="str">"cmt">//--- check the signal
   if(fract_Dn.price != EMPTY_VALUE     &&                                                   class=class="str">"cmt">// skip empty value
      bid                <= fract_Dn.price &&                                                class=class="str">"cmt">// if the ask price is less than or equal to the fractal AND
      fract_Dn.done   == false)                                                               class=class="str">"cmt">// signal has not been processed
   {
      class="kw">return true;                                                                            class=class="str">"cmt">// generate a signal to process
   }
   class="kw">return false;                                                                               class=class="str">"cmt">// otherwise false
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  SellDone                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFractalsSignal::SellDone(class="type">void)
  {
   fract_Dn.done = true;                                                                       class=class="str">"cmt">// processed
  }

◍ 把风险管理器拼进EA并跑历史回测

集成阶段用 #include 把前面写好的 RiskManagerAlgo.mqh、TradeModel.mqh、CFractalsSignal.mqh 三个自定义类,以及终端标准的 Indicators\BillWilliams.mqh 和 Trade\Trade.mqh 一并挂到主文件头部。新增一个 int 型输入参数 inp_sl_in_int 让用户自己填每笔交易可接受的最小止损点数;正式做全自动系统时,更合理的做法是让止损类或波动率类回算这个值,而不是暴露给用户输入。 指针声明和 OnInit 里的初始化是关键:CTrade 对象用 SetTypeFillingBySymbol(Symbol()) 绑定当前品种,SetDeviationInPoints 写死一个偏大值(不做成输入),再给所有仓位打统一 magic。析构函数里判空后 delete 指针清内存,避免 MT5 EA 反复加载卸载时漏内存。 OnTick 主干先调 RiskManagerBase 的 ContoMonitor() 做风控监控,再跑 SlippageCheck()。本例分形信号类只做演示、不设止损、靠 23:00 强平收尾,所以 SlippageCheck 会放行所有单;真要上线,必须等止损非零才发单给经纪商。 买入检查用 CFractalsSignal.BuySignal() 出信号,且 SpreadMonitor(inp_sl_in_int) 点差合规才以 0.1 标准手下单;Buy() 成功立即 BuyDone() 锁信号,失败写日志留待重试。卖出对称处理,尾盘平仓块直接复用前文代码,想持隔夜就注释掉。 回测用 USDJPY、M15、2023 全年逐 tick、无延迟、1:100 杠杆、1万美金起步。优化只动 inp_riskperday(0.1→1,步0.5)、inp_riskperweek(0.5→3)、inp_riskpermonth(2→8)、inp_plandayprofit(0.1→3)、dayProfitControl(false→true),滑点与点差类参数交用户自调。 最优迭代落在 inp_riskperday=0.6、inp_plandayprofit=3.1:计划盈利是风险成本的 5 倍,即冒 0.6% 本金亏损博 3.1% 收益,分形突破日线形态存在正向动量。最差迭代 inp_riskperday=1.1、inp_plandayprofit=0.1,风险没对准动量,资金曲线大幅回撤。外汇与贵金属属高风险,历史优化不代表未来,开 MT5 用表 3 参数自测 USDJPY 才能验证这套风控是否对你有效。

MQL5 / C++
class="macro">#include <RiskManagerAlgo.mqh>
class="macro">#include <Indicators\BillWilliams.mqh>
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <TradeModel.mqh>
class="macro">#include <CFractalsSignal.mqh>
input group "RiskManagerAlgoExpert"
input class="type">int inp_sl_in_int       = class="num">2000;   class=class="str">"cmt">// inp_sl_in_int - a stop loss level for a separate trade
RiskManagerAlgo *RMA;                                                                         class=class="str">"cmt">// risk manager
CTrade          *cTrade;                                                                       class=class="str">"cmt">// trade

「正文」

CFractalsSignal *cFract;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// fractal</span> <span class="comment">//+------------------------------------------------------------------+</span>

<span class="comment">//Expert initialization function&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>

<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">int</span> <span class="functions">OnInit</span>() &nbsp;&nbsp;{ <span class="comment">//---</span> &nbsp;&nbsp; RMA = <span class="keyword">new</span> RiskManagerAlgo();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// algorithmic risk manager</span> <span class="comment">//---</span> &nbsp;&nbsp; cFract =<span class="keyword">new</span> CFractalsSignal();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nb

信号触发前的指针回收与下单链路

在每根 K 线或每次 tick 重算前,先对上轮实例做指针有效性判断再 delete,是避免 MT5 EA 内存泄漏的硬动作。下面这段先把 cFract 与 RMA 两个对象按 CheckPointer 结果清理,再重新拉起风控与分形处理。 if(CheckPointer(cFract)!=POINTER_INVALID) // 若 cFract 实例仍存在 { delete cFract; // 释放旧分形对象 } if(CheckPointer(RMA)!=POINTER_INVALID) // 若 RMA 实例仍存在 { delete RMA; // 释放旧风控对象 } RMA.ContoMonitor(); // 启动资金监控 RMA.SlippageCheck(); // 滑点核查 cFract.Process(); // 跑分形识别 真正的交易触发被压在双重条件后:cFract.BuySignal() 给出分形买点,且 RMA.SpreadMonitor(inp_sl_in_int) 确认点差在容忍带内。此时 cTrade.Buy(0.1) 以 0.1 手试单,成交后 BuyDone() 回写状态并打印 "Buy has been done",失败则打印 "Error: buy"。 外汇与贵金属杠杆品种跳空频繁,这套链路里滑点核查不通过就直接不下单,实盘里能砍掉一批假突破损耗。开 MT5 把 inp_sl_in_int 设成你经纪商常态点差上限,回看最近 200 根 H1 就能看出过滤效果。

MQL5 / C++
if(CheckPointer(cFract)!=POINTER_INVALID)                      class=class="str">"cmt">// if an instance is found,
   {
   class="kw">delete cFract;                                                        class=class="str">"cmt">// class="kw">delete
   }
class=class="str">"cmt">//---
if(CheckPointer(RMA)!=POINTER_INVALID)                          class=class="str">"cmt">// if an instance is found,
   {
   class="kw">delete RMA;                                                          class=class="str">"cmt">// class="kw">delete
   }
  }
  RMA.ContoMonitor();                                                class=class="str">"cmt">// run the risk manager
  RMA.SlippageCheck();                                               class=class="str">"cmt">// check slippage
  cFract.Process();                                                  class=class="str">"cmt">// start the fractal process
  if(cFract.BuySignal() &&
     RMA.SpreadMonitor(inp_sl_in_int))                               class=class="str">"cmt">// if there is a buy signal
    if(cTrade.Buy(class="num">0.1))                                              class=class="str">"cmt">// if Buy executed,
     {
     cFract.BuyDone();                                               class=class="str">"cmt">// the signal has been processed
     Print("Buy has been done");                                     class=class="str">"cmt">// notify
     }
    else                                                             class=class="str">"cmt">// if Buy not executed,
     {
     Print("Error: buy");                                            class=class="str">"cmt">// notify
     }

◍ 用时间结构与OnTick串起收盘与信号

在MT5 EA里,先抓当前时间再决定动作,是控制隔夜风险最直接的办法。下面这段把 MqlDateTime 结构和 OnTick 主循环拼在一起,逻辑不复杂但少一步就会漏单。 MqlDateTime time_curr; // 声明当前时间结构 TimeCurrent(time_curr); // 向终端请求当前时间并写入结构 if(time_curr.hour >= 23) // 若小时数已到23点及以后 { RMA.AllOrdersClose(); // 调用风险管理器平掉所有持仓 } Comment(RMA.Message()); // 把风险管理器状态打印到图表左上角 上面这段放在 OnTick 之外或之前做环境准备,真正每跳执行的是 OnTick 函数。它依次跑风险监控、滑点检查、分形处理,再判断买信号与点差条件。 void OnTick() { RMA.ContoMonitor(); // 运行账户风险监控 RMA.SlippageCheck(); // 检查当前滑点是否越界 cFract.Process(); // 启动分形计算流程 if(cFract.BuySignal() && RMA.SpreadMonitor(inp_sl_in_int)) // 分形给买信号且点差监控通过 { if(cTrade.Buy(0.1)) // 以0.1手尝试买入 { cFract.BuyDone(); // 标记该信号已成交处理 Print("Buy has been done"); // 向专家日志输出通知 } } } 实盘里把平仓阈值卡在 hour>=23,意味着每天23:00(服务器时区)后不再留仓,外汇与贵金属杠杆品种跳空概率可能抬升,这一刀切能回避部分隔夜缺口。开MT5把这段代码塞进自己的EA模板,改 inp_sl_in_int 那个点差上限参数,就能看信号过滤松紧对成交频率的影响。

MQL5 / C++
class="type">MqlDateTime time_curr;                                                                     class=class="str">"cmt">// current time structure
   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
     {
      RMA.AllOrdersClose();                                                                   class=class="str">"cmt">// close all positions
     }
   Comment(RMA.Message());                                                                    class=class="str">"cmt">// display the data state in a comment
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   RMA.ContoMonitor();                                                                        class=class="str">"cmt">// run the risk manager
   RMA.SlippageCheck();                                                                       class=class="str">"cmt">// check slippage
   cFract.Process();                                                                          class=class="str">"cmt">// start the fractal process
   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                                       class=class="str">"cmt">// if there is a buy signal
     {
      if(cTrade.Buy(class="num">0.1))                                                                    class=class="str">"cmt">// if Buy executed,
        {
         cFract.BuyDone();                                                                   class=class="str">"cmt">// the signal has been processed
         Print("Buy has been done");                                                         class=class="str">"cmt">// notify
        }
     }
  }

「信号触发后的下单与收盘逻辑」

这段片段承接分形信号判定的后续动作:当 cFract.BuySignal() 返回真,尝试以 0.1 手买入;成交后调用 BuyDone() 标记信号已处理并打印 "Buy has been done",失败则打印 "Error: buy"。卖单分支完全对称,cFract.SellSignal() 为真时以 0.1 手卖出,成交调 SellDone() 并打印 "Sell has been done",失败打印 "Error: sell"。 时间控制上,用 TimeCurrent() 取当前时间结构,若 time_curr.hour >= 23 就调 RMA.AllOrdersClose() 强平所有持仓,避免隔夜跳空的外汇/贵金属高风险暴露。 循环末尾用 Comment(RMA.Message()) 把风险管理器状态直接刷在图表左上角,方便肉眼盯盘。下面逐行拆一遍核心代码。 别把 23 点平仓当万能钥匙 不同品种流动性在美盘尾段骤降,硬平可能滑点放大;黄金和外汇直盘在该时点点差可能扩张 2~3 倍,建议先在策略测试器跑一周 tick 数据看成交偏差。

MQL5 / C++
      else                                                                      class=class="str">"cmt">// if Buy not executed,
      {
       Print("Error: buy");                                                      class=class="str">"cmt">// notify
      }
   }
  if(cFract.SellSignal())                                                        class=class="str">"cmt">// if there is a sell signal
   {
     if(cTrade.Sell(class="num">0.1))                                                        class=class="str">"cmt">// if sell executed,
      {
       cFract.SellDone();                                                        class=class="str">"cmt">// the signal has been processed
       Print("Sell has been done");                                              class=class="str">"cmt">// notify
      }
     else                                                                        class=class="str">"cmt">// if sell failed,
      {
       Print("Error: sell");                                                     class=class="str">"cmt">// notify
      }
   }
  class="type">MqlDateTime time_curr;                                                          class=class="str">"cmt">// current time structure
  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
   {
   RMA.AllOrdersClose();                                                          class=class="str">"cmt">// close all positions
   }
  Comment(RMA.Message());                                                         class=class="str">"cmt">// display the data state in a comment
  }

画得少,看得清

把前面十二节拆开的模型、信号与风控拼回来,结论其实只有一条:策略赚不赚钱,先看风险有没有被标准化。原文给的 RiskManagerAlgo.mq5 约 10.53 KB、RiskManagerBase.mqh 约 61.49 KB,这些文件就是把「每仓止损 + 风险归一」落进代码的载体,而不是又一套圣杯。 外汇和贵金属杠杆高,黑天鹅一来,没挂止损的盈利策略也可能在几根 K 线内变亏损;反过来,同等仓位下用风险管理器约束敞口,亏损策略的回撤倾向会被压住,存活概率更高。 真要验证,就开 MT5 把 ZIP 里的 mqh 拖进 Include,加载 RiskManagerAlgo 跑历史品种,看权益曲线是不是比裸信号平。少盯几个指标,把风险开关焊死在每笔单子上,比到处找新策略实在。

交给小布盯盘看回撤
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时回撤与滑点异常提示,把重复劳动交给小布,你专注决策。

常见问题

算数止损依赖波动计算值、不挂钩图表形态;技术形态止损基于K线高低点或分型。类中建议用不同方法分支或独立校验函数处理。
需设置最小距离校验与拒单回退逻辑,避免 broker 拒绝或异常成交,通常在开仓前接口里先拦截。
可以,把类的回撤、滑点、止损触发状态按品种页字段暴露,小布盯盘即可可视化预警,省去手动翻日志。
封装逻辑后 Getter 只返回经校验的值,防止外部误改止损计算基准,保证类内风险规则不被绕过。
优先查交易模块开仓前点差过滤与接口传参,确认测试环境 tick 模拟覆盖了极端点差分支。