从头开始开发智能交易系统(第 8 部分):概念上的飞跃·进阶篇
(2/3)· 当浮动窗口逻辑崩坏时,如何退一步进两步而不重写全部代码
子窗口里挂指标与模板的底层写法
想在 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" 即可验证。外汇与贵金属杠杆高,这类自动挂窗口逻辑若句柄失效可能漏画,需实盘前在模拟盘跑通。
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 指令通道,需先在策略测试器里验证边界行为。
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 个参数被拒收。
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 却没做判错——若模板名实际不是合法交易品种(外汇/贵金属高风险,品种名拼写错很常见),后续依赖该句柄的调用可能全部落空。
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 档。 外汇与贵金属图表多窗口布局受报价跳空影响,子窗口重绘可能出现瞬移,属高风险环境下的正常表现,参数调试时建议先用模拟账户跑。
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));