EA 交易运行期间平衡曲线斜率的控制(基础篇)
📘

EA 交易运行期间平衡曲线斜率的控制(基础篇)

第 1/3 篇

用斜率给 EA 平衡曲线踩刹车

EA 跑起来以后,账户余额曲线不是直线上升就健康,斜率太陡往往意味着仓位过重或追单频繁。在 MT5 里我们可以用一段简单代码实时读取权益变化,间接估出曲线坡度,超过阈值就降速。 以下片段放在 OnTick 里,用上根 K 线收盘权益和当前权益做差分,再除以时间秒数,得到近似斜率。注意这只是代理指标,真实订单盈亏还受点差和滑点扰动。 外汇与贵金属杠杆高,曲线斜率失控可能数小时内回吐大段利润,务必先在策略测试器用历史数据验证再上实盘。

MQL5 / C++
class="type">class="kw">double prevEquity = class="num">0;
class="type">class="kw">double GetSlope()
{
   class="type">class="kw">double cur = AccountInfoDouble(ACCOUNT_EQUITY);
   class="type">class="kw">double dt = (class="type">class="kw">double)(TimeCurrent() - prevTime) / class="num">60.0;
   if(dt <= class="num">0) class="kw">return class="num">0;
   class="type">class="kw">double slope = (cur - prevEquity) / dt;
   prevEquity = cur;
   class="kw">return slope;
}

◍ 用权益曲线斜率给 EA 自动踩刹车

一种可行的 EA 性能改善思路,是拿账户权益(平衡)曲线的斜率当反馈信号:斜率掉头向下,说明当前节奏在亏钱,系统就自动削减交易量;斜率回正,再逐步把风险恢复到初始手数。原文给出的工作模式分为三类——削减交易量、按初始设定运行的有效手数、以及两者之间的中间交易量,切换完全自动。 反馈链里提供了三种调节特性:步进、带延迟的步进、线性。它们决定斜率控制系统的响应方式,你可以按自己 EA 的特性选一种去匹配,而不是套用统一参数。 需要泼盆冷水:这套机制不是万能药,它不会把一个原本亏损的 EA 变成盈利 EA。它本质上是资金管理的补充,作用是在账户出现连续不利时压低敞口,避免单账户过大回撤。外汇与贵金属杠杆高,这类自动降险逻辑也只是降低概率性风险,不消除爆仓可能。 作者随文给了一个库,能直接把这套斜率反馈函数嵌进任意 EA 代码里,你想验证只需把库挂上自己跑过的 EA,观察权益曲线陡降段是否被自动减仓。

「用平衡曲线坡度反推仓位缩放」

先看一条用固定手数跑出来的平衡曲线:每笔平仓用红点标出,连成黑粗线就是权益轨迹。系统在每笔新单开盘前,取最近两笔已平仓交易算出这段连线的斜率(图中蓝细线示意),把它当成当前盈利动能的代理指标。 当斜率掉到阈值以下,控仓模块介入,按预设调节函数把手数从 Vmax 往 Vmin 压。图 1 里 T3 到 T5 就是缩量区,过了 T5 若坡度仍不达标,就切到最小手数拒绝模式(绿色段),直到 T8 附近坡度回到阈值之上才重新放量,T10 后恢复 Vmax。下半图显示:原本 B1→B2 的回撤被改成 B1→B2*,幅度明显收窄,代价是 T8–T10 恢复期的利润比原曲线略低。 调节特性有三种可切换形态:步进(离散步进缩放)、线性(随坡度连续变)、带延迟步进(扩量与缩量在不同坡度触发)。图 2 给出对照,具体选哪个得跑实验挑回测最优。黄色段代表最大↔最小手数的过渡区。 别把正态当圣经 这种坡度反馈只适合手数本身不参与系统逻辑的策略。马丁格尔类 EA 不能直接套,否则原系统逻辑被破坏。控仓效力取决于正常手数与拒绝模式手数的比率——比率越大,削峰填谷效果越强,所以 Vmax 要显著大于 Vmin。 另一个硬约束:EA 平衡涨跌的平均周期必须远大于控仓系统的反应时间,否则斜率调节根本来不及生效。反应时间相对周期的比值越大,系统越稳。外汇与贵金属杠杆高、滑点跳空频发,实盘前务必在 MT5 策略测试器里用历史数据验证这套缩放逻辑是否真能降回撤。

用面向对象给 MQL5 库留扩展口

