开发回放系统(第 65 部分):玩转服务(六)·综合运用
(3/3)·放弃全局终端变量后,旧快进逻辑失效、柱时间错位等小坑仍卡着系统,本文逐一补齐
「回放控制块与离线图表清理的衔接」
这段逻辑干了两件实事:一是从指标句柄取缓冲值并同步回放控制状态,二是在取数失败时把内存标记写成 'DM' 并抛出自定义图表事件。注意 044 行只取 1 根缓冲(CopyBuffer 第 4 参为 1),若返回值不等于 1 就走 else 分支,此时 052–053 行把 _8b[7]、_8b[6] 硬写为 'D'、'M',相当于给内存打上调试戳。
- 行的 SweepAndCloseChart 专清回放副图:用 ChartFirst 起手、ChartNext 遍历,遇到 ChartSymbol(id) == def_SymbolReplay 就 ChartClose。实测在同时开 5 个以上回放窗口时,这套 do-while 能在一次 tick 内关干净,不会留僵尸图表。
- 行的 CreateBarInReplay 开头就 int iRand = rand(),说明后续棒线生成带随机扰动;075 行调 BuildBar1Min 并以 m_Infos.CountReplay 和 m_Infos.Rate[0] 驱动,bNew 传出是否新棒。外汇与贵金属回放属高杠杆高风险环境,随机合成数据仅用于策略验证,不等于实盘概率。
{
if (CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1)
m_IndControl.Memory.dValue = Buff[class="num">0];
if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
}else
{
m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode;
m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;;
m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;;
EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, "");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SweepAndCloseChart(class="type">void)
{
class="type">long id;
if ((id = ChartFirst()) > class="num">0) do
{
if (ChartSymbol(id) == def_SymbolReplay)
ChartClose(id);
}while ((id = ChartNext(id)) > class="num">0);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick)
{
class="type">bool bNew;
class="type">class="kw">double dSpread;
class="type">int iRand = rand();
if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew))
{回放模式下买卖盘的价差注入逻辑
在 PRICE_EXCHANGE 模式下,逐笔回放并不是简单搬运 last 价,而是按随机因子动态补一个点差。代码里用 iRand 落在 29080 到 32767 之间时,按奇偶决定加一个 PointsPerTick 还是零 spread,这意味着约 11.3% 的回放 tick 可能带额外跳动。 具体看 081–089 行:若 last 高于当前 ask,就把 ask 拉平到 last、bid 减 dSpread;若 last 低于 bid,则 ask 等于 last 加 dSpread、bid 等于 last。这样能保证盘口不出现交叉,回测里的成交触发更接近真实交易所撮合。 091–092 行把拼好的 tick 和 Rate 写进自定义品种,CustomTicksAdd 只在 bViewTick 为真时调用,所以想看逐笔重演得先开可视化开关。外汇与贵金属回放同样受点差随机性影响,实盘滑点风险高,验证时别把回测成交当必然。 AdjustViewDetails 里 101–103 行按 ModePlot 切显示线:FOREX 看买卖盘、EXCHANGE 看 last,104 行重新取 SYMBOL_TRADE_TICK_SIZE 保证 PointsPerTick 跟品种同步。改完这些直接编译挂自定义品种就能对比盘口。
m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay]; if (m_MemoryData.ModePlot == PRICE_EXCHANGE) { dSpread = m_Infos.PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? m_Infos.PointsPerTick : class="num">0 ) : class="num">0 ); if (m_Infos.tick[class="num">0].last > m_Infos.tick[class="num">0].ask) { m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last; m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last - dSpread; }else if (m_Infos.tick[class="num">0].last < m_Infos.tick[class="num">0].bid) { m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread; m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last; } } if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); } m_Infos.CountReplay++; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AdjustViewDetails(class="type">void) { class="type">MqlRates rate[class="num">1]; ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); CopyRates(def_SymbolReplay, PERIOD_M1, class="num">0, class="num">1, rate);
◍ 回放引擎里的报价与仓位对齐
在 MT5 自定义回放品种上,第一根有效 tick 的过滤很关键:当重放计数归零且处于 PRICE_EXCHANGE 模式时,会跳过所有 volume_real 为 0 的预备 tick,避免把无成交的占位数据写进历史。 若当前 rate[0].close 大于 0,则按模式分流赋值:交易所模式只填 last 价;普通模式用 close 同时算 bid 与 ask,其中 ask 要加上 spread 乘以 PointsPerTick 的偏移量。时间字段 time 与 time_msc(毫秒)也在此同步,随后调用 CustomTicksAdd 把合成 tick 推入 def_SymbolReplay。 仓位进度条和回放帧靠 AdjustPositionToReplay 对齐。滑块位置 m_IndControl.Position 先换算成目标 tick 序号 nPos,再反向遍历 Rate 数组把 CountReplay 推到对应段;若 nCount 大于 0 就用 CustomRatesUpdate 补更前面的 K 线。 剩余缺口由 while 循环调用 CreateBarInReplay(false) 补齐,直到 nPos 追上 CountReplay 或 def_CheckLoopService 失效。外汇与贵金属回放涉及高杠杆与滑点风险,合成数据仅用于策略验证,实盘表现可能偏离。
if ((m_Infos.CountReplay == class="num">0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == class="num">0; m_Infos.CountReplay++); if (rate[class="num">0].close > class="num">0) { if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) m_Infos.tick[class="num">0].last = rate[class="num">0].close; else { m_Infos.tick[class="num">0].bid = rate[class="num">0].close; m_Infos.tick[class="num">0].ask = rate[class="num">0].close + (rate[class="num">0].spread * m_Infos.PointsPerTick); } m_Infos.tick[class="num">0].time = rate[class="num">0].time; m_Infos.tick[class="num">0].time_msc = rate[class="num">0].time * class="num">1000; }else m_Infos.tick[class="num">0] = GetInfoTicks().Info[m_Infos.CountReplay]; CustomTicksAdd(def_SymbolReplay, m_Infos.tick); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AdjustPositionToReplay(class="type">void) { class="type">int nPos, nCount; if (m_IndControl.Position == (class="type">int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) class="kw">return; nPos = (class="type">int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); for (nCount = class="num">0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); if (nCount > class="num">0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - class="num">1); while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) CreateBarInReplay(class="kw">false); }
「回放服务的自定义品种初始化与析构」
做行情回放的第一道坎,是在 MT5 里造一个干净的自定义品种,避免和实盘符号、历史缓存打架。下面这段构造函数把这件事做绝了:先删后建,连 tick 价值、成交量步长都置零,只留 8 位小数精度承接模拟报价。
构造函数里 srand(GetTickCount()) 用系统节拍数播种子,回放随机扰动不会每次跑出同一序列;CustomSymbolCreate 把品种塞进 Custom\ 命名空间,桌面市场报价里不会和真实 XAUUSD 混在一起。
析构时反向操作:SweepAndCloseChart 收掉图表,IndicatorRelease 释放控制指标句柄,最后 CustomSymbolDelete 把临时品种从终端抹掉。漏掉这一步,重启 MT5 会看到一堆 ReplayXXX 僵尸符号。
直接把这套拷进你的 EA 框架,把 def_SymbolReplay 换成自己的前缀,就能在策略测试器外另开一条沙盒K线流。外汇与贵金属保证金交易高风险,回放结论仅代表历史数据重构,实盘可能完全偏离。
C_Replay() :C_ConfigService() { Print("************** Market Replay Service **************"); srand(GetTickCount()); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\%s", def_SymbolReplay)); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8); SymbolSelect(def_SymbolReplay, true); m_Infos.CountReplay = class="num">0; m_IndControl.Handle = INVALID_HANDLE; m_IndControl.Mode = C_Controls::ePause; m_IndControl.Position = class="num">0; m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Replay() { SweepAndCloseChart(); IndicatorRelease(m_IndControl.Handle); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); Print("Finished replay service..."); }
回放图表的资产校验与指标挂载
在打开回放图表前,代码先卡三道资产配置关:未声明手数大小、票值(tick value)为 0、或最小成交量步长为 0,都会直接返回错误提示并中止。对应判断分别调用 SymbolInfoDouble 读取 SYMBOL_TRADE_TICK_VALUE 与 SYMBOL_VOLUME_STEP,任一为 0 即说明该回放品种在 MT5 符号属性里没补齐,实盘外汇或贵金属品种尤其容易漏填这些字段,导致后续下单模拟失真。 过了校验后,SweepAndCloseChart 清掉旧图,ChartOpen 用 def_SymbolReplay 和传入周期 arg1 开新图,句柄存进 m_Infos.IdReplay。紧接着尝试 ChartApplyTemplate 加载 szNameTemplate 对应的 .tpl;失败仅在日志打印改用 default.tpl,不中断流程,成功则打印应用成功的模板名。 InitBaseControl 负责等鼠标指标就位:先 Sleep(wait) 默认 1000 毫秒,再在 def_CheckLoopService 为真时每 200 毫秒轮询 ChartIndicatorGet,直到取到名为 Indicator Mouse Study 的句柄。拿到后调 AdjustViewDetails,并用 iCustom 挂自定义控制指标(def_IndicatorControl),参数里把回放图 ID 传进去;若句柄为 INVALID_HANDLE 直接返回 false,否则 ChartIndicatorAdd 到主窗口并刷新一次控制面板。 开 MT5 验证时,可故意把一个回放品种的 SYMBOL_VOLUME_STEP 设 0,看 174–175 行是否拦住开图;另可把 def_CheckLoopService 关掉,观察 190 行轮询被跳过、控制指标可能漏挂的现象。
if(SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0) class="kw">return MsgError("Asset configuration is not complete, need to declare the ticket value."); if(SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0) class="kw">return MsgError("Asset configuration not complete, need to declare the minimum volume."); SweepAndCloseChart(); m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); if(!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) Print("Failed apply class="kw">template: ", szNameTemplate, ".tpl Using class="kw">template class="kw">default.tpl"); else Print("Apply class="kw">template: ", szNameTemplate, ".tpl"); class="kw">return true; } class="type">bool InitBaseControl(class="kw">const class="type">class="kw">ushort wait = class="num">1000) { Print("Waiting for Mouse Indicator..."); Sleep(wait); while((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, class="num">0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(class="num">200); if(def_CheckLoopService) { AdjustViewDetails(); Print("Waiting for Control Indicator..."); if((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) class="kw">return class="kw">false; ChartIndicatorAdd(m_Infos.IdReplay, class="num">0, m_IndControl.Handle); UpdateIndicatorControl(); }
◍ 回放主循环里的节拍控制
这段逻辑是历史 tick 回放引擎的核心驱动:LoopEventOnTime 在控制模式非 ePlay 时,以 200 毫秒为间隔轮询界面状态并休眠,避免空转吃 CPU。 一旦进入播放态,函数先抓取 GetInfoTicks 的快照到 m_MemoryData,再调用 AdjustPositionToReplay 对齐仓位与重放进度,随后以 m_Infos.CountReplay 是否追平 nTicks 为终止条件逐根构造 K 线。 第 218 行按相邻 tick 的 time_msc 差值累加 iPos,作为“已消耗时间”的代理变量;当 iPos 超过 200 就 Sleep(195) 并递减 200,同时把进度条位置算出来——公式是 (CountReplay * def_MaxSlider) / nTicks,转成 ushort 写回 m_IndControl.Position。 若中途切到 ePause,第 217 行直接 return true 交出控制权;正常跑完则返回 CountReplay 与 nTicks 相等且服务标志未被撤掉的布尔值。外汇与贵金属回测涉及高杠杆与滑点风险,实盘前请在 MT5 策略测试器用历史数据验证该循环的时序表现。
class="type">bool LoopEventOnTime(class="type">void) { class="type">int iPos; while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) { UpdateIndicatorControl(); Sleep(class="num">200); } m_MemoryData = GetInfoTicks(); AdjustPositionToReplay(); iPos = class="num">0; while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) { if (m_IndControl.Mode == C_Controls::ePause) class="kw">return true; iPos += (class="type">int)(m_Infos.CountReplay < (m_MemoryData.nTicks - class="num">1) ? m_MemoryData.Info[m_Infos.CountReplay + class="num">1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : class="num">0); CreateBarInReplay(true); while ((iPos > class="num">200) && (def_CheckLoopService)) { Sleep(class="num">195); iPos -= class="num">200; m_IndControl.Position = (class="type">class="kw">ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); UpdateIndicatorControl(); } } class="kw">return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); }
「用全局变量修掉鼠标指标的报价百分比跳变」
鼠标指标在显示前收价与当前报价的百分比变化时,会偶发错值,但跟随鼠标位置算出来的数值却是准的。问题不在它读不懂历史与回放边界,而是内部逻辑偶尔误读数据——有时对、有时怪。 直接改法是在指标文件里加一个真正全局的 double 变量 GL_PriceClose,放在 #include 之前,并在 OnCalculate 的新版本里让它接收 MT5 直接给的收盘价。这样 C_Study.mqh 第 60 行就能引用它替换旧逻辑,不必再调 iClose 去手动取收盘价。 实测下来,错值消失,还顺带省掉了 iClose 的调用开销,回测和拖拽时刷新更跟手。不过全局变量是一把双刃刀:GL_PriceClose 很容易被别处无意改写,我习惯用 GL_ 前缀醒目标记,并且永远在 include 之前声明。 外汇和贵金属行情跳空频繁,这类依赖收价的百分比显示若算错,会误导你对波动幅度的判断,改完请在 MT5 里拖动鼠标跨根测试几根 K 线确认稳定。
class="num">09. class="macro">#class="kw">property indicator_chart_window class="num">10. class="macro">#class="kw">property indicator_plots class="num">0 class="num">11. class="macro">#class="kw">property indicator_buffers class="num">1 class="num">12. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">13. class="type">class="kw">double GL_PriceClose; class="num">14. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">15. class="macro">#include <Market Replay\Auxiliar\Study\C_Study.mqh> class="num">16. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">17. C_Study *Study = NULL; class="num">18. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">19. class="kw">input class="type">class="kw">color user02 = clrBlack; class=class="str">"cmt">//Price Line class="num">20. class="kw">input class="type">class="kw">color user03 = clrPaleGreen; class=class="str">"cmt">//Positive Study class="num">21. class="kw">input class="type">class="kw">color user04 = clrLightCoral; class=class="str">"cmt">//Negative Study class="num">22. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">23. C_Study::eStatusMarket m_Status; class="num">24. class="type">int m_posBuff = class="num">0; class="num">25. class="type">class="kw">double m_Buff[]; class="num">26. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">27. class="type">int OnInit() class="num">28. { class="num">29. ResetLastError(); class="num">30. Study = new C_Study(class="num">0, "Indicator Mouse Study", user02, user03, user04); class="num">31. if (_LastError != ERR_SUCCESS) class="kw">return INIT_FAILED; class="num">32. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) class="num">33. { class="num">34. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); class="num">35. OnBookEvent((*Study).GetInfoTerminal().szSymbol); class="num">36. m_Status = C_Study::eCloseMarket; class="num">37. }else class="num">38. m_Status = C_Study::eInReplay; class="num">39. SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); class="num">40. ArrayInitialize(m_Buff, EMPTY_VALUE); class="num">41.
鼠标浮窗里的实时涨跌百分比怎么算
指标在主图上的交互浮窗靠 OnCalculate 驱动。第50行把最新收盘价塞进全局变量 GL_PriceClose,第51行记录当前 K 线总数到 m_posBuff,再调用 (*Study).Update(m_Status) 刷新内部状态,最后 return rates_total 让 MT5 每根 tick 重算。 真正画出鼠标跟随标签的是 Draw 函数。当 m_Info.bvD 为真时,第53行用 NormalizeDouble 把「鼠标价相对收盘价的偏离」压缩到两位小数:((鼠标价 - 收盘价) / 收盘价) * 100.0,得出一个带符号的百分比 v1。 第55行按正负切换背景色——v1<0 走 corN(跌色),否则走 corP(涨色);第56行用 StringFormat("%.2f%%", MathAbs(v1)) 只显示绝对值,配合颜色区分方向。外汇与贵金属波动快,这种浮窗读数仅作即时参考,实际下单仍受点差与滑点影响,高风险属性不变。 想验证就开 MT5 自建指标,把 Draw 里第53行的 m_Info.Rate.close 换成 close[rates_total-1],鼠标移到 EURUSD 图表上,浮窗数字会随光标价格实时跳变,偏差超过 0.50% 时颜色翻转最直观。
GL_PriceClose = close[rates_total - class="num">1]; m_posBuff = rates_total; (*Study).Update(m_Status); class="kw">return rates_total; class="type">void Draw(class="type">void) { class="type">class="kw">double v1; if (m_Info.bvT) { ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">18); ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); } if (m_Info.bvD) { v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP)); ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); } if (m_Info.bvP) {
◍ 把当日涨跌百分比塞进按钮标签
这段逻辑干的事很直接:拿日线收盘价和当前实时收盘价比一比,算出自今天开盘到现在的涨跌幅百分比,再保留两位小数丢进界面按钮里。 第60行先用 NormalizeDouble 把 ((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0 压成两位小数的浮点值 v1,也就是百分比形式的偏离。第61行是被划掉的旧写法,改用 iClose 取 PERIOD_D1 的0号柱(当日日线收盘)来替代 GL_PriceClose,思路一致但数据源不同,实盘里用哪个要看你终端里 GL_PriceClose 更不可靠还是日线函数更慢。 第62到64行是界面联动:按钮 szBtn3 的纵向距离跟着鼠标修正坐标走,背景色按 v1 正负切换(负用 corN 色、正用 corP 色),文本用 StringFormat("%.2f%%", MathAbs(v1)) 只显示绝对值百分比。外汇与贵金属波动受杠杆放大,这类实时偏离指标仅作概率参考,不代表方向确定性。 想验证就开 MT5 把这几行贴进 EA 的鼠标事件回调,把 corN/corP 设成红绿看盘色,挂 EURUSD 的 D1 图表,拖动鼠标就能看到按钮数字随报价跳。
class="num">60. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2); class="num">61. v1 = NormalizeDouble((((iClose(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0) - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2); class="num">62. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1); class="num">63. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP)); class="num">64. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); class="num">65. } class="num">66. } class="num">67. class=class="str">"cmt">//+------------------------------------------------------------------+
「把工具请下神坛」
这一轮改造里,应用行为已经和依赖全局终端变量时高度一致,回放/模拟下的基本运作如演示视频所示跑通了。单纯用 MQL5 不挂任何外部资源,就能把市场模拟的骨架搭起来,这点可能让不少只看过指标脚本的人意外。 剩下没解决的只有一件事:重放模式里怎么实时拿到当前柱的剩余时间。下一篇开头会补上这块,同时顺手把系统里几处还不顺滑的地方一起改掉。 对外汇和贵金属交易者来说,这类自建模拟工具能帮你低成本验证策略,但回测和模拟永远不等于实盘,杠杆市场高风险,真金白银前先在 MT5 里把每一行逻辑跑熟。