从头开始开发一款智能交易系统(基础篇)
📘

从头开始开发一款智能交易系统(基础篇)

第 1/2 篇

用 MQL5 从零搭一个 EA 骨架

在 MT5 里写智能交易系统,第一步不是想策略,而是先把 EA 的生存框架立起来:初始化、去初始化、以及每笔 tick 的入口。这三个钩子函数缺一个,策略再漂亮也跑不起来。 MQL5 规定 EA 文件必须包含 OnInit()OnDeinit()OnTick()。其中 OnInit 在加载时跑一次,用来校验输入参数和申请资源;OnDeinit 在卸载或重编译时清理;OnTick 则是行情每变动一次就触发一次,是真正下单逻辑的地盘。 下面这段是最精简的空骨架,编译后能在 MT5 策略测试器里直接加载,但不会做任何交易。把它复制进 MetaEditor 新建的 EA 文件,你就有了可验证的起点。 外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据跑通逻辑,再考虑是否接入真实账户。

MQL5 / C++
class="macro">#include <Trade/Trade.mqh>
CTrade trade;

class="type">int OnInit()
{
   class="kw">return(INIT_SUCCEEDED);
}

class="type">void OnDeinit(class="kw">const class="type">int reason)
{
}

class="type">void OnTick()
{
}

◍ 手动下单为什么容易翻车

外汇和贵金属市场里的新账户一直在增加,但多数人连订单簿怎么撮合都不清楚,只靠平台按钮裸做单。MT5 虽然给了持仓的精细控制,可对经验少的人,手动填合约品种、手数、止损位非常耗时,期货品种在行情跳空时几乎没时间让你逐格填完。 一旦填错方向或价格,错过的不是一两个点的滑点,而是整段行情甚至直接浮亏。尤其是贵金属杠杆拉到 1:500 时,一个字段手抖就可能把本金吞掉,这类高风险操作不建议纯手动硬扛。 用 EA 把规则先写好会顺手很多:你可以提前定死杠杆、能亏多少本金、想赚多少美元而不是含糊的“点数”。之后只在图表上拖一下入场价、点买或卖,剩下的交给程序填单。这样至少把“人手慢半拍”的变量去掉了。

「用枚举和价格吸附避开订单被拒」

做 EA 最费劲的不是写逻辑,而是先想清楚它该怎么跑。代码越短,运行时出错的概率越低,所以我倾向用 OOP 把功能隔离,后面加 Cross 订单或换市场都只动局部。 原文针对 B3 的迷你美元(WDO)和迷你指数(WIN)设计,但只需改一个枚举就能扩到别的品种。枚举 eTypeSymbolFast {WIN, WDO, OTHER} 把品种类型收拢,避免满屏判断交易标的,也降低填错参数的可能。 不同资产的价格阶梯差异很大:WDO 是 0.5 点,WIN 是 5 点,股票是 0.01 点。若直接往图表画线发单,服务器会因报价不符合阶梯拒绝。AdjustPrice 函数就是把任意传入价格吸附到正确即时报价上。 别把正态当圣经 AdjustPrice 里 WDO 走 arg*10 取整再除回,WIN/OTHER 走 round 后 fmod 5 修正。你在 MT5 里把 WDO 图表拖一根线试 3500.23,函数会吐回 3500.20 附近合法值,否则 OrderSend 直接报 4756 类错误。 CreateOrderPendent 是发单核心,只做 OCO 挂单且只用 LIMIT/STOP 类型。类型按 last 与 Price 比较自动选:买时 last>=Price 走 BUY_LIMIT,否则 BUY_STOP;卖反之。这样即使价格瞬跳,限价破位单执行也稳。 杠杆系数 nContracts 不用你算最小交易量。最小 5 手合约、系数填 3,EA 自动开 15 手;TP/SL 级别随手数反向调,单子变大止损点距自动缩。外汇贵金属波动剧烈,这种自动换算能少犯人工计算错,但杠杆放大也意味爆仓风险更高。

MQL5 / C++
enum eTypeSymbolFast {WIN, WDO, OTHER};
  class="type">class="kw">double AdjustPrice(class="kw">const class="type">class="kw">double arg)
    {
      class="type">class="kw">double v0, v1;
      if(m_Infos.TypeSymbol == OTHER)
        class="kw">return arg;
      v0 = (m_Infos.TypeSymbol == WDO ? round(arg * class="num">10.0) : round(arg));
      v1 = fmod(round(v0), class="num">5.0);
      v0 -= ((v1 != class="num">0) || (v1 != class="num">5) ? v1 : class="num">0);
      class="kw">return (m_Infos.TypeSymbol == WDO ? v0 / class="num">10.0 : v0);
    };
  class="type">class="kw">ulong CreateOrderPendent(class="kw">const class="type">bool IsBuy, class="kw">const class="type">class="kw">double Volume, class="kw">const class="type">class="kw">double Price, class="kw">const class="type">class="kw">double Take, class="kw">const class="type">class="kw">double Stop, class="kw">const class="type">bool DayTrade = true)
    {
      class="type">class="kw">double last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action       = TRADE_ACTION_PENDING;
      TradeRequest.symbol       = m_szSymbol;
      TradeRequest.volume       = Volume;
      TradeRequest.type         = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
      TradeRequest.price        = NormalizeDouble(Price, m_Infos.nDigits);
      TradeRequest.sl           = NormalizeDouble(Stop, m_Infos.nDigits);
      TradeRequest.tp           = NormalizeDouble(Take, m_Infos.nDigits);
      TradeRequest.type_time    = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);