MQL5 从 build 1570 起稳定支持 class / struct 的面向对象写法,把信号计算、订单路由这些逻辑封进类里,后续加新品种或换策略只改派生类,不动核心基类。 对做贵金属和外汇的人来说,这类库最实在的好处是:同一套风控基类可以挂 XAUUSD 和 EURUSD 两个品种实例,参数各自独立,避免复制粘贴出的隐性 bug。 外汇与贵金属杠杆高、滑点跳空频繁,任何库在上线前都应在 MT5 策略测试器用至少 3 个月 tick 数据跑一遍,验证对象生命周期里没有未释放的持仓引用。

◍ 把多品种封装进一个交易品种类

MT5 已经支持多货币回测,但要在 EA 里同时跑多个品种,就得把每个品种的市场信息获取和规范化操作单独封起来。TradeSymbol 类就是干这个的:它不直接参与交易决策,只是个辅助壳,让库能在多货币环境下按指定品种取数、存数、算数。 类的结构很薄。核心就三个对外方法:RefreshSymbolInfo 按品种刷新市场快照,NormalizeLots 把想法中的手数压到经纪商允许的标准化区间,NormalizePrice 把价格截到对应品种的小数位和最小跳动。 注意一个坑:当前 MQL5 不允许带参构造函数,所以对象建完后必须显式调用一次初始化方法设定品种,否则后续取数全是空。 NormalizeLots 会参考 SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAX 和最小步进,返回最接近底值的手数;若超上限则截断。NormalizePrice 则按 symbol_digits 和 tick_size 处理——期货类品种最小变动常大于 1 点,价格必须是最小离散值的整数倍。 下面这段是类声明原文,私有段存了点值、最小/最大交易量等 7 个字段,公开段暴露了刷新与两个规范化接口: 别拿构造函数当初始化 MQL5 无参构造的限制意味着多品种 EA 里每切一个 symbol 都得手动 RefreshSymbolInfo,漏掉一次,后续 Normalize 拿到的是上上个品种的精度,订单可能直接被服务器拒。

MQL5 / C++
class TradeSymbol
{
class="kw">private:
  class="type">class="kw">string  trade_symbol;                 class=class="str">"cmt">// 工作交易品种
class="kw">private:
  class="type">class="kw">double  min_trade_volume;             class=class="str">"cmt">// 交易操作的最小交易量
  class="type">class="kw">double  max_trade_volume;             class=class="str">"cmt">// 交易操作的最大交易量
  class="type">class="kw">double  min_trade_volume_step;        class=class="str">"cmt">// 最小交易量增量
  class="type">class="kw">double  max_total_volume;             class=class="str">"cmt">// 最大交易量增量
  class="type">class="kw">double  symbol_point;                 class=class="str">"cmt">// 点数大小
  class="type">class="kw">double  symbol_tick_size;            class=class="str">"cmt">// 最小价格增量
  class="type">int     symbol_digits;                class=class="str">"cmt">// 小数点之后位数
class="kw">protected:
class="kw">public:
  class="type">void    RefreshSymbolInfo( );         class=class="str">"cmt">// 刷新工作交易品种的市场情报
  class="type">void    SetTradeSymbol( class="type">class="kw">string _symbol ); class=class="str">"cmt">// 设置/改变 工作交易品种
  class="type">class="kw">string  GetTradeSymbol( );            class=class="str">"cmt">// 获取工作交易品种
  class="type">class="kw">double  GetMaxTotalLots( );           class=class="str">"cmt">// 获得最大累积量
class="type">class="kw">double  GetPoints( class="type">class="kw">double _delta );     class=class="str">"cmt">// 得到价格变化点数
class="kw">public:
  class="type">class="kw">double  NormalizeLots( class="type">class="kw">double _requied_lot ); class=class="str">"cmt">// 得到归一化交易量
  class="type">class="kw">double  NormalizePrice( class="type">class="kw">double _org_price );  class=class="str">"cmt">// 得到归一化的价格,同时考虑报价的逐步变化
class="kw">public:
  class="type">void    TradeSymbol( );               class=class="str">"cmt">// 构造器
  class="type">void    ~TradeSymbol( );              class=class="str">"cmt">// 析构器
};

「把手数价格压回经纪商能成交的刻度」

