EA 交易运行期间平衡曲线斜率的控制(基础篇)
用斜率给 EA 平衡曲线踩刹车
EA 跑起来以后,账户余额曲线不是直线上升就健康,斜率太陡往往意味着仓位过重或追单频繁。在 MT5 里我们可以用一段简单代码实时读取权益变化,间接估出曲线坡度,超过阈值就降速。 以下片段放在 OnTick 里,用上根 K 线收盘权益和当前权益做差分,再除以时间秒数,得到近似斜率。注意这只是代理指标,真实订单盈亏还受点差和滑点扰动。 外汇与贵金属杠杆高,曲线斜率失控可能数小时内回吐大段利润,务必先在策略测试器用历史数据验证再上实盘。
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 拿到的是上上个品种的精度,订单可能直接被服务器拒。
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)) 验证输出是否贴合品种限制。
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 ); }