开发回放系统(第 47 部分):Chart Trade 项目(六)·进阶篇
📊

开发回放系统(第 47 部分):Chart Trade 项目(六)·进阶篇

(2/3)· 只改几行类代码,就能让回放指标像普通指标一样被 EA 调用,省掉外接数据的麻烦

进阶 第 2/3 篇

很多人把 Chart Trade 只当成画线的辅助面板,EA 想读它的状态就得另写一套通信。其实只要在指标里开一个数据缓冲区,回放系统就能直接被策略当作普通指标来访问,这一步没做,后面接 EA 全是绕路。

子图对象的尺寸与坐标轴锁定

在 MT5 里用 ObjectSetInteger 给图表子图对象设属性时,YDISTANCE、XSIZE、YSIZE 三个整型参数直接决定它在主图上的位置和像素占位。057~059 行把纵向偏移、宽、高分别写入 m_Info 结构,跑完这一步子图才不会出现错位或拉伸。

  • 和 061 行把 OBJPROP_DATE_SCALE 与 OBJPROP_PRICE_SCALE 都置为 false,等于关掉子图自己的时间轴和价格轴。做overlay面板时若不关,会出现双轴挤压主图可视区,EURUSD 这类报价跳动快的品种上尤其明显。
  • 行用 ObjectGetInteger 取 OBJPROP_CHART_ID 存进 WinHandle,后续向子图发消息或改背景都得靠这个句柄。外汇与贵金属杠杆高,面板逻辑写错可能卡死图表,建议先在策略测试器里用小周期验证。
MQL5 / C++
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, m_Info.y);
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XSIZE, w);
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, h);
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_DATE_SCALE, class="kw">false);
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_PRICE_SCALE, class="kw">false);
m_Info.WinHandle = ObjectGetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_CHART_ID);

◍ 用模板宏批量接管面板坐标与字体

面板区域的外观参数如果逐个手写赋值,维护成本会随区域数量线性膨胀。下面这段把颜色、字号、字体名从模板对象里按枚举键取出,一次循环灌进 m_Info.Regions 数组,c0 遍历到第 N 个区域就自动适配第 N 套样式。 076~078 行在循环体内分别取 bgcolor、fontsz、fontnm 三个字段:前两个用 StringToInteger 强转,字体名直接存字符串。MT5 里 Template.Get 返回的是文本,不转类型编译都过不了。 更省事的是 AdjustTemplate 里的两个宏。macro_AddAdjust 一口气给某个区域 A 注册 size_x / size_y / pos_x / pos_y 四个空字段;macro_GetAdjust 则反向把这四个字段读回 Regions[A] 的 x、y、w、h。外汇与贵金属 GUI 工具多为高波动品种设计,面板错位可能影响下单判断,坐标参数建议实盘前在策略测试器里逐项核对。 直接把这两段抄进你的指标工程,把 c0 换成你自己的区域枚举,开 MT5 编译看 Regions 数组是否按模板填实,比手填坐标快得多。

MQL5 / C++
m_Info.Regions[c0].bgcolor = (class="type">class="kw">color) StringToInteger(Template.Get(EnumToString(c0), "bgcolor"));
m_Info.Regions[c0].FontSize = (class="type">int) StringToInteger(Template.Get(EnumToString(c0), "fontsz"));
m_Info.Regions[c0].FontName = Template.Get(EnumToString(c0), "fontnm");
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void AdjustTemplate(class="kw">const class="type">bool bFirst = class="kw">false)
	{
class="macro">#define macro_AddAdjust(A)      {               \
   (*Template).Add(A, "size_x", NULL);     \
   (*Template).Add(A, "size_y", NULL);     \
   (*Template).Add(A, "pos_x", NULL);      \
   (*Template).Add(A, "pos_y", NULL);      \
class="macro">#define macro_GetAdjust(A) {               \
   m_Info.Regions[A].x = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "pos_x"));   \
   m_Info.Regions[A].y = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "pos_y"));   \
   m_Info.Regions[A].w = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "size_x"));

