开发回放系统(第 75 部分):新 Chart Trade(二)·进阶篇
📘

开发回放系统(第 75 部分):新 Chart Trade(二)·进阶篇

第 2/3 篇

◍ 模板加载与杠杆换算的实盘坑

面板控件第一次初始化时,代码会用终端 ID 拼出独立 tpl 文件名(如 12345.tpl),并为枚举 0 到 MSG_CLOSE_POSITION 的每个控件写入 size_x / size_y / pos_x / pos_y 四个空值占位。这样多账户同机不会互相覆盖模板,开 MT5 数据文件夹就能看到以数字命名的 tpl。 宏 macro_PointsToFinance 把点数按 VolumeMinimal + VolumeMinimal*(Leverage-1) 再乘 AdjustToTrade 折算成金额。当 m_Info.Leverage 读到 0 或负数时,第 96 行强制兜底为 1,否则杠杆计算会直接崩出 ERR_USER_ERROR_FIRST 并跳过后续赋值。 第 97–98 行对止盈止损金额先取绝对值转回点数、再套宏算回金额,等于按当前杠杆重算一遍风控占用。外汇和贵金属杠杆波动大、强平风险高,改完 Leverage 后务必在策略测试器用不同杠杆值跑一遍,确认 FinanceTake / FinanceStop 数值符合预期。

MQL5 / C++
class="kw">inline class="type">void AdjustTemplate(class="kw">const class="type">bool bFirst = class="kw">false)
  {
  class="macro">#define macro_PointsToFinance(A) A * (GetInfoTerminal().VolumeMinimal + (GetInfoTerminal().VolumeMinimal * (m_Info.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++)
   {
      (*Template).Add(EnumToString(c0), "size_x", NULL);
      (*Template).Add(EnumToString(c0), "size_y", NULL);
      (*Template).Add(EnumToString(c0), "pos_x", NULL);
      (*Template).Add(EnumToString(c0), "pos_y", NULL);
   }
      AdjustEditabled(Template, true);
    }else Template = new C_AdjustTemplate(m_Info.szFileNameTemplate);
    if (_LastError >= ERR_USER_ERROR_FIRST)
    {
      class="kw">delete Template;
      class="kw">return;
    }
   m_Info.Leverage = (m_Info.Leverage <= class="num">0 ? class="num">1 : m_Info.Leverage);
   m_Info.FinanceTake = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceTake), m_Info.Leverage));
   m_Info.FinanceStop = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceStop), m_Info.Leverage));

模板字段回填与界面区域坐标初始化

这段逻辑负责把终端运行态数据写回 UI 模板,并在首次加载时把可交互区域的几何坐标固化到内存结构。099–104 行向 Template 注入品种名、杠杆、止盈止损金额(保留两位小数)、日内交易与最大化状态;若 Execute() 失败则释放模板对象并触发文件访问错误宏。 首次加载分支(111–122 行)遍历 eObjectsIDE 枚举 0 到 MSG_CLOSE_POSITION,把模板里每个控件的 pos_x / pos_y / size_x / size_y 转成 short 存入 m_Info.Regions[],其中标题区宽度被强制对齐为最大化按钮的 x 坐标。 123–124 行依据是否最大化设置图表对象高度(最大化固定 210 像素,否则取标题区高 +6)与水平距离(最大化用 m_Info.x,否则用 m_Info.minx)。开 MT5 把 123 行的 210 改成 180,能直观看到面板纵向压缩效果,外汇与贵金属品种下该界面行为同样受杠杆参数影响,属高风险环境操作。

