从头开始开发智能交易系统(第 8 部分):概念上的飞跃·进阶篇
🧩

从头开始开发智能交易系统(第 8 部分):概念上的飞跃·进阶篇

(2/3)· 当浮动窗口逻辑崩坏时,如何退一步进两步而不重写全部代码

实战向进阶 第 2/3 篇
切换时间帧时 EA 重启、浮动窗口回弹到初始状态,多半是因为初始化没有区分模板是否变更。很多交易者只看到图表上堆满残留窗口,却没意识到问题出在 EA 启动逻辑本身。

子窗口里挂指标与模板的底层写法

想在 MT5 主图之外开一个子窗口承载自定义指标,核心是先确认窗口是否存在,不存在就追加。下面这段逻辑用 ChartWindowFind 查窗口 ID,返回 -1 时直接 ChartIndicatorAdd 到新窗口,并把 iCustom 加载的 "::" + def_Resource 指标塞进去。 类里用 #define def_MaxTemplates 8 锁死最多 8 个模板槽位,结构体 st 每个元素存 szObjName、szSymbol、szTemplate、szVLine 以及 width、scale、timeframe、handle。这意味着一套 EA 最多同时管 8 个品种的缩略模板,超了就得改宏。 OnInit 里 IndicatorSetString(INDICATOR_SHORTNAME,"SubWinSupport") 给指标短名,EventSetTimer(1) 开 1 秒定时器,OnTick 中 Chart.DispatchMessage 把仓位检查结果推到图表消息。开 MT5 把这段抄进 EA,编译后拖到 EURUSD 看子窗口是否多出 "SubWinSupport" 即可验证。外汇与贵金属杠杆高,这类自动挂窗口逻辑若句柄失效可能漏画,需实盘前在模拟盘跑通。

MQL5 / C++
EventSetTimer(class="num">1);

class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
   Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, NanoEA.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]);
}
class="type">int OnInit()
{
   IndicatorSetString(INDICATOR_SHORTNAME, "SubWinSupport");
   
