开发多币种 EA 交易(第 4 部分):虚拟挂单和保存状态·进阶篇
(2/3)· 为什么终端重启后 EA 总从零开始,以及怎样用虚拟挂单补齐策略执行的可视化盲区
◍ 挂单方向与价格精度的配对逻辑
这段代码片段展示了根据 m_openDistance 符号决定挂单类型的分支结构:当距离小于 0 时,系统下达 BUY_LIMIT 单;否则若等于 0 则直接开虚拟 BUY 仓位。外汇与贵金属杠杆高,挂单触发受滑点影响,实盘可能偏离预期。 所有挂单的 price、sl、tp 都经过 NormalizeDouble(value, digits) 处理,目的是把价格对齐到品种小数位,避免 MT5 因精度不符拒绝成交。digits 通常来自 SymbolInfoInteger(_Symbol, SYMBOL_DIGITS),对 XAUUSD 多为 2,对 EURUSD 多为 5。 注意 BUY_LIMIT 分支里 sl、tp 同样用 NormalizeDouble 包裹,而市价开仓分支传了 0 作为 price 占位,由经纪商按市价撮合。复制这段代码到 MT5 策略测试器,改 m_openDistance 阈值,能直接观察不同分支的订单发送结果。
NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), "", expiration); } else if(m_openDistance < class="num">0) { class=class="str">"cmt">// Set SELL LIMIT pending order res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_LIMIT, m_fixedLot, NormalizeDouble(price, digits), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), "", expiration); } else { class=class="str">"cmt">// Open a class="kw">virtual SELL position res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, class="num">0, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); } class="kw">break; class=class="str">"cmt">// and exit
「EA 状态落地:只存虚拟仓位还不够」
我们基于同一套类做了两个 EA:SimpleVolumesExpertSingle.mq5 只用来在策略测试器里优化单实例参数,SimpleVolumesExpert.mq5 内嵌多份策略副本、用前者的优参跑实盘。以后真账户只可能上第二个,所以状态保存只针对它——里面要涵盖几十个策略实例的运行时信息。 这里说的状态不是『策略集合长什么样』,那是写死的:每次启动 EA 里带指定参数的策略实例都一样。状态指的是启动后开的真实/虚拟仓、以及按价格算出来的中间量。终端重启后,EA 得认回自己的真实仓,并把虚拟仓和算值恢复出来;真实仓能从终端读,虚拟仓和算值必须自己落盘。 以这个简单策略为例,它不累积计算数据,状态完全由一组虚拟仓位+挂单构成。但光把 CVirtualOrder 对象数组写文件会出事:设想你跑了几个不同策略的 EA,每个都用 9 个策略实例 × 3 个虚拟仓对象 = 27 个 CVirtualOrder。万一某个 EA 把别的 EA 的 27 个对象读进来了,状态就串台了。 防串台的办法是在文件里带上 EA 自身参数、以及作为组件的策略参数,相当于给状态盖个戳。保存时机上,策略参数恒定不用老写;虚拟仓属性只在开/平操作后才变,所以那之后存一次就够了,每个 tick 或定时器硬存是多余开销。 实现上别把所有保存逻辑堆在 EA 顶层。对象是分层结构:EA → 策略 → 虚拟仓位。每一层都给自己加 Save() / Load(),顶层负责开文件,下层拿已开文件的描述符接着写。这样文件名只在 EA 层定一次,策略和虚拟仓层不操心 IO。
给虚拟仓位加个断点续跑的存档
CVirtualAdvisor 里新增两个成员:m_name 存 EA 名(构造时写死,可视化测试下拼上幻数和 .test 后缀),m_lastSaveTime 记上次落盘时间。仅在虚拟仓位组成或实际交易量修正后才值得写文件,所以保存前要先比大小——若 m_lastSaveTime 小于接收者对象里的“最后一次变动时间”,说明状态脏了,才执行保存。 非可视化模式的单测或优化直接跳过保存;可视模式无论优化还是单测都落盘,这样策略测试器里也能验证文件写入对不对。EA 层 Save() 先写当前时间与策略数,再循环调各策略的保存,最后用虚拟仓位变动时间刷新 m_lastSaveTime,下次变动前不再写任何字节。 Load() 是反向操作:先读保存时间和策略数,若与 EA 内已挂策略数量不一致就判定文件错误直接退出,一致才逐策略委托读取。注意不能在构造函数里调 Load(),因为那时策略实例还没挂进 EA——必须等 OnInit() 里全部添加完毕再调一次。 下面这段类声明与构造、Tick 逻辑可以直接抄进 VirtualAdvisor.mqh:构造里用 StringFormat 拼出 "名称-幻数.test.csv" 这类文件名,Tick() 末尾挂 Save() 调用,非可视测试自动不吃后缀。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders) |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CVirtualAdvisor : <span class="keyword">class="kw">public</span> CAdvisor { <span class="keyword">class="kw">protected</span>: ... <span class="keyword">class="type">class="kw">string</span> m_name; <span class="comment">class=class="str">"cmt">// EA name</span> <span class="keyword">class="type">class="kw">datetime</span> m_lastSaveTime; <span class="comment">class=class="str">"cmt">// Last save time</span> <span class="keyword">class="kw">public</span>: CVirtualAdvisor(<span class="keyword">class="type">class="kw">ulong</span> p_magic = <span class="number">class="num">1</span>, <span class="keyword">class="type">class="kw">string</span> p_name = <span class="class="type">class="kw">string">""</span>); <span class="comment">class=class="str">"cmt">// Constructor</span> ... <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Save(); <span class="comment">class=class="str">"cmt">// Save status</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Load(); <span class="comment">class=class="str">"cmt">// Load status</span> }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Constructor |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> CVirtualAdvisor::CVirtualAdvisor(<span class="keyword">class="type">class="kw">ulong</span> p_magic = <span class="number">class="num">1</span>, <span class="keyword">class="type">class="kw">string</span> p_name = <span class="class="type">class="kw">string">""</span>) : ... m_lastSaveTime(<span class="number">class="num">0</span>) { m_name = <span class="functions">StringFormat</span>(<span class="class="type">class="kw">string">"%s-%d%s.csv"</span>, (p_name != <span class="class="type">class="kw">string">""</span> ? p_name : <span class="class="type">class="kw">string">"Expert"</span>), p_magic, (<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>) ? <span class="class="type">class="kw">string">".test"</span> : <span class="class="type">class="kw">string">""</span>) ); }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| OnTick event handler |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CVirtualAdvisor::Tick(<span class="keyword">class="type">void</span>) { <span class="comment">class=class="str">"cmt">// Receiver handles class="kw">virtual positions</span> m_receiver.Tick(); <span class="comment">class=class="str">"cmt">// Start handling in strategies</span> CAdvisor::Tick(); <span class="comment">class=class="str">"cmt">// Adjusting market volumes</span> m_receiver.Correct(); <span class="comment">class=class="str">"cmt">// Save status</span> <span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);">Save()</span>; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Save status |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> CVirtualAdvisor::Save() {
◍ 状态落盘与重载的触发边界
虚拟策略跑在 MT5 里最怕一件事:终端重启后仓位逻辑全丢。这段实现把策略状态写进以 tab 分隔的 CSV 文件,只在「距上次改动时间有新变更、且不在优化模式、且非静默回测(或正开可视化测试)」时落盘,避免回测噪声污染实盘状态文件。 保存函数先用 FileOpen 以 FILE_WRITE 打开同名文件,依次写入最后变更时间、策略数组长度,再遍历每个 CVirtualStrategy 调用各自 Save。写完后把 m_lastSaveTime 同步为 s_lastChangeTime,并打印带时分秒的 OK 日志;打开失败则置 res=false 并输出 LastError,方便你直接在专家日志里抓文件权限或路径问题。 加载侧对称:FileIsExist 命中且同样避开优化/静默测试时,以 FILE_READ 打开并 FileReadDatetime 读回 m_lastSaveTime。这套判定意味着你若在策略测试器里跑非可视化回测,状态既不会存也不会读——想验证持久化逻辑,必须开可视化模式或直接在实盘/模拟盘环境跑。外汇与贵金属杠杆高,状态误载可能引发重复下单,上线前务必用可视化测试过一遍读写。
class="type">bool res = true; class=class="str">"cmt">// Save status if: if(true class=class="str">"cmt">// later changes appeared && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime class=class="str">"cmt">// currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) class=class="str">"cmt">// and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { class="type">int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, &class="macro">#x27;\t&class="macro">#x27;); if(f != INVALID_HANDLE) { class=class="str">"cmt">// If file is open, save FileWrite(f, CVirtualReceiver::s_lastChangeTime); class=class="str">"cmt">// Time of last changes FileWrite(f, ArraySize(m_strategies)); class=class="str">"cmt">// Number of strategies class=class="str">"cmt">// All strategies FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f)); FileClose(f); class=class="str">"cmt">// Update the last save time m_lastSaveTime = CVirtualReceiver::s_lastChangeTime; PrintFormat(__FUNCTION__" | OK at %s to %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Load status | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualAdvisor::Load() { class="type">bool res = true; class=class="str">"cmt">// Load status if: if(true class=class="str">"cmt">// file exists && FileIsExist(m_name) class=class="str">"cmt">// currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) class=class="str">"cmt">// and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { class="type">int f = FileOpen(m_name, FILE_CSV | FILE_READ, &class="macro">#x27;\t&class="macro">#x27;); if(f != INVALID_HANDLE) { class=class="str">"cmt">// If the file is open, then load m_lastSaveTime = FileReadDatetime(f); class=class="str">"cmt">// Last save time
「从文件复原策略状态的那几行关键代码」
EA 重启最怕丢状态:挂着的虚拟仓位、策略内部计数全没,等于裸奔。上面这段把上次保存的时间戳和策略数量先读出来,再逐条 Load,不匹配就直接报错退出,逻辑很硬。 注意 f_strategiesCount 是用 StringToInteger(FileReadString(f)) 拿的,文件里第一串就是策略数;紧接着拿 ArraySize(m_strategies) 做相等判断,不一致就打印『expected but found』。这一步若不过,后面 Load 根本不会跑。 OnInit 里写死了 CStrategy *strategies[9],下标 0 到 8 共 9 个实例,然后 new CVirtualAdvisor 并 FOREACH 添加,最后 expert.Load() 尝试恢复。外汇与贵金属杠杆高,状态恢复失败可能让仓位管理逻辑错乱,实盘前务必在 MT5 策略测试器里断点跟一遍 Load 返回值。
PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); class=class="str">"cmt">// Number of strategies class="type">long f_strategiesCount = StringToInteger(FileReadString(f)); class=class="str">"cmt">// Does the loaded number of strategies match the current one? res = (ArraySize(m_strategies) == f_strategiesCount); if(res) { class=class="str">"cmt">// Load all strategies FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).Load(f)); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name); } } else { PrintFormat(__FUNCTION__" | ERROR: Wrong strategies count(%d expected but %d found in file %s)", ArraySize(m_strategies), f_strategiesCount, m_name); } FileClose(f); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } class="kw">return res; } CVirtualAdvisor *expert; class=class="str">"cmt">// EA object class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Create and fill the array of strategy instances CStrategy *strategies[class="num">9]; strategies[class="num">0] = ... ... strategies[class="num">8] = ... class=class="str">"cmt">// Create an EA handling class="kw">virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes"); class=class="str">"cmt">// Add strategies to the EA FOREACH(strategies, expert.Add(strategies[i])); class=class="str">"cmt">// Load the previous state if available expert.Load(); class="kw">return(INIT_SUCCEEDED); }
给策略类补上存取和序列化
在交易策略基类里加三个东西就能让虚拟策略可持久化:Save() 把状态写进文件,Load() 按原顺序读回,~ 重载成一元运算符把对象压成字符串。字符串格式直接是「类名(虚拟仓位数组长度)」,例如 CVirtualStrategy(3) 表示当前挂了 3 个虚拟仓位。 写入时先 FileWrite 把 ~this 的字符串落盘,再用 FOREACH 挨个调每个虚拟仓位的 Save()。读取端先做一道校验:只有文件里读出的参数字符串和 ~this 完全相等,才继续 Load 每个仓位,避免版本错配把历史状态塞进新逻辑。 改动落地的文件是工作目录下的 VirtualStrategy.mqh。MT5 里开个脚本循环跑 Save/Load,断点看 m_orders 数组长度是否和字符串里的数字对得上,就能验证这套机制没漂。外汇与贵金属品种波动大,虚拟回测和实盘状态分离存储时尤其要防参数串档。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of a trading strategy with class="kw">virtual positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualStrategy : class="kw">public CStrategy { ... class="kw">public: ... class="kw">virtual class="type">bool Load(const class="type">int f); class=class="str">"cmt">// Load status class="kw">virtual class="type">bool Save(const class="type">int f); class=class="str">"cmt">// Save status class="type">class="kw">string class="kw">operator~(); class=class="str">"cmt">// Convert object to class="type">class="kw">string }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Convert an object to a class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CVirtualStrategy::class="kw">operator~() { class="kw">return StringFormat("%s(%d)", class="kw">typename(this), ArraySize(m_orders)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Save status | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualStrategy::Save(const class="type">int f) { class="type">bool res = true; FileWrite(f, ~this); class=class="str">"cmt">// Save parameters class=class="str">"cmt">// Save class="kw">virtual positions(orders) of the strategy FOREACH(m_orders, res &= m_orders[i].Save(f)); class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Load status | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualStrategy::Load(const class="type">int f) { class="type">bool res = true; class=class="str">"cmt">// Current parameters are equal to read parameters res = (~this == FileReadString(f)); class=class="str">"cmt">// If yes, then load the class="kw">virtual positions(orders) of the strategy if(res) { FOREACH(m_orders, res &= m_orders[i].Load(f)); } class="kw">return res; }