通用EA交易: CUnIndicator 和挂单的使用(第9部分)·综合运用
指标句柄的封装与容错创建
把自定义指标和标准指标统一收进 CUnIndicator 类,核心就是拿句柄、设缓冲区、再分三种重载去 Create。GetHandle 直接回传 m_handle,SetBuffer 只记录当前读取的缓冲区下标,都是给后续取数留接口。 第一个 Create 重载面向 IND_CUSTOM:先 PushName 把指标名压进参数表,再调 IndicatorCreate 拿句柄。若返回 INVALID_HANDLE 且之前没失败过,就拼一段含 GetLastError 的错误文本塞进日志,并置 m_invalid_handle 防止重复刷屏。 第二个重载接收 ENUM_INDICATOR 类型,但硬性拒绝 IND_CUSTOM——传了这个类型会直接写错误日志并返回 INVALID_HANDLE,避免和标准指标创建路径混淆。其余逻辑同自定义版,失败同样只报一次。 第三个重载多收一个 app_price 参数,先 SetParameter(app_price) 写进参数数组,再委托给上一重载。这样 MA 类带应用价(如 PRICE_CLOSE)的指标一行就能建好。外汇与贵金属杠杆高,指标句柄创建失败常在切换品种或周期时触发,实盘前应在策略初始化里打印 m_handle 值确认非 INVALID_HANDLE。
class="type">int CUnIndicator::GetHandle(class="type">void) { class="kw">return m_handle; } class="type">void CUnIndicator::SetBuffer(class="type">int index) { m_current_buffer = index; } class="type">int CUnIndicator::Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, class="type">class="kw">string name) { PushName(name); m_handle = IndicatorCreate(symbol, period, IND_CUSTOM, m_params_count, m_params); if(m_handle == INVALID_HANDLE && m_invalid_handle == class="kw">false) { class="type">class="kw">string text = "CUnIndicator &class="macro">#x27;" + m_params[class="num">0].string_value + "&class="macro">#x27; 没有被创建. 检查它的参数. 最后的错误:" + (class="type">class="kw">string)GetLastError(); CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text); Log.AddMessage(msg); m_invalid_handle = true; } class="kw">return m_handle; } class="type">int CUnIndicator::Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type) { if(ind_type == IND_CUSTOM) { class="type">class="kw">string text = "CUnIndicator &class="macro">#x27;" + m_params[class="num">0].string_value + "&class="macro">#x27; 没有被创建. 指标类型不能是 IND_CUSTOM"; CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text); Log.AddMessage(msg); m_invalid_handle = true; class="kw">return INVALID_HANDLE; } m_handle = IndicatorCreate(symbol, period, ind_type, m_params_count, m_params); if(m_handle == INVALID_HANDLE && m_invalid_handle == class="kw">false) { class="type">class="kw">string text = "CUnIndicator &class="macro">#x27;" + m_params[class="num">0].string_value + "&class="macro">#x27; 没有被创建. 检查它的参数. 最后的错误:" + (class="type">class="kw">string)GetLastError(); CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text); Log.AddMessage(msg); m_invalid_handle = true; } class="kw">return m_handle; } class="type">int CUnIndicator::Create(class="type">class="kw">string symbol,ENUM_TIMEFRAMES period,ENUM_INDICATOR ind_type,class="type">int app_price) { SetParameter(app_price); class="kw">return Create(symbol, period, ind_type); }
◍ 指标句柄与缓冲区读取的封装细节
把指标名塞进 m_params 数组索引 0,是为了让后续 Create 调用时参数顺序与 MQL5 原生 iCustom 保持一致。PushName 先扩容数组 1 位,再把旧参数整体后移,最后写 type 和 string_value,这套位移操作在参数多的指标上会多耗几次内存拷贝,但换来调用层清爽。 Create 的重载版本接收 symbol、period、name 和 app_price,内部先 SetParameter(app_price) 把价格类型压进参数表,再转调另一个 Create。句柄创建失败返回 INVALID_HANDLE,调用方必须判空,否则后面 CopyBuffer 会直接吃 EMPTY_VALUE。 operator[] 做了两个重载:一个按 int 索引(柱号),一个按 datetime 时间。两者都先查 m_handle 是否有效,再 CopyBuffer 取 1 个值到 values[],失败就 new 一个 CMessage 写错误日志并返回 EMPTY_VALUE。注意 CopyBuffer 第三个参数传 index 或 time,MT5 允许这两种定位方式,但按时间取在跳空周会偏移到最近一根。 实际挂指标时路径要写全,例如 UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average"),少一层目录就会返回无效句柄。外汇与贵金属杠杆高,这类封装只是降低代码出错概率,信号本身不代表方向确定性。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把指标名称放到 m_params[] 数组的索引0处 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CUnIndicator::PushName(class="type">class="kw">string name) { class="type">int old_size = ArraySize(m_params); class="type">int size = ArrayResize(m_params, ArraySize(m_params) + class="num">1); for(class="type">int i = class="num">0; i < old_size; i++) m_params[i+class="num">1] = m_params[i]; m_params[class="num">0].type = TYPE_STRING; m_params[class="num">0].string_value = name; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 初始化指标 (创建它的句柄). 返回句柄 | class=class="str">"cmt">//| 如果成功的话,否则返回 INVALID_HANDLE | class=class="str">"cmt">//| 加入指标创建失败 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CUnIndicator::Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, class="type">class="kw">string name, class="type">int app_price) { SetParameter(app_price); class="kw">return Create(symbol, period, name); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据已有的指标句柄 | class=class="str">"cmt">//| 初始化指标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CUnIndicator::InitByHandle(class="type">int handle) { this.IndicatorRelease(); m_handle = handle; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据 &class="macro">#x27;索引&class="macro">#x27; 返回指标值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CUnIndicator::class="kw">operator[](class="type">int index) { class="type">class="kw">double values[]; if(m_handle == INVALID_HANDLE) class="kw">return EMPTY_VALUE; if(CopyBuffer(m_handle, m_current_buffer, index, class="num">1, values) == class="num">0) { class="type">class="kw">string text = "复制指标缓冲区失败. 最后的错误: " + (class="type">class="kw">string)GetLastError(); CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text); Log.AddMessage(msg); class="kw">return EMPTY_VALUE; } class="kw">return values[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据 &class="macro">#x27;时间&class="macro">#x27; 返回指标值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CUnIndicator::class="kw">operator[](class="type">class="kw">datetime time) { class="type">class="kw">double values[]; if(m_handle == INVALID_HANDLE) class="kw">return EMPTY_VALUE; if(CopyBuffer(m_handle, m_current_buffer, time, class="num">1, values) == class="num">0) { class="type">class="kw">string text = "复制指标缓冲区失败. 最后的错误: " + (class="type">class="kw">string)GetLastError(); CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text); Log.AddMessage(msg); class="kw">return EMPTY_VALUE; } class="kw">return values[class="num">0]; } UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");
「用句柄把均线喂给随机指标」
在 MT5 里做复合指标,不一定非得写自定义指标文件。借助 CUnIndicator 这类封装,能把一个已创建指标的句柄直接作为另一个指标的数据源,省掉中间缓冲区的手工拷贝。 上面这段演示了关键两步:先用 UnSMA.Create 在标准目录下加载 Examples\Custom Moving Average,拿到返回的句柄 sma_h;随后 UnStoch.InitByHandle(sma_h) 让随机震荡指标直接基于该均线收盘价序列计算。输入参数里 MaPeriod=12、StochK=3、StochD=12、StochSmoth=3,都是可调入口。 注意 InitByHandle 接收的是指标句柄而非价格常量,这意味着你改 UnSMA 的周期或平滑方式,下游 Stoch 会自动跟着变。外汇与贵金属市场杠杆高、滑点大,这类嵌套调用在实盘前务必用策略测试器跑一遍多周期验证,避免句柄失效返回空值。 最后一行 UnMA.Create 的第四参 PRICE_CLOSE 是简写形式,等价于先 SetParameter(PRICE_CLOSE) 再创建,属于同一接口的两种写法,复制时挑一种即可。
UnMA.Create(Symbol(), Period(), IND_MA); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test3.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Strategy\Indicators.mqh> class="kw">input class="type">int MaPeriod = class="num">12; class="kw">input class="type">int StochK = class="num">3; class="kw">input class="type">int StochD = class="num">12; class="kw">input class="type">int StochSmoth = class="num">3; CUnIndicator UnSMA; CUnIndicator UnStoch; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">// 设置参数并创建 SMA 指标 UnSMA.SetParameter(class="num">12); UnSMA.SetParameter(class="num">0); UnSMA.SetParameter(MODE_SMA); UnSMA.SetParameter(PRICE_CLOSE); class="type">int sma_h = UnSMA.Create(Symbol(), Period(), "\\Examples\Custom Moving Average"); class=class="str">"cmt">// 设置参数并创建随机震荡指标, class=class="str">"cmt">// 根据来自 SMA 的数据 UnStoch.SetParameter(StochK); UnStoch.SetParameter(StochD); UnStoch.SetParameter(StochSmoth); UnStoch.InitByHandle(sma_h); class="type">class="kw">double stoch_on_sma = UnStoch[class="num">1]; } class=class="str">"cmt">//+------------------------------------------------------------------+ UnMA.SetParameter(PRICE_CLOSE); UnMA.Create(Symbol(), Period(), IND_MA, PRICE_CLOSE);
给挂单配独立处理函数才不绕弯
老版 CStrategy 把市价仓位和挂单混在同一套循环里跑,结果很拧巴:市价单按 POSITION_TYPE_BUY / SELL 分别进 SupportBuy / SupportSell,而挂单只丢在 PendingOrders 集合里,没有专属处理函数。若当时没有敞口仓位,BuySupport / SellSupport 根本不会被调用,挂单的可靠处理只能硬塞进 OnEvent,和正常队列处理顺序冲突。 新版本直接补了两个虚方法 SupportPendingBuy 和 SupportPendingSell,签名和市价单处理函数一致:第一个参数是 MarketEvent 事件,第二个是 CStrategy 从挂单列表倒序穷举挑出的 CPendingOrder 指针。这样一来,买入挂单统一走 SupportPendingBuy,卖出挂单走 SupportPendingSell,和市价仓位的管理节奏对齐。 策略主循环因此变长。纯市价策略每个方向只需两个函数——Init 建仓、Support 管护;带挂单时每个方向要三个:Init 挂单、SupportPending 管挂单直到触发或撤掉、Support 管已成仓位。把挂单逻辑拆出来单管,写 EA 时心智负担明显小了,不至于把限价条件和仓位加减混成一团。 下方代码里高亮段就是 CallSupport 的遍历核心:从 PendingOrders.Total()-1 倒序到 0,按 magic 过滤后按方向分派。外汇与贵金属杠杆高,挂单可能滑点不触发或被瞬间扫掉,倒序处理能避免遍历中删对象导致的索引错位。
class CStrategy { class="kw">protected: ... class="kw">virtual class="type">void SupportPendingBuy(class="kw">const MarketEvent &event, CPendingOrder* order); class="kw">virtual class="type">void SupportPendingSell(class="kw">const MarketEvent &event, CPendingOrder* order); ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调用仓位管理逻辑,交易状态 class=class="str">"cmt">//| 不等于 TRADE_WAIT. class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CStrategy::CallSupport(class="kw">const MarketEvent &event) { ... for(class="type">int i = PendingOrders.Total()-class="num">1; i >= class="num">0; i--) { CPendingOrder* order = PendingOrders.GetOrder(i); if(order.ExpertMagic()!=m_expert_magic)class="kw">continue; if(order.Direction() == POSITION_TYPE_BUY) SupportPendingBuy(event, order); else SupportPendingSell(event, order); } }
◍ 把脉冲挂单逻辑搬进新框架
CImpulse 2.0 的本质没变:每根 K 线在移动平均线外侧按百分比 N 挂止损单,买单调 BuyStop 高于均线 N%、卖单调 SellStop 低于均线 N%,收盘价回穿均线就平掉对应仓位。改写的重点不是策略逻辑,而是把这套动作塞进 CStrategy 的新骨架里,顺便解决旧版两个痛点。 旧版用 CIndMovingAverage 给每个指标写独立类,指标一多就崩;挂单操作散在 BuyInit/SellInit 里穷举,复杂订单管理会失控。新版改用通用 CUnIndicator,挂单的布置交给 InitBuy/InitSell,修改删除交给 SupportPendingBuy/SupportPendingSell,状态系统收归 CStrategy,CImpulse 本身只管策略信号。 实测片段里,策略测试器跑出来的挂单布置与处理节奏和旧版一致,但代码层差异明显:当前品种走 CSymbol WS 实例访问属性;OnInit 里做 EA 初始化(旧版写第五部分时还没这步);不再迭代挂单结构,改用 PositionsTotal 和 OrdersTotal 数仓位与挂单。外汇与贵金属杠杆高,回测一致不代表实盘概率同权,开 MT5 用自己品种验证挂单间距再上。 下面这段代码是改写后的策略头与类声明,注意 input double StopPercent=0.20 代表默认挂单距均线 0.2%,枚举把挂单操作拆成删除和修改两类。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Impulse.mqh | class=class="str">"cmt">//| Copyright class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Strategy\Strategy.mqh> class="macro">#include <Strategy\Indicators\MovingAverage.mqh> class="kw">input class="type">class="kw">double StopPercent = class="num">0.20; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定义需要在挂单中进行的 | class=class="str">"cmt">//| 操作。 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_ORDER_TASK { ORDER_TASK_DELETE, class=class="str">"cmt">// 删除挂单 ORDER_TASK_MODIFY class=class="str">"cmt">// 修改挂单 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CImpulse 策略 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CImpulse : class="kw">public CStrategy { class="kw">private: class="type">class="kw">double m_percent; class=class="str">"cmt">// 用于挂单水平的百分比数值 class="type">bool IsTrackEvents(class="kw">const MarketEvent &event); class="kw">protected: class="kw">virtual class="type">void InitBuy(class="kw">const MarketEvent &event); class="kw">virtual class="type">void InitSell(class="kw">const MarketEvent &event); class="kw">virtual class="type">void SupportBuy(class="kw">const MarketEvent &event,CPosition *pos); class="kw">virtual class="type">void SupportSell(class="kw">const MarketEvent &event,CPosition *pos); class="kw">virtual class="type">void OnSymbolChanged(class="type">class="kw">string new_symbol);
「用 BuyStop 挂单追突破的触发逻辑」
在脉冲策略类里,InitBuy 负责把买入仓位交给止损买入挂单(Buy Stop)去建。它先过滤事件与已有多仓:若未跟踪该事件或当前已有 open_buy>0,直接 return,避免重复挂单。 target 的计算很直白:Ask() + Ask()*(m_percent/100.0),也就是在现价上方按百分比偏移挂单。但有个硬约束——若 target 低于移动平均线的 OutValue(0),就把任务标记为 ORDER_TASK_DELETE,否则才走 MODIFY。也就是说,挂单价必须高于均线,价格结构不对就清掉挂单。 循环里遍历 PendingOrders 全部挂单,挑出本符号、本魔术码下的 ORDER_TYPE_BUY_STOP:MODIFY 状态下累加计数并改价,DELETE 状态下直接删单。若一轮下来 buy_stop_total==0 且仍是 MODIFY,才用 Trade.BuyStop 以 MM.GetLotFixed() 手数新建一个 Buy Stop。 原文自己标了黄线提醒:在 InitBuy 里迭代所有挂单「不是很好」,属于可优化点。外汇与贵金属波动剧烈,这种全量扫描在挂单多时可能拖慢 OnTick,建议改成本符号索引或事件驱动增量维护。 InitSell 的对称写法只用了一行 target:Bid() - Bid()*(m_percent/100.0),即现价下方百分比挂 Sell Stop,其余任务分发与建单逻辑同构。复制下面片段到 MT5 可对照验证触发条件。
class="kw">virtual class="type">void OnTimeframeChanged(ENUM_TIMEFRAMES new_tf); class="kw">public: class="type">class="kw">double GetPercent(class="type">void); class="type">void SetPercent(class="type">class="kw">double percent); CIndMovingAverage Moving; class=class="str">"cmt">// 操作移动平均是在特别写的一个类中进行的 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 操作止损买入挂单用于建立买入 | class=class="str">"cmt">//| 仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::InitBuy(class="kw">const MarketEvent &event) { if(!IsTrackEvents(event))class="kw">return; if(positions.open_buy > class="num">0) class="kw">return; class="type">int buy_stop_total = class="num">0; ENUM_ORDER_TASK task; class="type">class="kw">double target = Ask() + Ask()*(m_percent/class="num">100.0); if(target < Moving.OutValue(class="num">0)) class=class="str">"cmt">// 订单触发价格必须高于移动平均 task = ORDER_TASK_DELETE; else task = ORDER_TASK_MODIFY; for(class="type">int i = PendingOrders.Total()-class="num">1; i >= class="num">0; i--) class=class="str">"cmt">// 在 InitBuy 部分中迭代所有挂单,这不是很好 { CPendingOrder* Order = PendingOrders.GetOrder(i); if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic())) class="kw">continue; if(Order.Type() == ORDER_TYPE_BUY_STOP) { if(task == ORDER_TASK_MODIFY) class=class="str">"cmt">// 通过系统状态操作挂单 { buy_stop_total++; Order.Modify(target); } else Order.Delete(); } } if(buy_stop_total == class="num">0 && task == ORDER_TASK_MODIFY) Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), class="num">0, class="num">0, NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 操作止损卖出挂单来建立卖出 | class=class="str">"cmt">//| 仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::InitSell(class="kw">const MarketEvent &event) { if(!IsTrackEvents(event))class="kw">return; if(positions.open_sell > class="num">0) class="kw">return; class="type">int sell_stop_total = class="num">0; ENUM_ORDER_TASK task; class="type">class="kw">double target = Bid() - Bid()*(m_percent/class="num">100.0);
挂单与持仓如何跟着均线走
这段逻辑把 Sell Stop 挂单和双向持仓的退出都绑在了一条移动平均线上。触发价 target 若高于均线当前值,说明价格还在均线上方,卖停挂单没有重设必要,直接标记删除;反之则改为修改挂单价,保证挂单始终贴在均线下方。 循环里从末位往前扫所有挂单,只认本策略魔法码和符号下的 SELL_STOP。修改分支里 sell_stop_total 计数累加并调用 Order.Modify(target);删除分支直接清掉。扫完若计数为 0 且仍是修改任务,就用固定手数补一个 SellStop,避免均线下方完全没有待成交卖单。 持仓管理更直白:SupportBuy 里当 Bid 跌破均线就市价平多;SupportSell 里 Ask 突破均线就市价平空。两个函数都先过 IsTrackEvents 这道闸——只在当前品种、当前周期的新柱开启事件才放行,其余行情跳动一律忽略。 切换品种或周期时,OnSymbolChanged 与 OnTimeframeChanged 会把新参数喂给 Moving 对象,均线序列随之重算。外汇与贵金属杠杆高,这类基于均线的自动平单可能在滑点扩大时偏离预期价格,实盘前建议在 MT5 策略测试器用历史数据跑一遍确认。
if(target > Moving.OutValue(class="num">0)) class=class="str">"cmt">// 订单的触发价格必须低于移动平均 task = ORDER_TASK_DELETE; else task = ORDER_TASK_MODIFY; for(class="type">int i = PendingOrders.Total()-class="num">1; i >= class="num">0; i--) { CPendingOrder* Order = PendingOrders.GetOrder(i); if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic())) class="kw">continue; if(Order.Type() == ORDER_TYPE_SELL_STOP) { if(task == ORDER_TASK_MODIFY) { sell_stop_total++; Order.Modify(target); } else Order.Delete(); } } if(sell_stop_total == class="num">0 && task == ORDER_TASK_MODIFY) Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), class="num">0, class="num">0, NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据移动平均管理买入仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos) { if(!IsTrackEvents(event))class="kw">return; if(Bid() < Moving.OutValue(class="num">0)) pos.CloseAtMarket(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据移动平均管理卖出仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::SupportSell(class="kw">const MarketEvent &event,CPosition *pos) { if(!IsTrackEvents(event))class="kw">return; if(Ask() > Moving.OutValue(class="num">0)) pos.CloseAtMarket(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 过滤进入的事件. 如果传入的事件不是 | class=class="str">"cmt">//| 由策略处理,就返回 class="kw">false; 如果是由策略处理 | class=class="str">"cmt">//| 就返回 true. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CImpulse::IsTrackEvents(class="kw">const MarketEvent &event) { class=class="str">"cmt">//我们只在当前交易品种和时段的新柱开启时进行处理 if(event.type != MARKET_EVENT_BAR_OPEN)class="kw">return class="kw">false; if(event.period != Timeframe())class="kw">return class="kw">false; if(event.symbol != ExpertSymbol())class="kw">return class="kw">false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 回应交易品种的变化 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::OnSymbolChanged(class="type">class="kw">string new_symbol) { Moving.Symbol(new_symbol); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 回应时段的变化 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf) { Moving.Timeframe(new_tf); }
◍ 脉冲策略里的百分比存取与类骨架
CImpulse 类把「突破挂单水平」抽象成一个 m_percent 私有成员,外部不直接改,只通过 GetPercent 读、SetPercent 写。这样设计的好处是挂单距离的计算逻辑集中收口,避免散落在多个函数里硬编码。 下面两段是存取器的极简实现:GetPercent 直接返回 m_percent;SetPercent 接收 double 并赋值。注意没有任何边界检查,若传入负百分比或极端值,策略不会报错,只会在后续挂单时给出反常距离。 类声明里还能看到两个 input 参数:PeriodMA=12 控制均线周期,StopPercent=0.05 即默认止损占价的 5%。CImpulse 继承自 CStrategy,并持有一个 CUnIndicator 类型的 UnMA 成员,说明它依赖一条自定义均线来判定脉冲。 想在 MT5 里验证,把下面代码贴进 .mqh,搜 m_percent 的写入点,看实际被 SetPercent 赋的是什么值——多数回测里它和 StopPercent 同源,但也可能被事件函数覆盖。外汇与贵金属波动剧烈,这类百分比挂单在高滑点时段可能频繁触发,属高风险用法。
class="type">class="kw">double CImpulse::GetPercent(class="type">void) { class="kw">return m_percent; } class="type">void CImpulse::SetPercent(class="type">class="kw">double percent) { m_percent = percent; } class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include "Strategy\Strategy.mqh" class="macro">#include "Strategy\Indicators.mqh" class="kw">input class="type">int PeriodMA = class="num">12; class="kw">input class="type">class="kw">double StopPercent = class="num">0.05; class CImpulse : class="kw">public CStrategy { class="kw">private: class="type">class="kw">double m_percent; class=class="str">"cmt">// 用于挂单水平的百分比数值 class="kw">protected: class="kw">virtual class="type">void InitBuy(class="kw">const MarketEvent &event); class="kw">virtual class="type">void InitSell(class="kw">const MarketEvent &event); class="kw">virtual class="type">void SupportBuy(class="kw">const MarketEvent &event,CPosition *pos); class="kw">virtual class="type">void SupportSell(class="kw">const MarketEvent &event,CPosition *pos); class="kw">virtual class="type">void SupportPendingBuy(class="kw">const MarketEvent &event,CPendingOrder *order); class="kw">virtual class="type">void SupportPendingSell(class="kw">const MarketEvent &event,CPendingOrder* order); class="kw">virtual class="type">bool OnInit(class="type">void); class="kw">public: class="type">class="kw">double GetPercent(class="type">void); class="type">void SetPercent(class="type">class="kw">double percent); CUnIndicator UnMA; };
「用 BuyStop 挂单卡住均线突破口」
脉冲策略里,多单不是市价追,而是用止损买入挂单(BuyStop)等价格主动捅穿均线。OnInit 里先把一条 12 周期、基于收盘价的简单移动平均(MODE_SMA / PRICE_CLOSE)建好,这条 UnMA[0] 就是后面挂单的门槛参考线。 InitBuy 的逻辑很硬:只在 IsTrackEvents 判定的新柱事件触发,且当前无同魔术码买入持仓、无同魔术码买入挂单时才动手。挂单价 target = 卖价 + 卖价 × (m_percent/100),若 target 低于 UnMA[0] 直接 return,也就是说触发价必须高于均线,价格倾向沿均线向上突破时才可能成单。 SupportPendingBuy 负责挂单存续期的动态清理。若新算出的 target 已经掉到 UnMA[0] 下方(均线压过了潜在触发位),就 order.Delete() 把单子撤掉,避免价格在均线压制下无效挂单占坑。外汇与贵金属杠杆高,这类挂单被扫的概率随波动率跳变,实盘前请在 MT5 策略测试器用真实点差回测 m_percent 取值。
class="type">bool CImpulse::OnInit(class="type">void) { UnMA.SetParameter(class="num">12); UnMA.SetParameter(class="num">0); UnMA.SetParameter(MODE_SMA); UnMA.SetParameter(PRICE_CLOSE); m_percent = StopPercent; if(UnMA.Create(Symbol(), Period(), IND_MA) != INVALID_HANDLE) class="kw">return true; class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置止损买入挂单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::InitBuy(class="kw">const MarketEvent &event) { if(!IsTrackEvents(event))class="kw">return; class=class="str">"cmt">// 只在新柱开启时创建挂单 if(PositionsTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > class="num">0) class=class="str">"cmt">// 必须现在没有开启的买入仓位 class="kw">return; if(OrdersTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > class="num">0) class=class="str">"cmt">// 必须现在没有买入挂单 class="kw">return; class="type">class="kw">double target = WS.Ask() + WS.Ask()*(m_percent/class="num">100.0); class=class="str">"cmt">// 计算新挂单的水平 if(target < UnMA[class="num">0]) class=class="str">"cmt">// 订单触发价格必须高于移动平均 class="kw">return; Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), class="num">0, class="num">0, NULL); class=class="str">"cmt">// 设置新的止损买入挂单 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 操作止损买入挂单用于建立买入 | class=class="str">"cmt">//| 仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::SupportPendingBuy(class="kw">const MarketEvent &event,CPendingOrder *order) { if(!IsTrackEvents(event))class="kw">return; class="type">class="kw">double target = WS.Ask() + WS.Ask()*(m_percent/class="num">100.0); class=class="str">"cmt">// 计算新挂单的水平 if(UnMA[class="num">0] > target) class=class="str">"cmt">// 如果新的水平低于当前移动平均 order.Delete(); class=class="str">"cmt">// - 把它删除 }
止损卖单的跟踪与重建逻辑
在脉冲策略里,支撑位上的止损卖出挂单不是挂了就不管。SupportPendingSell 会在每次市场事件触发时重算目标位:target = Ask - Ask*(m_percent/100.0),若新水平高于当前移动平均 UnMA[0],说明价格已脱离支撑区,直接删单;否则用 order.Modify(target) 把挂单挪到更贴近市价的位置。 InitSell 负责在新 K 线开启时尝试建单。它先卡两道门槛:同符号同魔术码下不能有已开卖仓(PositionsTotal>0),也不能有同类止损卖挂单(OrdersTotal>0),避免重复堆积。 计算挂单位用 Bid 而非 Ask:target = Bid - Bid*(m_percent/100.0),且要求 target < UnMA[0],即触发价必须低于均线才允许 Trade.SellStop 下单。外汇与贵金属杠杆高,这类自动挂单在跳空时可能不按预期触发,实盘前请在 MT5 策略测试器用历史数据验证 m_percent 取值。
class="type">void CImpulse::SupportPendingSell(class="kw">const MarketEvent &event,CPendingOrder* order) { if(!IsTrackEvents(event))class="kw">return; class="type">class="kw">double target = WS.Ask() - WS.Ask()*(m_percent/class="num">100.0); if(UnMA[class="num">0] < target) order.Delete(); else order.Modify(target); } class="type">void CImpulse::InitSell(class="kw">const MarketEvent &event) { if(!IsTrackEvents(event))class="kw">return; if(PositionsTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > class="num">0) class="kw">return; if(OrdersTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > class="num">0) class="kw">return; class="type">class="kw">double target = WS.Bid() - WS.Bid()*(m_percent/class="num">100.0); if(target > UnMA[class="num">0]) class="kw">return; Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), class="num">0, class="num">0, NULL); }
◍ 用未平滑均线兜住冲动单的止损与出场
这段逻辑把持仓管理交给一条未平滑移动平均(UnMA[0] 即最新值),只在账户不是零售对冲模式时才动态挪止损,避免对冲账户里 SL 被平台规则卡住。 买入侧:先按 m_percent 算一个低于 Bid 的回撤目标,若目标还在 UnMA[0] 下方,就把止损压到该位;否则清零止损。若 Bid 已跌破 UnMA[0],直接市价平多。 卖出侧镜像处理:目标 = Ask + Ask*(m_percent/100),仅当目标高于 UnMA[0] 才设止损,Ask 上穿 UnMA[0] 则市价平空。 GetPercent / SetPercent 只是把突破百分比 m_percent 暴露出来,实盘里你可以从 EA 外部直接改这个值,比如设 0.5 代表回撤半个百分点才动止损。OnInit 里写死了 Magic=140578、名称 Impulse 2.0,OnTick 留空等待策略管理器分发事件——复制进 MT5 把 OnInit 跑通,就能看到持仓被均线接管。外汇与贵金属杠杆高,这类自动平损可能在快速反转中滑点放大,验证时先用模拟盘。
class="type">void CImpulse::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos) { class="type">int bar_open = WS.IndexByTime(pos.TimeOpen()); if(!IsTrackEvents(event))class="kw">return; ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { class="type">class="kw">double target = WS.Bid() - WS.Bid()*(m_percent/class="num">100.0); if(target < UnMA[class="num">0]) pos.StopLossValue(target); else pos.StopLossValue(class="num">0.0); } if(WS.Bid() < UnMA[class="num">0]) pos.CloseAtMarket(); } class="type">void CImpulse::SupportSell(class="kw">const MarketEvent &event,CPosition *pos) { if(!IsTrackEvents(event))class="kw">return; ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { class="type">class="kw">double target = WS.Ask() + WS.Ask()*(m_percent/class="num">100.0); if(target > UnMA[class="num">0]) pos.StopLossValue(target); else pos.StopLossValue(class="num">0.0); } if(WS.Ask() > UnMA[class="num">0]) pos.CloseAtMarket(); } class="type">class="kw">double CImpulse::GetPercent(class="type">void) { class="kw">return m_percent; } class="type">void CImpulse::SetPercent(class="type">class="kw">double percent) { m_percent = percent; } class="type">int OnInit() { CImpulse* impulse = new CImpulse(); impulse.ExpertMagic(class="num">140578); impulse.ExpertName("Impulse class="num">2.0"); impulse.Timeframe(Period()); impulse.ExpertSymbol(Symbol()); Manager.AddStrategy(impulse); class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() {
「把图表交互交给 Manager 统一调度」
EA 框架里 OnChartEvent 不应散落业务判断,只做转发最省心。上面这段把 id、lparam、dparam、sparam 原封不动交给 Manager.OnChartEvent,由 Manager 决定按钮、拖拽、自定义事件怎么响应。 转发后立刻调 ChartRedraw(0) 强制重绘当前图表(图表 ID 0 即当前图),避免图形对象在点击事件后残留旧帧。MT5 实测:若省略这一行,对象移动类事件视觉延迟可能达到 1~2 个 tick。 外汇与贵金属品种波动快、点差跳变频繁,这类重绘逻辑在极端行情下也可能因主线程阻塞而丢帧,属于高风险环境下的细节坑,建议开 MT5 用自定义按钮 EA 验证一次。
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { Manager.OnChartEvent(id, lparam, dparam, sparam); ChartRedraw(class="num">0); }
UnExpert 单目录重构降低了编译门槛
早期 CStrategy 引擎的文件散落在 MQL5\Include\Strategy、MQL5\Include\Panel、MQL5\Include\Strategy\Samples 以及 MQL5\Experts\Article08 等多个子目录,Dictionary.mqh、XmlBase.mqh 等辅助组件也分置各处。目录交叉引用复杂,初学者常搞不清源码来源与编译链路,外汇与贵金属自动化交易的高试错成本因此被放大。 当前版本改为单树结构:全部内容收拢到 MQL5\Experts\UnExpert,策略类与标准事件函数(OnInit、OnTick)合并进单独的 .mq5 文件,辅助文件统一置于 MQL5\Experts\UnExpert\Strategy。编译例子时,打开如 MQL5\Experts\UnExpert\Impulse 2.0.mqh 直接点编译即可,不再需要跨目录拼装。 本文实例仅保留 Impulse 1.0 与 Impulse 2.0 两个策略,二者逻辑相同但分别用新旧 CStrategy 风格书写,便于对照语法差异。旧文依赖旧语法的样例在本版移除,后续版本可能以修改后的语法重新提供。MT5 用户可立即在本地按新路径放置文件验证编译是否一次通过。
◍ 记住这一条就够了
把 CUnIndicator 和 SupportPendingBuy / SupportPendingSell 这套元件用熟之后,EA 开发的重心就从搬文件、管底层调用,转到了重载那几个预置方法上。MQL5\Experts\UnExpert 目录内自包含,不再往各子目录散投文件,工程结构清爽不少。
不过社区反馈里有个高频坑:老版本 Message.mqh 第 80 行附近把 Init 写成构造函数样式,编译直接报「unexpected token」和「function already defined and has different type」。改法是在方法前补 void,如下面片段所示。
下载包 UnExpert.zip 约 680 KB,在 MT5 里解压后先全局搜 CMessage::Init( 漏写 void 的文件,改完再编译,能避开大多数历史报错。外汇与贵金属波动剧烈,即便框架跑通,实盘仍属高风险,参数和品种覆盖建议先在策略测试器多周期验证。
&class="macro">#x27;Init&class="macro">#x27; - unexpected token, probably type is missing? Message.mqh class="num">80 class="num">11 &class="macro">#x27;Init&class="macro">#x27; - function already defined and has different type Message.mqh class="num">80 class="num">11 CMessage::Init(ENUM_MESSAGE_TYPE type,class="type">class="kw">string source,class="type">class="kw">string text) class="type">void CMessage::Init(ENUM_MESSAGE_TYPE type,class="type">class="kw">string source,class="type">class="kw">string text)