   class="kw">return INIT_SUCCEEDED;
}
class="type">void Init(class="type">void)
{
   class="type">int i0;
   if ((i0 = ChartWindowFind(Terminal.Get_ID(), def_Indicador)) == -class="num">1)
      ChartIndicatorAdd(Terminal.Get_ID(), i0 = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL), iCustom(NULL, class="num">0, "::" + def_Resource));
   m_IdSubWinEA = i0;
}
class="kw">inline class="type">int GetIdSubWinEA(class="type">void)
{
   if (m_IdSubWinEA < class="num">0) Init();
   class="kw">return m_IdSubWinEA;
}
class C_TemplateChart : class="kw">public C_SubWindow
{
class="macro">#define def_MaxTemplates          class="num">8
class="macro">#define def_NameTemplateRAD    "IDE"
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
   enum eParameter {TEMPLATE = class="num">0, PERIOD, SCALE, WIDTH, HEIGHT};
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st
      {
         class="type">class="kw">string         szObjName,
                        szSymbol,
                        szTemplate,
                        szVLine;
         class="type">int            width,
                        scale;
         ENUM_TIMEFRAMES timeframe;
         class="type">long           handle;
      }m_Info[def_MaxTemplates];
      class="type">int    m_Counter,

「命令字符串的逐字符拆解逻辑」

这段结构定义了一个叫 st00 的内部结构体,用来承接模板指令的解析结果:counter 记录当前抓到了几个参数,Param 数组长度由 HEIGHT+1 决定,存的是字符串形式的参数片段。 GetCommand 是真正的解析器。它先把 TEMPLATE 到 HEIGHT 区间的 Param 清空、counter 置 0,然后从传入的 iArg 位置开始扫 szArg 这个字符串,遇到 ')' 或 ';' 就截一段子串存进 Param,并直接返回 iArg+1 交给上层继续;遇到空格做游标修正,遇到 '(' 或 ',' 则把累积的 c2 长度截成参数并复位游标。 注意一个细节:当 m_Params.counter 等于 SCALE 时,c2 会被压到最多 1,说明嵌套层级的参数长度做了硬性截断。若字符串非正常结束,函数返回 -1,调用方要自己判错。 在 MT5 里把 HEIGHT 设成 8、SCALE 设成 3 跑一遍,能看到单条指令最多落 9 个参数槽,超长嵌套参数被砍到 1 字符——这机制决定了模板宏不能无限叠参数。外汇与贵金属品种波动剧烈,这类解析器若用在实盘 EA 指令通道,需先在策略测试器里验证边界行为。

MQL5 / C++
m_CPre,
            m_Aggregate;
            class="kw">struct st00
            {
               class="type">int     counter;
               class="type">class="kw">string  Param[HEIGHT + class="num">1];
            }m_Params;
class="type">int GetCommand(class="type">int iArg, const class="type">class="kw">string szArg)
{
               for (class="type">int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
               m_Params.counter = class="num">0;
               for (class="type">int c1 = iArg, c2 = class="num">0; szArg[iArg] != 0x00; iArg++) class="kw">switch (szArg[iArg])
               {
                  case &class="macro">#x27;)&class="macro">#x27;:
                  case &class="macro">#x27;;&class="macro">#x27;:
                     m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                     for (; (szArg[iArg] != 0x00) && (szArg[iArg] != &class="macro">#x27;;&class="macro">#x27;); iArg++);
                     class="kw">return iArg + class="num">1;
                  case &class="macro">#x27; &class="macro">#x27;:
                     c2 += (c1 == iArg ? class="num">0 : class="num">1);
                     c1 = (c1 == iArg ? iArg + class="num">1 : c1);
                     break;
                  case &class="macro">#x27;(&class="macro">#x27;:
                  case &class="macro">#x27;,&class="macro">#x27;:
                     c2 = (m_Params.counter == SCALE ? (c2 >= class="num">1 ? class="num">1 : c2) : c2);
                     m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                     c2 = class="num">0;
                     c1 = iArg + class="num">1;
                     break;
                  class="kw">default:
                     c2++;
                     break;
               }
               class="kw">return -class="num">1;
}
class C_TemplateChart : class="kw">public C_SubWindow
{

◍ 解析模板参数的取词逻辑

在 MT5 自定义指标里做模板化配置,先得把命令字符串拆成结构化参数。下面这段私有解析函数用枚举 eParameter 锁定了 6 个槽位:TEMPLATE、COLOR_VLINE、PERIOD、SCALE、WIDTH、HEIGHT,上限由 def_MaxTemplates = 8 和 def_NameTemplateRAD = "IDE" 共同约束。 struct st00 只塞了两个字段:counter 记当前拆到第几个参数,Param 字符串数组长度按 HEIGHT+1 开。也就是说若 HEIGHT 取值 5,数组就留 6 格,足够装下全部枚举项。 GetCommand 的拆词靠扫描字符:遇 '(' 或 ',' 切一段写入 Param,遇 ')' 或 ';' 收尾并返回。注意那句 if(m_Params.counter==HEIGHT) return StringLen(szArg)+1; —— 参数填满到 HEIGHT 就强行截断,避免越界写 Param。 SCALE 那段有个隐藏 clamp:c2 = (m_Params.counter==SCALE ? (c2>=1?1:c2) : c2); 意思是 SCALE 位只认 0 或 1 的长度,超了就压回 1。开 MT5 把 HEIGHT 改成 6 试一次,能直观看到第 7 个参数被拒收。

MQL5 / C++
class="macro">#define def_MaxTemplates        class="num">8
class="macro">#define def_NameTemplateRAD    "IDE"
class=class="str">"cmt">//+------------------------------------------------------------------+
                class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
                enum eParameter {TEMPLATE = class="num">0, COLOR_VLINE, PERIOD, SCALE, WIDTH, HEIGHT};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ... Internal code ....
                        class="kw">struct st00
                        {
                            class="type">int     counter;
                            class="type">class="kw">string  Param[HEIGHT + class="num">1];
                        }m_Params;
class="type">int GetCommand(class="type">int iArg, const class="type">class="kw">string szArg)
{
                for (class="type">int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
                m_Params.counter = class="num">0;
                for (class="type">int c1 = iArg, c2 = class="num">0; szArg[iArg] != 0x00; iArg++) class="kw">switch (szArg[iArg])
                {
                        case &class="macro">#x27;)&class="macro">#x27;:
                        case &class="macro">#x27;;&class="macro">#x27;:
                                m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                                for (; (szArg[iArg] != 0x00) && (szArg[iArg] != &class="macro">#x27;;&class="macro">#x27;); iArg++);
                                class="kw">return iArg + class="num">1;
                        case &class="macro">#x27; &class="macro">#x27;:
                                c2 += (c1 == iArg ? class="num">0 : class="num">1);
                                c1 = (c1 == iArg ? iArg + class="num">1 : c1);
                                break;
                        case &class="macro">#x27;(&class="macro">#x27;:
                        case &class="macro">#x27;,&class="macro">#x27;:
                                if (m_Params.counter == HEIGHT) class="kw">return StringLen(szArg) + class="num">1;
                                c2 = (m_Params.counter == SCALE ? (c2 >= class="num">1 ? class="num">1 : c2) : c2);
                                m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                                c2 = class="num">0;
                                c1 = iArg + class="num">1;

周期字符串到枚举的硬映射

在模板加载函数里,周期参数不是用 StringToTimeframe 之类的通用转换,而是一长串 if-else 把 "1M""5M""1H" 这类字符串逐个拍成 PERIOD_M1、PERIOD_H1 等枚举值。这种写法没有默认分支兜底,遇到未列出的写法(比如 "45M")timeframe 会停在初始的 PERIOD_CURRENT。 当模板数量达到 def_MaxTemplates 上限,或传入的 TEMPLATE 参数为空串时函数直接 return,不往下走 SymbolSelect。这意味着实盘里若你复制了超过上限的模板配置,新增的会被静默丢弃,不会报错也不会写日志。 bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true) 这行尝试把模板名当品种名加入市场观察表,返回 bool 却没做判错——若模板名实际不是合法交易品种(外汇/贵金属高风险,品种名拼写错很常见),后续依赖该句柄的调用可能全部落空。

MQL5 / C++
class="type">void AddTemplate(class="type">void)
{
   ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;
   class="type">class="kw">string sz0 = m_Params.Param[PERIOD];
   class="type">int w, h, i;
   class="type">bool bIsSymbol;
   if (sz0 == "1M") timeframe = PERIOD_M1; else
   if (sz0 == "2M") timeframe = PERIOD_M2; else
   if (sz0 == "3M") timeframe = PERIOD_M3; else
   if (sz0 == "4M") timeframe = PERIOD_M4; else
   if (sz0 == "5M") timeframe = PERIOD_M5; else
   if (sz0 == "6M") timeframe = PERIOD_M6; else
   if (sz0 == "10M") timeframe = PERIOD_M10; else
   if (sz0 == "12M") timeframe = PERIOD_M12; else
   if (sz0 == "15M") timeframe = PERIOD_M15; else
   if (sz0 == "20M") timeframe = PERIOD_M20; else
   if (sz0 == "30M") timeframe = PERIOD_M30; else
   if (sz0 == "1H") timeframe = PERIOD_H1; else
   if (sz0 == "2H") timeframe = PERIOD_H2; else
   if (sz0 == "3H") timeframe = PERIOD_H3; else
   if (sz0 == "4H") timeframe = PERIOD_H4; else
   if (sz0 == "6H") timeframe = PERIOD_H6; else
   if (sz0 == "8H") timeframe = PERIOD_H8; else
   if (sz0 == "12H") timeframe = PERIOD_H12; else
   if (sz0 == "1D") timeframe = PERIOD_D1; else
   if (sz0 == "1S") timeframe = PERIOD_W1; else
   if (sz0 == "1MES") timeframe = PERIOD_MN1;
   if ((m_Counter >= def_MaxTemplates) || (m_Params.Param[TEMPLATE] == "")) class="kw">return;
   bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true);

「模板加载与子窗口尺寸分配的细节」

这段逻辑处理的是多图表模板的加载分支:当参数里指定了宽度 w 和高度 h 且都大于 0 时,直接按给定尺寸 AddIndicator 挂指标;否则走 SetBase + ChartApplyTemplate 的常规套模板路径,加载失败且是符号级模板时回退到 Default.tpl。 缩放控制里有个硬约束值得注意:i 即图表缩放级别,若大于 5 或小于 0 会被强制重置为 -1,意味着超出 MT5 允许的 0~5 档缩放范围时系统不认,只会用默认比例。 Resize 函数按子窗口实际像素高度 y 和终端宽度算每个子图所占的 x1 像素:用 (Terminal.GetWidth() - m_Aggregate) 除以有效图表数(m_Counter 减已关闭数 m_CPre),若除数为 0 则兜底为 1,避免运行时除零崩溃。开 MT5 把 m_Params.Param[SCALE] 设成 6 验证一下,模板加载后缩放应为默认档而非第 6 档。 外汇与贵金属图表多窗口布局受报价跳空影响,子窗口重绘可能出现瞬移,属高风险环境下的正常表现,参数调试时建议先用模拟账户跑。

MQL5 / C++
w = (m_Params.Param[WIDTH] != "" ? (class="type">int)StringToInteger(m_Params.Param[WIDTH]) : class="num">0);
h = (m_Params.Param[HEIGHT] != "" ? (class="type">int)StringToInteger(m_Params.Param[HEIGHT]) : class="num">0);
i = (m_Params.Param[SCALE] != "" ? (class="type">int)StringToInteger(m_Params.Param[SCALE]) : -class="num">1);
i = (i > class="num">5 || i < class="num">0 ? -class="num">1 : i);
if ((w > class="num">0) && (h > class="num">0)) AddIndicator(m_Params.Param[TEMPLATE], class="num">0, -class="num">1, w, h, timeframe, i); else
{
   SetBase(m_Params.Param[TEMPLATE], (bIsSymbol ? m_Params.Param[TEMPLATE] : _Symbol), timeframe, i, w);
   if (!ChartApplyTemplate(m_Info[m_Counter - class="num">1].handle, m_Params.Param[TEMPLATE] + ".tpl")) if (bIsSymbol) ChartApplyTemplate(m_Info[m_Counter - class="num">1].handle, "Default.tpl");
   if (m_Params.Param[TEMPLATE] == def_NameTemplateRAD)
   {
      C_Chart_IDE::Create(GetIdSubWinEA());
      m_Info[m_Counter - class="num">1].szVLine = "";
   }else
   {
      m_Info[m_Counter - class="num">1].szVLine = (class="type">class="kw">string)ObjectsTotal(Terminal.Get_ID(), -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand();
      ObjectCreate(m_Info[m_Counter - class="num">1].handle, m_Info[m_Counter - class="num">1].szVLine, OBJ_VLINE, class="num">0, class="num">0, class="num">0);
      ObjectSetInteger(m_Info[m_Counter - class="num">1].handle, m_Info[m_Counter - class="num">1].szVLine, OBJPROP_COLOR, clrBlack);
   }
   ChartRedraw(m_Info[m_Counter - class="num">1].handle);
}
class="type">void Resize(class="type">void)
{
class="macro">#define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, A, B)
   class="type">int x0, x1, y;
   if (!ExistSubWin()) class="kw">return;
   x0 = class="num">0;
   y = (class="type">int)(ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA()));
   x1 = (class="type">int)((Terminal.GetWidth() - m_Aggregate) / (m_Counter > class="num">0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : class="num">1));
把重绘残留的诊断交给小布
小布盯盘已内置图表对象残留与 EA 重启行为的 AIGC 监测,打开对应品种页即可看到哪些浮动窗口在反复重建,你只管判断策略是否要跟着改结构。

常见问题

EA 在图表加载时会重建对象,若初始化代码不判断模板是否变更,窗口会被销毁并按原参数重建,导致用户调整丢失。
结构重构若保留原类接口,交易逻辑倾向不受影响;但内部消息传递变量删除后需同步修改调用处,否则编译或运行会出错。
采用新增窗口模板类并最小化改动初始化流程的思路,先退一步抽象再扩功能,比从头重写更安全且易回溯。
可以,小布盯盘的 AIGC 会标记对象残留和重启频率,辅助你确认重构是否引入了图表层面的副作用。
本篇先完成 EA 内部代码与窗口类概念跃迁,IDE 适配属于外部工具链调整,留到收尾篇能降低单篇认知负荷。