开发回放系统(第 47 部分):Chart Trade 项目(六)·进阶篇
(2/3)· 只改几行类代码,就能让回放指标像普通指标一样被 EA 调用,省掉外接数据的麻烦
很多人把 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,后续向子图发消息或改背景都得靠这个句柄。外汇与贵金属杠杆高,面板逻辑写错可能卡死图表,建议先在策略测试器里用小周期验证。
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 数组是否按模板填实,比手填坐标快得多。
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,再检查止盈点数是否随杠杆线性变化。
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 的数值仅反映当前持仓测算,实盘滑点可能让显示值偏离。
(*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 值即可。
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 置空表示本次交互结束。外汇与贵金属图表上跑这类自定义面板属于高风险操作,参数错位可能让输入框飞出可视区。
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;