MQL5 / C++
(*Template).Add("MSG_NAME_SYMBOL", "descr", GetInfoTerminal().szSymbol);
(*Template).Add("MSG_LEVERAGE_VALUE", "descr", IntegerToString(m_Info.Leverage));
(*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.IsDayTrade ? "class="num">1" : "class="num">0"));
(*Template).Add("MSG_MAX_MIN", "state", (m_Info.IsMaximized ? "class="num">1" : "class="num">0"));
if (!(*Template).Execute())
{
   class="kw">delete Template;
   macro_CloseIndicator(C_Terminal::ERR_FileAcess);
};
if (bFirst)
{
   for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++)
   {
      m_Info.Regions[c0].x = (class="type">short) StringToInteger((*Template).Get(EnumToString(c0), "pos_x"));
      m_Info.Regions[c0].y = (class="type">short) StringToInteger((*Template).Get(EnumToString(c0), "pos_y"));
      m_Info.Regions[c0].w = (class="type">short) StringToInteger((*Template).Get(EnumToString(c0), "size_x"));
      m_Info.Regions[c0].h = (class="type">short) StringToInteger((*Template).Get(EnumToString(c0), "size_y"));
   }
   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));

「图表模板套用与鼠标命中区判定」

这段逻辑负责在窗口状态切换后把模板真正落到图表上:第125行先按是否最大化修正编辑框的纵向距离,第129行用 ChartApplyTemplate 从 /Files/ 目录加载模板文件,随后 ChartRedraw 强制重绘,确保界面元素不残留旧布局。 CheckMousePosition 是面板交互的核心。它遍历从 0 到 MSG_CLOSE_POSITION 的枚举区,用当前窗口偏移(最大化走 m_Info.x/y,否则走 minx/miny)加上各区域局部坐标,算出 xi/yi 到 xf/yf 的矩形。若传入的 (x,y) 落在该矩形内,立即返回对应枚举值,否则回 MSG_NULL——这意味着你改 Regions[] 的 w/h 就能直接调整按钮热区大小。 CreateObjectEditable 用模板函数接收任意类型 value,先 DeleteObjectEdit 清掉旧 OBJ_EDIT,再在指定区域偏移 +3 像素处重建可编辑框。外汇与贵金属图表上做这类自定义面板时,注意 MT5 对象坐标系在不同 DPI 下可能有偏差,建议实盘前在策略测试器外接窗口手动点一遍热区验证。

MQL5 / C++
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=class="str">"cmt">//+------------------------------------------------------------------+
   eObjectsIDE CheckMousePosition(class="kw">const class="type">short x, class="kw">const class="type">short 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);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">template <class="kw">typename T >
   class="type">void CreateObjectEditable(eObjectsIDE arg, T value)
   {
      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);

◍ 可编辑面板的像素级定位与状态回灌

在 MT5 自定义指标里,把一块可编辑文本区域钉到图表上,靠的是 ObjectSetInteger 对 OBJPROP_YDISTANCE / XSIZE / YSIZE 的连续赋值。第 164 行把纵向偏移算成 Region.y + m_Info.y + 3,多出的 3 像素是为避免和边框贴死;第 165–166 行直接吃 Region 的宽高,意味着区域尺寸完全由外部配置驱动,改配置不用动绘制逻辑。 字体与对齐也有讲究:第 168 行强制 ALIGN_CENTER,第 169 行把 FontSize 减 1 再下发,实际渲染字号比配置小一号,肉眼看更不容易溢出背景。第 171 行用 typename(T) 判断是不是 double,是就 DoubleToString(value,2) 保留两位小数,否则直接转 string——这套分支让你往面板塞价格或文字都通用。 RestoreState 走的是全局变量回灌:第 181 行用 GlobalVariableGet 取一个 uCast_Double,再把它的四个 16 位字段拆回 x/y/minx/miny(183–186 行)。也就是说面板位置在终端重启后可能从全局变量恢复,而不是每次从头算。 第 187 行按终端 ID 拼出 .tpl 文件名 new 一个模板对象,若 _LastError 大于等于 ERR_USER_ERROR_FIRST 就直接 bRet=false 短路。下一段的 Template.Add 只是往模板挂描述字段,真正写盘不在这几行里,调试时别以为 Add 完就落盘了。

