开发回放系统(第 75 部分):新 Chart Trade(二)·进阶篇
◍ 模板加载与杠杆换算的实盘坑
面板控件第一次初始化时,代码会用终端 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 数值符合预期。
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,能直观看到面板纵向压缩效果,外汇与贵金属品种下该界面行为同样受杠杆参数影响,属高风险环境操作。
(*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 下可能有偏差,建议实盘前在策略测试器外接窗口手动点一遍热区验证。
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 完就落盘了。
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,后续成员处于未就绪状态。
(*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;