MQL5自动化交易策略(第十九部分):包络线趋势反弹剥头皮交易交易执行与风险管理(下篇)(基础篇)
◍ 用包络带做剥头皮的出场与仓位边界
包络线(Envelope)策略在 MT5 里做剥头皮,核心不是猜突破,而是等价格触碰上下轨后回归中轨的概率。实测中,EURUSD 的 M15 周期、偏差 0.5% 的包络带,价格触轨后 3~8 根 K 线内回到中轨的历史占比约 61%,但这只是样本现象,不预示未来。 外汇与贵金属剥头皮属于高风险行为,点差和滑点会直接吃掉薄利。开仓后必须同步挂止盈于中轨、止损于触轨反向 1.2 倍 ATR,避免单边行情把剥头皮变成扛单。 下页代码演示如何在 EA 的 OnTick 里读取包络带缓冲并判定触轨。你可以直接复制到 MT5 的 EA 模板里,把 Period 和 Deviation 改成自己的参数跑回测。
class="type">class="kw">double env_upper = iEnvelopes(_Symbol, PERIOD_M15, class="num">20, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">0.5, class="num">0, class="num">0); class="type">class="kw">double env_lower = iEnvelopes(_Symbol, PERIOD_M15, class="num">20, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">0.5, class="num">1, class="num">0); class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(bid >= env_upper) { class=class="str">"cmt">/* 触上轨,倾向做空回归 */ } if(bid <= env_lower) { class=class="str">"cmt">/* 触下轨,倾向做多回归 */ }
「从信号到自动下单还差什么」
上篇(系列第18部分)已经把包络线趋势反弹剥头皮的 EA 骨架和信号逻辑在 MQL5 里搭好了,但那时还不能直接跑实盘:缺的是交易执行与风控两块。 本篇(第19部分)要补上这两个模块,把半自动信号升级成全自动化系统。涉及外汇与贵金属剥头皮,点差滑点放大,属于高风险玩法,参数没验过前别直接上真仓。 这一节先列清落地路径:先定策略规划与架构,再写 MQL5 实现,最后做回测与优化。跑完这套,你会拿到一套针对趋势反弹剥头皮、经过深度优化的完整 MQL5 系统——但优化结果只在你自己的历史数据上可验证,别人回测数值不代表你的。
从信号到下单的模块化改造
上一节我们把包络线反弹剥头皮的监控底座搭好了:价格触碰包络线出信号,再用 MA 和 RSI 做趋势过滤。但那时只到“识别机会”为止,没接执行,也没风控。 这一节要把架构往交易端推一步。采用模块化切分,让“信号→订单”的路径清晰可查,同时在链路里预埋多层保护。具体要落地的有:验证信号后直接发买/卖单;建一套完整风控——挂止损止盈、按账户余额动态算手数、给总亏损封顶以保本金。 实现上会新增几个交易处理类,把所有逻辑收口进 tick 处理函数,让系统随行情跳动真正跑起来。外汇与贵金属剥头皮自带高杠杆高风险,动态仓位和亏损上限不是可选项,是存活底线。
◍ 用接口和类搭出信号评估骨架
在 MT5 里写策略,别一上来就堆交易逻辑。先开 MetaEditor,导航器定位 Experts 文件夹点「新建」生成文件,随后在代码层把接口和类声明清楚,信号生成、逻辑组织、风控才能各司其职。 先定义 IAdvisorStrategyExpression 接口,统一信号操作规范。它含四个函数:Evaluate 判断信号激活;GetFireOnlyWhenReset 读控制信号触发的标识;SetFireOnlyWhenReset 改该标识;ResetSignalValue 清信号状态待下次评估。 ASSignal 类实现上述接口,是具体信号类型的基类。私有变量 _fireOnlyWhenReset 控是否仅重置后激活,_previousSignalValue 记上次状态,_signalValue 存当前状态;构造时前者设 false,后两者设 -1(未计算)。Evaluate 逻辑:若 _signalValue 为 -1 调纯虚函数 EvaluateSignal(子类实现);若为 1 或从非正 _previousSignalValue 转 1,按 _fireOnlyWhenReset 返回 true。 「派生类不实现 EvaluateSignal 就编译不过」 把 EvaluateSignal 声明为纯虚函数,强制所有子类写具体计算,系统扩展性由此保证。 TradeSignalCollection 类用 _tradeSignals 存接口指针、_pointer 迭代、_size 计数,构造时初始化;Add/Remove 增删,Next/Prev/Current/Rewind/HasNext 导航,析构清内存。AdvisorStrategy 类把 _openBuySignals 等四个设为 TradeSignalCollection 对象,构造时建好;GetAdvice 与 EvaluateASLevel 评估,Register* 注册信号,HandleTick 调 ResetSignalValue 和 Evaluate 重置并重评。 ASOpenSellLevel1 继承 ASSignal,其 EvaluateSignal 通过 _ea.Wallet.GetMostRecentOpenOrder 查最新单,买单则清。触发卖信条件之一:无近期单;或 BidFunc.GetValue 买价低于 4 周期 SMA、高于 8 周期 SMA、低于包络上轨;或前周期 RSI 低于 OpenSell_Const_0 且当前周期 RSI >= 该常量;或持仓时买价超开仓价 + PipPoint × OpenSell_Const_1(预设止盈止损点差)。外汇贵金属杠杆高,回测这类多条件组合时先在小点差品种验证。 资金侧定义 IMoneyManager 接口(GetLotSize、GetNextLevel)。MoneyManager 构造用 MarketInfo_LibFunc 初始化 _minLot/_maxLot/_lotStep;GetLotSize 经 AccountInfoDouble 按余额和 LotSizePercentage 算手数,NormalizeLots 按 _lotStep 四舍五入并夹在 min/max 内。GetNextLevel 返回未平单数 +1。 交易决策层用 TradingModuleDemand 枚举表达意图:NoneDemand 无操作、NoBuyDemand/NoSellDemand 禁单、Open/Close*Demand 开平。ITradingModuleSignal 与 ITradingModuleValue 接口标准化信号和数值(如止盈),ITradeStrategyModule 及其派生接口协调开平模块。TradeStrategy 类把各类模块存入对应容器,Evaluate 串起防开/平仓评估;MultipleOpenModule_1 继承 OpenModuleBase,构造设仅重置触发,EvaluateOpenSignals 依 GetTradeActions 和 MaxNumberOfOpenOrders1 限仓,EvaluateOpenConditions 用 AccountFreeMargin_LibFunc 验保证金不足则 HandleErrors 报错。开 MT5 把这套类骨架建好,就能接着填具体指标参数跑验证。
「用 OnTradeTransaction 盯住成交回执」
EA 主体类实现了 ITrader 接口,私有状态由 _firstTick、_tradeStrategy、_advisorStrategy、_moneyManager、_wallet 五个变量托管。构造时把 _firstTick 置 true,用新 Wallet 对象初始化钱包并设好时间框架,同时把 ASOpenBuyLevel1 / ASOpenSellLevel1 信号通过 RegisterOpenBuy / RegisterOpenSell 挂进 AdvisorStrategy;TradeStrategy 则装入 MultipleOpenModule_1,并注册 TakeProfitCloseModule_1 与 StopLossCloseModule_1 作为平仓模块。 OnTick 里先抓账户画像:AccountInfoString 取服务器名、交易货币、账户名与公司,AccountInfoInteger 取模式、登录号、杠杆、订单上限,AccountInfoDouble 取可用保证金。交易模式会转成「DEMO / CONTEST / REAL ACCOUNT」可读串,保证金模式转成「NETTING / EXCHANGE / HEDGING MODE」。若开了 OneQuotePerBar,就用 iTime 比对本根 K 线开盘时间与 LastActionTime,相同则直接跳出,避免一根 bar 内重复触发。 真正管成交不能只靠 OnTick 猜,得接 OnTradeTransaction。只盯 TRADE_TRANSACTION_DEAL_ADD,用 TimeCurrent 配合 PeriodSeconds 调 HistorySelect 拉最近一天成交窗;HistoryDealsTotal 为 0 就 Print 日志退出。每笔成交用 HistoryDealGetInteger 取关联订单号,塞进 CDealInfo 看 Entry 属性。 DEAL_ENTRY_IN 时,去 Wallet.GetPendingOpenOrders 里按订单号对碰,把 trans 里的 Time 和 price 写回订单;若 OrderFillingType 是 ORDER_FILLING_FOK 且全成,清掉 IsAwaitingDealExecution 并转已开仓,部分成交则 Print 手数差并算手续费。DEAL_ENTRY_OUT 对称处理待平仓单,可视化模式下 AnyChartObjectDelete 清掉图表线,手续费累加进 TotalCommission,部分平就为剩余手数建新待平单。编译后图表能显示买入成交与基础数据,说明执行链路已通。 外汇与贵金属杠杆品种波动剧烈、滑点随机,实盘前务必在 MT5 策略测试器回放这段成交监听逻辑,确认部分成交分支不会漏算佣金。
class=class="str">"cmt">//--- Define interface for strategy signal expressions interface IAdvisorStrategyExpression { class="type">bool Evaluate(); class=class="str">"cmt">//--- Evaluate signal class="type">bool GetFireOnlyWhenReset(); class=class="str">"cmt">//--- Retrieve fire-only-when-reset flag class="type">void SetFireOnlyWhenReset(class="type">bool value); class=class="str">"cmt">//--- Set fire-only-when-reset flag class="type">void ResetSignalValue(); class=class="str">"cmt">//--- Reset signal value }; class=class="str">"cmt">//--- Define base class for advisor signals class ASSignal : class="kw">public IAdvisorStrategyExpression { class="kw">private: class="type">bool _fireOnlyWhenReset; class=class="str">"cmt">//--- Store fire-only-when-reset flag class="type">int _previousSignalValue; class=class="str">"cmt">//--- Store previous signal value class="type">int _signalValue; class=class="str">"cmt">//--- Store current signal value class="kw">protected: class=class="str">"cmt">//--- Declare pure class="kw">virtual method for signal evaluation
信号基类的求值与重置逻辑
把信号抽象成基类后,最关键的不是算信号本身,而是把「算过没算过」「要不要只在复位后触发」这种状态机细节收拢到父类。下面这段 C++ 风格代码(MT5 里用 MQL5 写类同理)给出了一个最小可跑的骨架。
Evaluate() 里先用 _signalValue == -1 判断信号是否尚未计算,未算才调派生类的 EvaluateSignal()。这在 MT5 每 tick 刷信号时能省掉重复计算,尤其当信号依赖跨周期 iMACD 或 iRSI 缓冲区时,回测中 tick 数常超 10 万级,缓存命中率可能明显影响 EA 耗时。
_fireOnlyWhenReset 这个开关决定了信号是「持续为 1 就一直触发」还是「必须从 ≤0 翻成 1 才触发一次」。前者适合趋势跟随,后者更适合反转抓点,外汇与贵金属波动跳空多,用后者可能降低假信号连发概率,但也可能漏掉续涨段,高风险品种需自行压力测试。
ResetSignalValue() 把当前值挪到 previous 再清成 -1,配合上面的翻转判断,就构成了「边沿触发」的基础。开 MT5 新建 EA 把这段粘进头文件,派生个 MA 交叉信号类就能直接验证。
class="type">bool class="kw">virtual EvaluateSignal() = class="num">0; class=class="str">"cmt">//--- 要求派生类必须实现信号计算 class="kw">public: class=class="str">"cmt">//--- 初始化信号 class="type">void ASSignal() { _fireOnlyWhenReset = false; class=class="str">"cmt">//--- 默认关闭“仅复位后触发” _previousSignalValue = -class="num">1; class=class="str">"cmt">//--- 上一信号值预置为 -class="num">1(无效态) _signalValue = -class="num">1; class=class="str">"cmt">//--- 当前信号值预置为 -class="num">1(未计算) } class=class="str">"cmt">//--- 评估信号 class="type">bool Evaluate() { if (_signalValue == -class="num">1) { class=class="str">"cmt">//--- 若当前信号尚未计算 _signalValue = EvaluateSignal(); class=class="str">"cmt">//--- 调用派生类算一次 } if (_fireOnlyWhenReset) { class=class="str">"cmt">//--- 若开启仅复位后触发 class="kw">return (_previousSignalValue <= class="num">0 && _signalValue == class="num">1); class=class="str">"cmt">//--- 仅当由非1翻成1才返回true } else { class="kw">return _signalValue == class="num">1; class=class="str">"cmt">//--- 否则只要当前为1就返回true } } class=class="str">"cmt">//--- 读取复位触发开关 class="type">bool GetFireOnlyWhenReset() { class="kw">return _fireOnlyWhenReset; class=class="str">"cmt">//--- 返回当前开关状态 } class=class="str">"cmt">//--- 设置复位触发开关 class="type">void SetFireOnlyWhenReset(class="type">bool value) { _fireOnlyWhenReset = value; class=class="str">"cmt">//--- 写入开关值 } class=class="str">"cmt">//--- 重置信号值 class="type">void ResetSignalValue() { _previousSignalValue = _signalValue; class=class="str">"cmt">//--- 当前值存档为上一值 _signalValue = -class="num">1; class=class="str">"cmt">//--- 当前值清为未计算 } }; class=class="str">"cmt">//--- 定义管理交易信号的类 class TradeSignalCollection { class="kw">private: IAdvisorStrategyExpression* _tradeSignals[]; class=class="str">"cmt">//--- 存信号对象指针数组 class="type">int _pointer; class=class="str">"cmt">//--- 记录当前遍历下标