「模板里的手数换算与杠杆兜底」

这段逻辑干两件事:从图表模板 tpl 里读区域尺寸,以及把「金额」和「点数」按杠杆互相折算。外汇和贵金属波动大、杠杆放大盈亏,下面宏和分支都只是防除零和负杠杆,不代表任何收益保证。 macro_PointsToFinance 把点数 A 乘上「最小交易量 + 最小交易量×(杠杆-1)」再乘合约调整系数。杠杆若为 1,就退化成纯最小手数对应的金额;杠杆 100 时,系数约为最小手数的 100 倍,仓位敏感度高,风险相应放大。 代码里 m_Info.ConfigChartTrade.Leverage 若 ≤0 会被强制置 1(行106),避免后续折算出现负或无穷。紧接着行107–110 用 MathAbs 取止盈止损金额绝对值后转点数、再转回金额,确保模板里写的 Take/Stop 都是非负且杠杆一致。 首次加载时(bFirst)以终端 ID 命名新建 tpl 并循环写入 MSG_CLOSE_POSITION 以内所有对象(行102–104);非首次直接读已有文件。想验证,把 Leverage 改成 0 重跑,看行106 是否把它拉回 1,再检查止盈点数是否随杠杆线性变化。

MQL5 / C++
m_Info.Regions[A].h = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "size_y"));
}
class="macro">#define macro_PointsToFinance(A) A * (GetInfoTerminal().VolumeMinimal + (GetInfoTerminal().VolumeMinimal * (m_Info.ConfigChartTrade.Leverage - class="num">1))) * GetInfoTerminal().AdjustToTrade


C_AdjustTemplate  *Template;

if (bFirst)
{
  Template = new C_AdjustTemplate(m_Info.szFileNameTemplate = IntegerToString(GetInfoTerminal().ID) + ".tpl", true);
  for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) macro_AddAdjust(EnumToString(c0));
  AdjustEditabled(Template, true);
}else Template = new C_AdjustTemplate(m_Info.szFileNameTemplate);
m_Info.ConfigChartTrade.Leverage = (m_Info.ConfigChartTrade.Leverage <= class="num">0 ? class="num">1 : m_Info.ConfigChartTrade.Leverage);
m_Info.FinanceTake = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceTake), m_Info.ConfigChartTrade.Leverage));
m_Info.FinanceStop = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceStop), m_Info.ConfigChartTrade.Leverage));
m_Info.ConfigChartTrade.PointsTake = FinanceToPoints(m_Info.FinanceTake, m_Info.ConfigChartTrade.Leverage);
m_Info.ConfigChartTrade.PointsStop = FinanceToPoints(m_Info.FinanceStop, m_Info.ConfigChartTrade.Leverage);
(*Template).Add("MSG_NAME_SYMBOL", "descr", GetInfoTerminal().szSymbol);
(*Template).Add("MSG_LEVERAGE_VALUE", "descr", IntegerToString(m_Info.ConfigChartTrade.Leverage));

面板渲染时的字段注入与窗口自适应

这段逻辑发生在交易面板对象刷新阶段,先把盈亏与状态量写进模板,再按最大化标志重算图表子窗口几何。 Template->Add("MSG_TAKE_VALUE","descr",DoubleToString(m_Info.FinanceTake,2)); 把浮动盈利额格式化为两位小数塞进模板描述位;MSG_STOP_VALUE 同理处理浮动亏损额。DayTrade 与 MaxMin 两个状态用 "1"/"0" 字符串标记,供前端判断是否显示日内交易角标或最大化布局。 首次绘制时(bFirst 为真)会遍历 eObjectsIDE 枚举从 0 到 MSG_CLOSE_POSITION 调用 macro_GetAdjust 重算各控件坐标,并把标题区宽度对齐到最大化按钮的 x 位置;随后 AdjustEditabled(Template,false) 锁定编辑态,避免误拖。 窗口尺寸走的是硬参数:最大化时 OBJPROP_YSIZE 固定 210 像素,否则取标题区高度加 6 像素余量;XDISTANCE/YDISTANCE 在最大化时用 m_Info.x/y,否则回落到 minx/miny。最后 delete Template 释放、ChartApplyTemplate 套用 /Files/ 下的模板文件并 ChartRedraw 强制重绘。 外汇与贵金属品种点值随杠杆和合约而异,FinanceTake/FinanceStop 的数值仅反映当前持仓测算,实盘滑点可能让显示值偏离。