MT5 里直接拿策略算出的手数和价格去下单,大概率被拒。每个品种都有自己的 SYMBOL_VOLUME_MAX、SYMBOL_VOLUME_STEP、SYMBOL_VOLUME_LIMIT,还有 SYMBOL_POINT 和 SYMBOL_TRADE_TICK_SIZE,不按这些刻度规整,订单会被终端弹回。 NormalizeLots 先判断工作品种有没有设,没设直接返 0.0 避免乱下。有 step 时用 1/step 取倒数再 MathLog10 算小数位,没 step 就退而用 min_trade_volume 且锁 2 位;MathFloor 把请求手数截到 step 整数倍,再夹在最小和最大交易量之间,最后 NormalizeDouble 收尾。 NormalizePrice 更绕一点:先用 tick_size / point 算出每跳对应几个 point,把原始价除以 point 取整、再除以跳数取整、乘回跳数和 point,最后按品种小数位规整。这样挂单价格才落在经纪商报价网格上。 外汇和贵金属杠杆高、点差跳变频繁,规整逻辑写错可能在大行情时把单子卡在无效价位。把下面两段直接塞进你的 TradeSymbol 类,开 MT5 用 Print(NormalizeLots(0.12345)) 和 Print(NormalizePrice(1.234567)) 验证输出是否贴合品种限制。

MQL5 / C++
  max_trade_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_MAX );
  min_trade_volume_step = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_STEP );
  max_total_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_LIMIT );
  symbol_point = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_POINT );
  symbol_tick_size = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_TRADE_TICK_SIZE );
  symbol_digits = ( class="type">int )SymbolInfoInteger( GetTradeSymbol( ), SYMBOL_DIGITS );
}
class="type">void      SetTradeSymbol( class="type">class="kw">string _symbol );      class=class="str">"cmt">// 设置/改变 工作交易品种
class=class="str">"cmt">//-----------------------------------------------------
class=class="str">"cmt">//  获得标准化的交易量:
class=class="str">"cmt">//-----------------------------------------------------
class=class="str">"cmt">//  - 输入必要的交易量;
class=class="str">"cmt">//  - 输出标准化交易量;
class=class="str">"cmt">//-----------------------------------------------------
class="type">class="kw">double
TradeSymbol::NormalizeLots( class="type">class="kw">double _requied_lots )
{
  class="type">class="kw">double   lots, koeff;
  class="type">int      nmbr;
class=class="str">"cmt">//  如果工作交易品种未设置, 不做任何事:
  if( GetTradeSymbol( ) == NULL )
  {
    class="kw">return( class="num">0.0 );
  }
  if( this.min_trade_volume_step > class="num">0.0 )
  {
    koeff = class="num">1.0 / min_trade_volume_step;
    nmbr = ( class="type">int )MathLog10( koeff );
  }
  else
  {
    koeff = class="num">1.0 / min_trade_volume;
    nmbr = class="num">2;
  }
  lots = MathFloor( _requied_lots * koeff ) / koeff;
class=class="str">"cmt">//  交易量下限:
  if( lots < min_trade_volume )
  {
    lots = min_trade_volume;
  }
class=class="str">"cmt">//  交易量上限:
  if( lots > max_trade_volume )
  {
    lots = max_trade_volume;
  }
  lots = NormalizeDouble( lots, nmbr );
  class="kw">return( lots );
}
class=class="str">"cmt">//-----------------------------------------------------
class=class="str">"cmt">//  考虑价格改变步骤的正常价格:
class=class="str">"cmt">//-----------------------------------------------------
class="type">class="kw">double
TradeSymbol::NormalizePrice( class="type">class="kw">double _org_price )
{
class=class="str">"cmt">//  每步报价的最小点数:
  class="type">class="kw">double   min_price_step = NormalizeDouble( symbol_tick_size / symbol_point, class="num">0 );
  class="type">class="kw">double   norm_price = NormalizeDouble( NormalizeDouble(( NormalizeDouble( _org_price / symbol_point, class="num">0 ) ) / min_price_step, class="num">0 ) * min_price_step * symbol_point, symbol_digits );
  class="kw">return( norm_price );
}

常见问题

实时算最近N根平衡曲线点的斜率,超过阈值就把下单单手数乘个系数缩小,曲线回平缓再恢复,外汇贵金属高风险需留足缓冲。
没有统一标准,常见用最近20根点的线性回归斜率,绝对值超账户余额0.5%~1%每根就触发缩仓,具体看品种波动。
可以,小布盯盘能按品种页展示曲线坡度诊断,斜率越界时直接推提醒,你再决定缩手数或暂停,不用自己写监控。
用 NormalizeDouble(val, digits) 按品种成交刻度四舍五入,再和最小手数、步长取模对齐,保证发单不被拒。
封装一个交易品种类,把平衡曲线、斜率、手数计算都塞进去,主循环只调对象方法,后续加品种只new一个实例。