从头开始开发智能交易系统(第 26 部分):面向未来(I)(基础篇)
把 EA 架构留条后路
写 EA 最容易被忽略的不是信号逻辑,而是结构本身能不能扛住后续扩展。很多交易者在 MT5 里把一个策略全塞进 OnTick,等到要加多周期过滤或切换品种时只能推倒重来。 从 2022 年 11 月那篇面向未来的设计思路看,核心做法是把决策层和执行层拆开:行情处理、信号计算、订单路由各管各的。这样后续接入 AIGC 分析模块时,只需替换信号计算部分,不必动下单与风控。 实操上,开 MT5 新建一个 EA,先别写买卖条件,先把 CTrade 实例和信号结构体定义好。宁可前期多花半小时分层,也别在策略跑顺后卡在改不动的泥潭里。外汇与贵金属杠杆高,结构混乱的 EA 在极端行情下更易出非预期单。
◍ 入场执念如何暴露系统缺陷
很多有经验的交易者习惯在预设价位挂单后纹丝不动,无论行情怎么走都认定那是完美入场点,最多平移或删除破位线,但绝不改入场价。这种静态操作在 MT5 自动化系统里其实掩盖了不少底层订单逻辑的问题。 现有代码里残留的缺陷,对“挂了就等”的人几乎无感——他们甚至可能知道系统有毛病,比如本文要修的那类,也不影响实际下单动作。 但另一类喜欢追价、总想凑一笔交易却又临场不敢进场的用户,会在系统里撞见大量错误反馈;有人强行干预把账户搞到极不安全,有人却在那段时间赚到钱,留前者对着行情发懵。外汇和贵金属杠杆高,这类干预极易放大亏损概率。 所以系统健壮与否,不只看代码跑不跑得通,更看你交易行为落在哪一类上。
「EA 入场缺陷的修复取舍」
很多自带 EA 在实时运行时直接沿用初始入场价,若行情在订单发送前跳空或重报价,就会以过时价位盲目成交,这种缺陷在波动剧烈的贵金属品种上更容易触发滑点爆仓。 如果你从不在运行中改入场点,这个坑暂时碰不到;但一旦涉及动态挂单或延迟补单,就必须改代码堵住它。 文中附带的最终代码已经内置了修复,代价是每 tick 多几次价格校验,会吃掉一点回测与实盘的执行性能。 问题不在于修不修,而在于你更接受哪种损失:是牺牲些许性能,还是冒着用错误价位入场赔钱的概率。外汇与贵金属杠杆高,这类隐性入场错误可能直接放大账户回撤。
移动挂单入场点引发的灾难
在 MT5 里用 EA 下 BUY STOP 后手动拖动点位,本想改成 BUY LIMIT,图表上看着改了,服务器侧却仍是旧单类型——这种图表与服务器不一致,是挂单修改逻辑里最致命的一类错误。只在订单真正开仓时系统才会强制同步,此前 EA 显示的和 broker 端数据完全是两回事。 实测中,STOP 类挂单(BUY STOP / SELL STOP)加几行修改代码就能稳住;但 LIMIT 类(BUY LIMIT、STOP LIMIT)一旦改点位,服务器可能立刻填单。若挂单带了止损且止损价已越过限价,服务器会秒开仓并秒平仓,账户直接遭重创。我们在模拟账户跑若干次看似正常,切实盘就开始莫名亏损,根源就在这。 关键事实:若入场点只设一次永不移动,该 bug 完全无影响;只有交易者或 EA 动态移点才会触发。外汇与贵金属杠杆高,这类静默错单可能造成快速本金回撤,务必在模拟环境复现后再上实盘。 修正思路是删单→移点→改类型→重挂。下方函数里高亮行即基础补丁:对 IT_PENDING 分支,调用 ModifyOrderPendent 时把 TP、SL 相对入场点重算,避免旧偏移量错位。 注意 ForceNewType 宏内含 break,改 case 模块时若不小心会让流程提前跳出。你可在 MT5 策略测试器里故意拖 BUY LIMIT 点位,观察日志是否出现类型互转,来验证补丁是否生效。
class="type">void SetPriceSelection(class="type">class="kw">double price) { class="type">char Pending; if (m_Selection.ticket == class="num">0) class="kw">return; Mouse.Show(); if (m_Selection.ticket == def_IndicatorTicket0) { CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, price + m_Selection.tp - m_Selection.pr, price + m_Selection.sl - m_Selection.pr, m_Selection.bIsDayTrade); RemoveIndicator(def_IndicatorTicket0); class="kw">return; } if ((Pending = GetInfosTradeServer(m_Selection.ticket)) == class="num">0) class="kw">return; m_TradeLine.SpotLight(); class="kw">switch (m_Selection.it) { case IT_TAKE: if (Pending < class="num">0) ModifyOrderPendent(m_Selection.ticket, m_Selection.pr, price, m_Selection.sl); else ModifyPosition(m_Selection.ticket, price, m_Selection.sl); class="kw">break; case IT_STOP: if (Pending < class="num">0) ModifyOrderPendent(m_Selection.ticket, m_Selection.pr, m_Selection.tp, price); else ModifyPosition(m_Selection.ticket, m_Selection.tp, price); class="kw">break; case IT_PENDING: if (!ModifyOrderPendent(m_Selection.ticket, price, (m_Selection.tp == class="num">0 ? class="num">0 : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == class="num">0 ? class="num">0 : price + m_Selection.sl - m_Selection.pr)))
◍ 挂单与持仓的鼠标拖拽改价逻辑
在 MT5 脚本里用鼠标直接拖拽图表上的线来改变挂单或持仓的止盈止损,核心落在 SetPriceSelection(double price) 这个函数。它先判断当前选中的对象 ticket,若为 0 就直接 return,避免空选中导致报错。 当 ticket 等于 def_IndicatorTicket0 时,说明用户是在 ghost 指标线上拖出了新挂单:代码调用 CreateOrderPendent 并按 price + m_Selection.tp - m_Selection.pr 推算 TP、price + m_Selection.sl - m_Selection.pr 推算 SL,随后 RemoveIndicator 清掉临时线。这一行就能让你在 MT5 里复现“拖线即发单”的手感。 对于已存在的挂单(IT_PENDING),先用 OrderGetInteger(ORDER_TYPE) 取回订单类型;若是 BUY_LIMIT 或 SELL_LIMIT,才允许把 price 写进挂单价。外汇与贵金属杠杆高,拖错价位可能瞬间触发滑点成交,实盘前务必在策略测试器用历史数据跑一遍。 下面这段是从原逻辑里抽出的关键分支,注意高亮部分就是 LIMIT 类挂单的识别与改写入口:
orderType = OrderGetInteger(ORDER_TYPE); if ((orderType == ORDER_TYPE_BUY_LIMIT) || (orderType == ORDER_TYPE_SELL_LIMIT))
「挂单被现价越过时的重挂逻辑」
当挂单方向为 Buy 且市场价格向上越过挂单价,或方向为 Sell 且价格向下跌破挂单价时,原挂单已经失去原定位意义。代码里用 last 抓取当前 SYMBOL_ASK 或 SYMBOL_BID,与 price 做交叉判断,命中即删单重挂。 删除旧挂单与附属指标后,CreateOrderPendent 会以新 price 为基准重算 tp、sl:若 m_Selection.tp 为 0 则止盈传 0,否则用 price + m_Selection.tp - m_Selection.pr 偏移。外汇与贵金属杠杆品种跳空时这种越价重挂可能连续触发,实盘前建议在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍确认次数。 若未触发越价分支,则走 ModifyOrderPendent 改单;失败时将幽灵指标线移回挂单价位并高亮。整段没有保证成交,只是把挂单位置锚定到最新参考价,重挂后是否成交仍取决于后续价格行为。
{
last = SymbolInfoDouble(Terminal.GetSymbol(), (m_Selection.bIsBuy ? SYMBOL_ASK : SYMBOL_BID));
if (((m_Selection.bIsBuy) && (price > last)) || ((!m_Selection.bIsBuy) && (price < last)))
{
RemoveOrderPendent(m_Selection.ticket);
RemoveIndicator(m_Selection.ticket);
CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, (m_Selection.tp == class="num">0 ? class="num">0 : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == class="num">0 ? class="num">0 : price + m_Selection.sl - m_Selection.pr), m_Selection.bIsDayTrade);
class="kw">break;
}
}
if (!ModifyOrderPendent(m_Selection.ticket, price, (m_Selection.tp == class="num">0 ? class="num">0 : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == class="num">0 ? class="num">0 : price + m_Selection.sl - m_Selection.pr)))
{
MoveSelection(macroGetLinePrice(def_IndicatorGhost, IT_PENDING));
m_TradeLine.SpotLight();
}
class="kw">break;
RemoveOrderPendent(m_Selection.ticket);
RemoveIndicator(m_Selection.ticket);
CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, (m_Selection.tp == class="num">0 ? class="num">0 : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == class="num">0 ? class="num">0 : price + m_Selection.sl - m_Selection.pr), m_Selection.bIsDayTrade);
class="kw">break;
if ((orderType == ORDER_TYPE_BUY_LIMIT) || (orderType == ORDER_TYPE_SELL_LIMIT))
{
last = SymbolInfoDouble(Terminal.GetSymbol(), (m_Selection.bIsBuy ? SYMBOL_ASK : SYMBOL_BID));