MQL5 / C++
(*Template).Add("MSG_TAKE_VALUE", "descr", DoubleToString(m_Info.FinanceTake, class="num">2));
(*Template).Add("MSG_STOP_VALUE", "descr", DoubleToString(m_Info.FinanceStop, class="num">2));
(*Template).Add("MSG_DAY_TRADE", "state", (m_Info.ConfigChartTrade.IsDayTrade ? "class="num">1" : "class="num">0"));
(*Template).Add("MSG_MAX_MIN", "state", (m_Info.IsMaximized ? "class="num">1" : "class="num">0"));
(*Template).Execute();
if (bFirst)
{
  for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) macro_GetAdjust(c0);
  m_Info.Regions[MSG_TITLE_IDE].w = m_Info.Regions[MSG_MAX_MIN].x;
  AdjustEditabled(Template, class="kw">false);
};
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, (m_Info.IsMaximized ? class="num">210 : m_Info.Regions[MSG_TITLE_IDE].h + class="num">6));
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, (m_Info.IsMaximized ? m_Info.x : m_Info.minx));
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, (m_Info.IsMaximized ? m_Info.y : m_Info.miny));

class="kw">delete Template;

ChartApplyTemplate(m_Info.WinHandle, "/Files/" + m_Info.szFileNameTemplate);
ChartRedraw(m_Info.WinHandle);

class="macro">#undef macro_PointsToFinance
class="macro">#undef macro_GetAdjust

◍ 命中区域判定与可编辑对象的清理重建

面板交互里最容易被忽略的一步,是鼠标坐标落在哪个控件区域。CheckMousePosition 用一组矩形边界做点包容测试:先把窗口是否最大化算进去,取区域左上角 (xi, yi) 和右下角 (xf, yf),只要传入的 (x, y) 满足 xi<x<xf 且 yi<y<yf 就返回对应枚举,否则回 MSG_NULL。 循环上界写到 MSG_CLOSE_POSITION,意味着区域表 m_Info.Regions[] 至少覆盖了从 0 到该枚举的所有交互区;若你以后增删控件,枚举顺序和区域数组长度必须同步,否则第 142 行的 c0<=MSG_CLOSE_POSITION 会越界或漏判。 DeleteObjectEdit 负责退出编辑态:先 ChartRedraw 强制重绘,再用 ObjectsDeleteAll 按对象名前缀 m_Info.szObj_Editable 清掉所有可编辑图形,最后把消息状态归零。外汇与贵金属图表上跑这类自绘面板属高风险操作,参数误删可能清掉你正在观察的挂单线。 CreateObjectEditable 是模板函数,第 160 行 template<typename T> 让同一个入口既能接 string 也能接 double/int,具体创建逻辑在 162 行函数体内延续。想验证命中逻辑,把 144–148 行抄进 EA 的 OnChartEvent,用 Comment 打印返回的 c0 值即可。

MQL5 / C++
class="macro">#undef macro_AddAdjust
               }
class=class="str">"cmt">//+------------------------------------------------------------------+
               eObjectsIDE CheckMousePosition(class="kw">const class="type">int x, class="kw">const class="type">int y)
                  {
                  class="type">int xi, yi, xf, yf;
                  
                  for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++)
                     {
                     xi = (m_Info.IsMaximized ? m_Info.x : m_Info.minx) + m_Info.Regions[c0].x;
                     yi = (m_Info.IsMaximized ? m_Info.y : m_Info.miny) + m_Info.Regions[c0].y;
                     xf = xi + m_Info.Regions[c0].w;
                     yf = yi + m_Info.Regions[c0].h;
                     if ((x > xi) && (y > yi) && (x < xf) && (y < yf)) class="kw">return c0;
                     }
                  class="kw">return MSG_NULL;
                  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void DeleteObjectEdit(class="type">void)
                  {
                  ChartRedraw();
                  ObjectsDeleteAll(GetInfoTerminal().ID, m_Info.szObj_Editable);
                  m_Info.ConfigChartTrade.Msg = MSG_NULL;
                  }
