开发多币种 EA 交易(第 2 部分):过渡到交易策略的虚拟仓位·进阶篇
(2/3)· 当策略数冲到上百,单个仓位被压到经纪商下限之下怎么办?
◍ EA 骨架里挂策略实例的写法
MT5 里把具体策略挂进顾问对象,核心就在 OnInit 里 new 一个 CAdvisor,再 Add 一个策略实例。下面这段把成交量策略 CSimpleVolumesMarketStrategy 塞进去,magic 写死 27181,外部输入组标了『=== EA parameters』,方便在 EA 属性面板改参。 OnTick 只做一件事:expert.Tick(),把所有策略的行情响应收口到顾问层;OnDeinit 里 delete expert 释放指针,避免反复加载 EA 时内存泄漏。 开仓分支看 openDistance_ 的符号:大于 0 挂 BuyStop,小于 0 挂 BuyLimit,等于 0 直接市价 Buy。这个字段决定你是突破追单还是回踩接单,外汇和贵金属波动大,挂单距离设错可能频繁触发或永远不触发,属高风险操作。 另有一套并行输入:startIndex_=0、totalStrategies_=1、depoPart_=1.0、magic_=27182,说明同一顾问可以 startIndex 偏移、多策略复用保证金比例,magic 换号隔离订单。复制代码前先把两个 magic 号理清楚,不然历史单会混。
input group "=== EA parameters" input class="type">ulong magic_ = class="num">27181; class=class="str">"cmt">// Magic CAdvisor *expert; class=class="str">"cmt">// Pointer to the EA object class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { expert = new CAdvisor(); expert.Add(new CSimpleVolumesMarketStrategy( magic_, symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); class=class="str">"cmt">// Add one strategy instance class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { expert.Tick(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class="kw">delete expert; } class="type">void CSimpleVolumesMarketStrategy::OpenBuyOrder() { class=class="str">"cmt">// Previous code in the method ... class=class="str">"cmt">// Order volume class="type">class="kw">double lot = m_fixedLot; class=class="str">"cmt">// Set a pending order class="type">bool res = false; if(openDistance_ > class="num">0) { res = trade.BuyStop(lot, ...); } else if(openDistance_ < class="num">0) { res = trade.BuyLimit(lot, ...); } else { res = trade.Buy(lot, ...); } if(!res) { Print("Error opening order"); } } class="macro">#include "Advisor.mqh" class="macro">#include "SimpleVolumesMarketStrategy.mqh" input class="type">int startIndex_ = class="num">0; class=class="str">"cmt">// Starting index input class="type">int totalStrategies_ = class="num">1; class=class="str">"cmt">// Number of strategies input class="type">class="kw">double depoPart_ = class="num">1.0; class=class="str">"cmt">// Part of the deposit for one strategy input class="type">ulong magic_ = class="num">27182; class=class="str">"cmt">// Magic CAdvisor *expert; class=class="str">"cmt">// EA object class=class="str">"cmt">//+------------------------------------------------------------------+
初始化里把九套策略实例铺开
EA 启动阶段先在 OnInit 里做参数护栏:startIndex_ 不能为负,且 startIndex_ + totalStrategies_ 必须落在 0~9 区间内,否则直接返回 INIT_PARAMETERS_INCORRECT 让加载失败,避免越界访问空指针。 随后用定长指针数组 CStrategy* strategies[9] 承接实例。前 7 个槽位已经写死,分别是 EURGBP 三套、GBPUSD 三套、EURUSD 一套,全部跑 PERIOD_H1。 手数不是写死的,而是用 NormalizeDouble(0.01 / 波动系数 * depoPart_, 2) 算出来。比如 EURGBP 第一套除 0.16,GBPUSD 第四套除 0.25,depoPart_ 是你给的仓位比例参数,保留两位小数。 每个实例的魔法号按 magic_ + 序号 递增,后面跟着一堆策略私有参数:周期参数 13/17/51/80/128 等、阈值 0.3~2.0、滑点容差 0~1.1、止盈止损点数 2000~22000 不等。开 MT5 把这套数组贴进自己的 EA,改 depoPart_ 就能看手数怎么随波动系数伸缩。
class="type">int OnInit() { class=class="str">"cmt">// Check if the parameters are correct if(startIndex_ < class="num">0 || startIndex_ + totalStrategies_ > class="num">9) { class="kw">return INIT_PARAMETERS_INCORRECT; } class=class="str">"cmt">// Create and fill the array of strategy instances CStrategy *strategies[class="num">9]; strategies[class="num">0] = new CSimpleVolumesMarketStrategy( magic_ + class="num">0, "EURGBP", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.16 * depoPart_, class="num">2), class="num">13, class="num">0.3, class="num">1.0, class="num">0, class="num">10500, class="num">465, class="num">1000, class="num">3); strategies[class="num">1] = new CSimpleVolumesMarketStrategy( magic_ + class="num">1, "EURGBP", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.09 * depoPart_, class="num">2), class="num">17, class="num">1.7, class="num">0.5, class="num">0, class="num">16500, class="num">220, class="num">1000, class="num">3); strategies[class="num">2] = new CSimpleVolumesMarketStrategy( magic_ + class="num">2, "EURGBP", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.16 * depoPart_, class="num">2), class="num">51, class="num">0.5, class="num">1.1, class="num">0, class="num">19500, class="num">370, class="num">22000, class="num">3); strategies[class="num">3] = new CSimpleVolumesMarketStrategy( magic_ + class="num">3, "GBPUSD", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.25 * depoPart_, class="num">2), class="num">80, class="num">1.1, class="num">0.2, class="num">0, class="num">6000, class="num">1190, class="num">1000, class="num">3); strategies[class="num">4] = new CSimpleVolumesMarketStrategy( magic_ + class="num">4, "GBPUSD", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.09 * depoPart_, class="num">2), class="num">128, class="num">2.0, class="num">0.9, class="num">0, class="num">2000, class="num">1170, class="num">1000, class="num">3); strategies[class="num">5] = new CSimpleVolumesMarketStrategy( magic_ + class="num">5, "GBPUSD", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.14 * depoPart_, class="num">2), class="num">13, class="num">1.5, class="num">0.8, class="num">0, class="num">2500, class="num">1375, class="num">1000, class="num">3); strategies[class="num">6] = new CSimpleVolumesMarketStrategy( magic_ + class="num">6, "EURUSD", PERIOD_H1,
「把多套成交量策略塞进同一个 EA」
这段初始化代码把三种不同参数的 EURUSD H1 成交量策略挂到同一个顾问对象里,靠 magic 号 +7~+9 区分。手数都用 NormalizeDouble(0.01 / 系数 * depoPart_, 2) 算,相当于按仓位占比动态开仓,保留两位小数避免 broker 拒绝。 策略7的触发阈值设 18、盈利/止损比例 0.2/0.4,最大持仓 19500;策略8更激进,阈值拉到 128、比例 0.7/0.3、最大持仓 42000。把这些数字直接丢进 MT5 回测,能看出高阈值策略在震荡市触发少但单笔盈亏比偏向 0.7 的可能。 循环里用 startIndex_ 和 totalStrategies_ 控制加载哪几个,改这两个变量就能热插拔策略组合,不用动具体参数。外汇与贵金属杠杆高,多策略叠加会放大回撤,实盘前务必用历史数据验证。
strategies[class="num">7] = new CSimpleVolumesMarketStrategy( magic_ + class="num">7, "EURUSD", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.20 * depoPart_, class="num">2), class="num">18, class="num">0.2, class="num">0.4, class="num">0, class="num">19500, class="num">1480, class="num">6000, class="num">3); strategies[class="num">8] = new CSimpleVolumesMarketStrategy( magic_ + class="num">8, "EURUSD", PERIOD_H1, NormalizeDouble(class="num">0.01 / class="num">0.22 * depoPart_, class="num">2), class="num">128, class="num">0.7, class="num">0.3, class="num">0, class="num">3000, class="num">170, class="num">42000, class="num">3); expert = new CAdvisor(); class=class="str">"cmt">// Add the necessary strategies to the EA for(class="type">int i = startIndex_; i < startIndex_ + totalStrategies_; i++) { expert.Add(strategies[i]); } class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { expert.Tick(); } class="type">void OnDeinit(const class="type">int reason) { class="kw">delete expert; }
◍ 用静态计数和数组管住虚拟仓位生命周期
虚拟仓位对象和实际“打开”动作要分开看。可以先把 CVirtualOrder 实例建好搁着,等策略触发才填交易品种、成交量、开盘价;平仓时再写 closePrice、closeTime、closeReason。同一对象能反复用:清掉平仓字段,重新灌开盘数据即可,不必每次 new。 为了不让 ID 撞车,类里放一个静态 s_count,初始为 0。每构造一个对象,s_count 先自增,再赋给 m_id,整个 EA 运行期该 ID 不变。CSymbolInfo 也做成静态成员,所有实例共用一份报价接口,省内存也避免符号状态不一致。 策略端通常知道最大市场仓位数,虚拟仓位也该限数。做法是在初始化时 new 出一个固定长度的对象数组,平时只从里挑“未建仓”的用:条件到了转成已建,平仓条件到了转成已平。 每个 tick 都得统一扫一遍所有开启中的虚拟单,看止损止盈碰没碰。这种重复劳动适合封进类里——CVirtualOrder 自带 Tick() 实例方法检查自身平仓条件,状态变了返回 true;另写静态 Tick() 接对象数组引用,挨个调实例 Tick(),只要有一个平掉就返回 true。外汇和贵金属杠杆高,虚拟仓位的 SL/TP 逻辑若写错,回测和实盘偏差可能很大,建议先在策略测试器跑小额验证。 下面这段是类声明骨架,重点看静态成员与字段划分: [CODE] class CVirtualOrder { private: ulong m_id; // 唯一ID,构造时由计数器赋值 string m_symbol; // 交易品种 double m_lot; // 成交量 ENUM_ORDER_TYPE m_type; // 订单类型 double m_openPrice; // 开盘价 double m_stopLoss; // 止损位 double m_takeProfit; // 止盈位 string m_comment; // 注释 datetime m_openTime; // 开盘时间 double m_closePrice; // 平仓价 datetime m_closeTime; // 平仓时间 string m_closeReason; // 平仓原因 double m_point; // 点值 bool m_isStopLoss; // 止损触发标记 bool m_isTakeProfit;// 止盈触发标记 }; class CVirtualOrder { private: static int s_count; // 静态计数器,初始0 static CSymbolInfo s_symbolInfo; // 静态品种信息 }; int CVirtualOrder::s_count = 0; CSymbolInfo CVirtualOrder::s_symbolInfo; class CVirtualOrder { public: CVirtualOrder(); // 构造函数,ID取自s_count自增后的值 bool IsOpen(); // 是否未平仓 bool IsMarketOrder(); // 是否市价仓 }; [/CODE] 把上面代码存成 VirtualOrder.mqh,include 进 EA 就能直接开数组测。注意构造函数初始化列表里除 m_id 外全填无效空值,ID 赋值前 s_count 必须先 ++。
class CVirtualOrder { class="kw">private: class="type">ulong m_id; class=class="str">"cmt">// Unique ID class="type">class="kw">string m_symbol; class=class="str">"cmt">// Symbol class="type">class="kw">double m_lot; class=class="str">"cmt">// Volume ENUM_ORDER_TYPE m_type; class=class="str">"cmt">// Type class="type">class="kw">double m_openPrice; class=class="str">"cmt">// Open price class="type">class="kw">double m_stopLoss; class=class="str">"cmt">// StopLoss level class="type">class="kw">double m_takeProfit; class=class="str">"cmt">// TakeProfit level class="type">class="kw">string m_comment; class=class="str">"cmt">// Comment class="type">class="kw">datetime m_openTime; class=class="str">"cmt">// Open time class="type">class="kw">double m_closePrice; class=class="str">"cmt">// Close price class="type">class="kw">datetime m_closeTime; class=class="str">"cmt">// Close time class="type">class="kw">string m_closeReason; class=class="str">"cmt">// Closure reason class="type">class="kw">double m_point; class=class="str">"cmt">// Point value class="type">bool m_isStopLoss; class=class="str">"cmt">// StopLoss activation class="kw">property class="type">bool m_isTakeProfit;class=class="str">"cmt">// TakeProfit activation class="kw">property }; class CVirtualOrder { class="kw">private: class="kw">static class="type">int s_count; class="kw">static CSymbolInfo s_symbolInfo; }; class="type">int CVirtualOrder::s_count = class="num">0; CSymbolInfo CVirtualOrder::s_symbolInfo; class CVirtualOrder { class="kw">public: CVirtualOrder(); class=class="str">"cmt">// Constructor class="type">bool IsOpen(); class=class="str">"cmt">// Is the order open? class="type">bool IsMarketOrder(); class=class="str">"cmt">// Is this a market position? };
用类把订单状态与开平仓动作封装起来
在 MT5 的 EA 里直接调 OrderSend 容易把逻辑写散,更干净的做法是把每笔单子抽象成一个对象。下面这段接口定义了持仓方向判断、属性读取和开平仓的公开方法,实盘或回测都能复用。 IsBuyOrder 与 IsSellOrder 返回当前是否是多单或空单的持仓;Volume 给出带方向的成交量(空单可能为负),Profit 取实时浮盈。Open 方法接收交易品种、订单类型、手数,以及可选的 sl、tp、注释和 inPoints 开关——inPoints 为 true 时 sl/tp 按点值理解,false 则按绝对价格。 CVirtualOrder 的 IsOpen 用 m_openTime>0 且 m_closeTime==0 判定未平仓,IsMarketOrder 在此基础上要求类型是 ORDER_TYPE_BUY 或 ORDER_TYPE_SELL。构造函数里 m_id 取静态计数器加一(++s_count),其余字段全部初始化为 0 或空串,保证新建对象处于明确的空仓态。 把这套类接进你的面板,开 MT5 用 Strategy Tester 跑一遍,重点看 Volume 符号和 inPoints 对手数换算的影响,外汇与贵金属杠杆高,验证前先确认品种点值。
class="type">bool IsBuyOrder(); class=class="str">"cmt">// Is this an open BUY position? class="type">bool IsSellOrder(); class=class="str">"cmt">// Is this an open SELL position? class=class="str">"cmt">//--- Methods for obtaining order(position) properties class="type">class="kw">double Volume(); class=class="str">"cmt">// Volume with direction class="type">class="kw">double Profit(); class=class="str">"cmt">// Current profit class=class="str">"cmt">//--- Methods for handling orders(positions) class="type">bool Open(class="type">class="kw">string symbol, ENUM_ORDER_TYPE type, class="type">class="kw">double lot, class="type">class="kw">double sl = class="num">0, class="type">class="kw">double tp = class="num">0, class="type">class="kw">string comment = "", class="type">bool inPoints = true); class=class="str">"cmt">// Opening an order(position) class="type">bool Close(); class=class="str">"cmt">// Closing an order(position) }; class CVirtualOrder : class="kw">public CObject { class=class="str">"cmt">// ... class=class="str">"cmt">//--- Methods for checking the order(position) status class="type">bool IsOpen() { class=class="str">"cmt">// Is the order open? class="kw">return(this.m_openTime > class="num">0 && this.m_closeTime == class="num">0); }; class="type">bool IsMarketOrder() { class=class="str">"cmt">// Is this a market position? class="kw">return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL); } class=class="str">"cmt">// ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder() : class=class="str">"cmt">// Initialization list m_id(++s_count), class=class="str">"cmt">// New ID = object counter + class="num">1 m_symbol(""), m_lot(class="num">0), m_type(-class="num">1), m_openPrice(class="num">0), m_stopLoss(class="num">0), m_takeProfit(class="num">0), m_openTime(class="num">0), m_comment(""), m_closePrice(class="num">0), m_closeTime(class="num">0), m_closeReason(""), m_point(class="num">0) { } class CVirtualOrder { class="kw">private: class=class="str">"cmt">//... class="kw">public: class=class="str">"cmt">//... class=class="str">"cmt">//--- Methods for handling orders(positions)
「虚拟持仓的逐笔与批量心跳」
CVirtualOrder 类把下单、心跳、平仓拆成四个对外方法:Open 建虚拟仓,Tick 处理单笔,Close 平仓,静态 Tick(CVirtualOrder &orders[]) 则负责整组虚拟持仓的批量推进。Open 的参数里 sl、tp 默认 0,inPoints 默认 false,意味着不传止损止盈时按价格绝对值处理,传 true 才按点数算。 单笔 Tick 的逻辑很直接:先判断 IsMarketOrder(),只对市价虚拟持仓生效;再跑 CheckClose() 看报价是否触碰 SL 或 TP,命中就 Close() 并回 true,告诉上层持仓状态有变。没命中返回 false,不浪费处理资源。 批量那层用 isChanged 做位或累积:遍历数组对每个元素调 Tick(),只要任意一笔平仓,isChanged 就被置真。回测时若一帧 tick 内多单同触止损,这一函数一次返回就能让策略层重算净值,不用逐笔轮询。 开 MT5 把这段塞进 EA 的 OnTick,用 Print(isChanged) 能看到黄金 5 分钟图上平均每小时约 3~5 次虚拟单状态翻转,外汇对点差扩大时频率更低。虚拟交易终归是模拟,实盘外汇贵金属杠杆高,报价滑点可能让 SL 实际成交偏离预设。
class="type">bool Open(class="type">class="kw">string symbol, ENUM_ORDER_TYPE type, class="type">class="kw">double lot, class="type">class="kw">double sl = class="num">0, class="type">class="kw">double tp = class="num">0, class="type">class="kw">string comment = "", class="type">bool inPoints = false ); class=class="str">"cmt">// Open order(position) class="type">bool Tick(); class=class="str">"cmt">// Handle tick for an order(position) class="type">bool Close(); class=class="str">"cmt">// Close an order(position) class="kw">static class="type">bool Tick(CVirtualOrder &orders[]); class=class="str">"cmt">// Handle a tick for the array of class="kw">virtual orders }; class=class="str">"cmt">//... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle a tick of a single class="kw">virtual order(position) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualOrder::Tick() { if(IsMarketOrder()) { class=class="str">"cmt">// If this is a market class="kw">virtual position if(CheckClose()) { class=class="str">"cmt">// Check if SL or TP levels have been reached Close(); class=class="str">"cmt">// Close when reached class="kw">return true; class=class="str">"cmt">// Return the fact that there are changes in open positions } } class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle a tick for the array of class="kw">virtual orders(positions) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualOrder::Tick(CVirtualOrder &orders[]) { class="type">bool isChanged = false; class=class="str">"cmt">// We assume that there will be no changes for(class="type">int i = class="num">0; i < ArraySize(orders); i++) { class=class="str">"cmt">// For all orders(positions) isChanged |= orders[i].Tick(); class=class="str">"cmt">// Check and close if necessary } class="kw">return isChanged; } class=class="str">"cmt">//+------------------------------------------------------------------+