MQL5 / C++
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();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RestoreState(class="type">void)
{
 uCast_Double info;
 class="type">bool bRet;
 C_AdjustTemplate *Template;

 if (bRet = GlobalVariableGet(macro_NameGlobalVariable("POST"), info.dValue))
 {
 m_Info.x = (class="type">short) info._16b[class="num">0];
 m_Info.y = (class="type">short) info._16b[class="num">1];
 m_Info.minx = (class="type">short) info._16b[class="num">2];
 m_Info.miny = (class="type">short) info._16b[class="num">3];
 Template = new C_AdjustTemplate(m_Info.szFileNameTemplate = IntegerToString(GetInfoTerminal().ID) + ".tpl");
 if (_LastError >= ERR_USER_ERROR_FIRST) bRet = class="kw">false; else
 {
 (*Template).Add("MSG_LEVERAGE_VALUE", "descr", NULL);
 (*Template).Add("MSG_TAKE_VALUE", "descr", NULL);

从模板回读持仓面板的状态字段

这段收尾逻辑负责把浮动面板模板里的键值取回,写进 m_Info 结构体。先向模板注册三个输出项:止损描述、日内交易标记、最大最小化标记,随后 Execute() 执行模板并判断是否成功,失败则直接把 bRet 置 false。 成功分支里用 StringToInteger 把 MSG_DAY_TRADE 和 MSG_MAX_MIN 的 state 转成 bool,等于 1 才为真;杠杆、止盈金额、止损金额则分别用 StringToInteger / StringToDouble 从 descr 字段解析,类型强转到 short 和 double。注意 199~201 行取的键名是 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE,而 192 行只注册了 MSG_STOP_VALUE 的 descr,若模板未实际产出前两者,Get 会返回空串,强转后得到 0 或 0.0。 函数末尾 delete Template 并调用 GlobalVariablesDeleteAll 清掉以 macro_NameGlobalVariable("") 为前缀的全部全局变量,最后返回 bRet。外汇与贵金属杠杆波动大、强平风险高,这类面板状态回读仅用于本地风控展示,不预示任何价格走向。 构造函数 C_ChartFloatingRAD 接收短名、鼠标指针和三项金融参数,初始化把 m_Info.IsSaveState 设 false,并立刻跑 IndicatorCheckPass,不通过就直接 return,后续成员处于未就绪状态。

MQL5 / C++
(*Template).Add("MSG_STOP_VALUE", "descr", NULL);
(*Template).Add("MSG_DAY_TRADE", "state", NULL);
(*Template).Add("MSG_MAX_MIN", "state", NULL);
if (!(*Template).Execute()) bRet = class="kw">false; else
{
m_Info.IsDayTrade  = (class="type">bool) StringToInteger((*Template).Get("MSG_DAY_TRADE", "state")) == class="num">1;
m_Info.IsMaximized = (class="type">bool) StringToInteger((*Template).Get("MSG_MAX_MIN", "state")) == class="num">1;
m_Info.Leverage     = (class="type">short)StringToInteger((*Template).Get("MSG_LEVERAGE_VALUE", "descr"));
m_Info.FinanceTake = (class="type">class="kw">double) StringToDouble((*Template).Get("MSG_TAKE_VALUE", "descr"));
m_Info.FinanceStop = (class="type">class="kw">double) StringToDouble((*Template).Get("MSG_STOP_VALUE", "descr"));
}
};
class="kw">delete Template;
};

GlobalVariablesDeleteAll(macro_NameGlobalVariable(""));

class="kw">return bRet;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public  :
class=class="str">"cmt">//+------------------------------------------------------------------+
C_ChartFloatingRAD(class="type">class="kw">string szShortName, C_Mouse *MousePtr, class="kw">const class="type">short Leverage, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">class="kw">double FinanceStop)
:C_Terminal(class="num">0)
{
 m_Mouse = MousePtr;
 m_Info.IsSaveState = class="kw">false;
 if (!IndicatorCheckPass(szShortName)) class="kw">return;

常见问题

检查模板里杠杆字段是否被硬编码,回放系统应按当前账户动态换算,不要直接读模板旧值。
在初始化时显式重算界面区域坐标并绑定窗口尺寸事件,避免只靠模板里存的绝对值。
可以,小布能扫描品种页的面板状态字段并标出未回灌或坐标异常的模块,省去手动逐帧核对。
多为命中区判定用了过期坐标,需在模板套用后重算鼠标命中区并刷新绘制缓存。
先确认模板保存时该字段已写入,加载后做空值兜底并从实时数据补状态,避免面板卡死。