class=class="str">"cmt">//+------------------------------------------------------------------+
               class="kw">template <class="kw">typename T >
               class="type">void CreateObjectEditable(eObjectsIDE arg, T value)
                  {

「可编辑文本框的落点与回写」

在面板区域里塞一个可编辑框,核心是先拿终端 ID 再清旧对象。GetInfoTerminal().ID 拿到图表所属终端句柄,DeleteObjectEdit() 把上一轮残留的编辑框删掉,避免叠图。 CreateObjectGraphics 以 OBJ_EDIT 类型新建黑色编辑框后,位置由区域坐标加 3 像素偏移确定:X 距 = Regions[arg].x + m_Info.x + 3,Y 距同理。宽高直接吃区域的 w 和 h,背景色用区域自身 bgcolor,这样框体视觉上嵌进面板格子。 字号比区域设定小 1(FontSize - 1),对齐走 ALIGN_CENTER,字体名从 Regions[arg].FontName 取。文本内容做了类型分支:double 走 DoubleToString(value, 2) 保留两位小数,其它类型直接强转 string,这一行决定了你填进去的数字会不会被截精度。 最后 ChartRedraw() 强制重绘,ConfigChartTrade.Msg 置空表示本次交互结束。外汇与贵金属图表上跑这类自定义面板属于高风险操作,参数错位可能让输入框飞出可视区。

MQL5 / C++
class="type">long id = GetInfoTerminal().ID;

DeleteObjectEdit();
CreateObjectGraphics(m_Info.szObj_Editable, OBJ_EDIT, clrBlack, class="num">0);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_XDISTANCE, m_Info.Regions[arg].x + m_Info.x + class="num">3);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_YDISTANCE, m_Info.Regions[arg].y + m_Info.y + class="num">3);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_XSIZE, m_Info.Regions[arg].w);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_YSIZE, m_Info.Regions[arg].h);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_BGCOLOR, m_Info.Regions[arg].bgcolor);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_ALIGN, ALIGN_CENTER);
ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_FONTSIZE, m_Info.Regions[arg].FontSize - class="num">1);
ObjectSetString(id, m_Info.szObj_Editable, OBJPROP_FONT, m_Info.Regions[arg].FontName);
ObjectSetString(id, m_Info.szObj_Editable, OBJPROP_TEXT, (class="kw">typename(T) == "class="type">class="kw">double" ? DoubleToString(value, class="num">2) : (class="type">class="kw">string) value));
ChartRedraw();
m_Info.ConfigChartTrade.Msg = MSG_NULL;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RestoreState(class="type">void)
  {
   uCast_Double info;
让小布替你跑这套回放校验
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到指标缓冲区是否按预期刷新,把重复劳动交给小布,你专注决策。

常见问题

因为 EA 通过 iCustom 访问指标时只需定位到指定 buffer 索引,单缓冲区足以承载命令或状态信号,多余缓冲区反而增加维护成本。
修改后类暴露了更直接的数据访问接口,EA 不再依赖全局变量或文件中转,调用延迟和出错概率都会下降。
那三篇铺好了缓冲区与回放内核的底层约定,跳过会导致不理解 buffer 初始化为零值的必要性,以及指标如何模拟实时 tick。
可以,把编译好的指标放到终端对应目录后,小布盯盘的 AIGC 诊断层会识别其 buffer 结构并在品种页给出刷新状态,不需手动盯盘口。
外汇贵金属属高风险,回放数据若来自历史 tick 模拟,实盘可能因点差和滑点出现偏差,策略信号倾向需重新评估概率而非直接照搬。