挂单指令与品种初始化的收口写法

这段代码片段把挂单请求的尾部和品种初始化逻辑收在了一起。先填 TradeRequest 的剩余字段:stoplimit 与 expiration 置 0 表示不用触发价偏移也不设到期;type_filling 用 ORDER_FILLING_RETURN,在流动性不足时退回未成交部分,对外汇与贵金属这种滑点可能突变的品种更稳妥;deviation 给到 1000 点容差,实盘里若经纪商报价跳空较大才不容易直接报 DEAL_FAILED。 下单动作靠 OrderSend(TradeRequest, TradeResult) 完成,失败就弹 MessageBox 显示 retcode 并 return 0,成功则返回 TradeResult.order。挂单方向由 last 与 Price 比较决定:买侧若最新价已大于等于挂单价走 BUY_LIMIT,否则 BUY_STOP;卖侧反之。注意外汇/贵金属波动剧烈,这类被动挂单可能在不利于你的价位成交,属高风险操作。 Initilize 里先截 _Symbol 前 3 字符区分 WDO/DOL/WIN/IND 等品种类型,再用 SYMBOL_TRADE_TICK_SIZE 除以 SYMBOL_TRADE_TICK_VALUE 得到每 tick 对应的价格系数 v1,把金融意义上的止盈止损金额换算成价格距离。Volume 直接等于 nContracts 乘 SYMBOL_VOLUME_MIN,若你经纪商最小手数是 0.01,传 nContracts=1 就是 0.01 手,开 MT5 改这个参数就能控制每单敞口。 最后 MoveTo 只声明了坐标变量 w、dt 与几个点击/按键布尔量,说明交互拖拽还在壳子阶段。你可以把这段直接塞进自己的 EA 骨架里,先把 deviation 和 type_filling 这两个字段按经纪商环境调一遍,比对着文档空想要直观得多。

MQL5 / C++
TradeRequest.stoplimit      = class="num">0;
TradeRequest.expiration     = class="num">0;
TradeRequest.type_filling  = ORDER_FILLING_RETURN;
TradeRequest.deviation     = class="num">1000;
TradeRequest.comment       = "Order Generated by Experts Advisor.";
if(!OrderSend(TradeRequest, TradeResult))
  {
   MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
   class="kw">return class="num">0;
  };
class="kw">return TradeResult.order;
 };
TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
TradeRequest.symbol = m_szSymbol;
TradeRequest.price        = NormalizeDouble(Price, m_Infos.nDigits);
TradeRequest.sl           = NormalizeDouble(Stop, m_Infos.nDigits);
TradeRequest.tp           = NormalizeDouble(Take, m_Infos.nDigits);
 class="type">void Initilize(class="type">int nContracts, class="type">int FinanceTake, class="type">int FinanceStop, class="type">color cp, class="type">color ct, class="type">color cs, class="type">bool b1)
  {
   class="type">class="kw">string sz0 = StringSubstr(m_szSymbol = _Symbol, class="num">0, class="num">3);
   class="type">class="kw">double v1 = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   m_Infos.Id = ChartID();
   m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
   m_Infos.nDigits = (class="type">int) SymbolInfoInteger(m_szSymbol, SYMBOL_DIGITS);
   m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
   m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
   m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
   m_Infos.IsDayTrade = b1;
   CreateHLine(m_Infos.szHLinePrice, m_Infos.cPrice = cp);
   CreateHLine(m_Infos.szHLineTake, m_Infos.cTake = ct);
   CreateHLine(m_Infos.szHLineStop, m_Infos.cStop = cs);
   ChartSetInteger(m_Infos.Id, CHART_COLOR_VOLUME, m_Infos.cPrice);
   ChartSetInteger(m_Infos.Id, CHART_COLOR_STOP_LEVEL, m_Infos.cStop);
  };
m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
   m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
   m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
   m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
 class="kw">inline class="type">void MoveTo(class="type">int X, class="type">int Y, class="type">uint Key)
  {
   class="type">int w = class="num">0;
   class="type">class="kw">datetime dt;
   class="type">bool bEClick, bKeyBuy, bKeySell;

常见问题

用代码把手数和止损写成固定规则,避免情绪化乱点;先在小周期回放验证再上实盘。
多半是价格没吸附到合法档位或手数不合最小限制;用枚举约束下单类型、对价格做规范化处理即可避坑。
小布可以读取你的策略逻辑,标出挂单初始化和品种未就绪这类常见漏点,并给改法建议。
在发送前用 NormalizeDouble 把挂单价吸附到品种小数点位,并确认初始化时已选中正确品种。
先吃透枚举定义订单类型、品种初始化和挂单函数三块,能跑通一个最小闭环就不亏。