如何基于HTML和CSV报表可视化多币种交易历史·进阶篇
📊

如何基于HTML和CSV报表可视化多币种交易历史·进阶篇

(2/3)·测试器只给单一品种成交图,跨币种复盘靠肉眼比对太低效,本篇教你并行挂多实例

实战向进阶 第 2/3 篇
策略测试跑完多币种,多数人只盯着设置里那个品种的历史图,其余品种的成交全靠翻报表肉眼对。等过了几天要复看,CSV和HTML散落各处,谁下过单、挂哪儿根本拼不出全景。

◍ 面板加载与信号落地的接口骨架

做信号回测面板,第一步是把外部数据文件塞进 MT5 并筛出涉及哪些品种。attach() 负责调 load(file) 拿到数据,再遍历 data.getSize() 行,用 getSymbolColumn() 定位品种列,StringTrimLeft 清掉左空格后塞进 symbols 集合,非空才调 controlPanel(symbols) 画按钮。 apply() 是真正把信号画到图表上的入口。它先 ObjectsDeleteAll(0,"SCR",ChartWindowFind(),OBJ_TREND) 清掉旧趋势线,再根据传入 symbol 或已存 symbol 找真实品种 findrealsymbol(),SymbolSelect(real,true) 拉进市场报价窗。遍历时只处理 s==symbol 的行,selected 计数后调 makeTrade(row) 逐条下单或标注。 跑完会在日志打出 data.getSize() 总记录数与 selected 命中数,并把按钮染成 clrGreen、把品种名和记录数写进指标短名。外汇与贵金属波动剧烈,这类脚本只做历史信号可视化,实盘触发前务必在策略测试器核对点差与滑点。 onChartEvent 里只截了 CHARTEVENT_OBJECT_CLICK,说明按钮交互依赖图表对象点击消息,没挂定时器或后台线程——想加自动刷新的话得自己补个 EventSetTimer。

MQL5 / C++
class="type">void createButton(class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int dx, class="kw">const class="type">int dy, class="kw">const class="type">class="kw">string text, class="kw">const class="type">bool selected)
class="type">void controlPanel(class="kw">const IndexMap &symbols)
class="kw">virtual IndexMap *load(class="kw">const class="type">class="kw">string file) = class="num">0;
class="kw">virtual class="type">int getColumnCount() = class="num">0;
class="kw">virtual class="type">int getSymbolColumn() = class="num">0;
class="kw">virtual class="type">class="kw">datetime getStart() = class="num">0;
class="kw">virtual class="type">bool applyInit() { class="kw">return true; }
class="kw">virtual class="type">void makeTrade(IndexMap *row) = class="num">0;
class="kw">virtual class="type">int render() = class="num">0;
class="type">bool attach(class="kw">const class="type">class="kw">string file)
class="type">bool apply(class="kw">const class="type">class="kw">string _s = NULL)
class="type">bool isEmpty() class="kw">const
class="type">class="kw">string findrealsymbol()
class="type">class="kw">string _symbol() class="kw">const
class="type">class="kw">string _realsymbol() class="kw">const
class="type">void onChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long& lparam, class="kw">const class="type">class="kw">double& dparam, class="kw">const class="type">class="kw">string& sparam)
class="type">bool attach(class="kw">const class="type">class="kw">string file)
{
data = load(file);
IndexMap symbols;
for(class="type">int i = class="num">0; i < data.getSize(); ++i)
{
IndexMap *row = data[i];
class=class="str">"cmt">// 收集唯一的交易品种
class="type">class="kw">string s = row[getSymbolColumn()].get<class="type">class="kw">string>();
StringTrimLeft(s);
if(StringLen(s) > class="num">0) symbols.set(s);
}
if(symbols.getSize() > class="num">0)
{
controlPanel(symbols);
}
class="kw">return true;
}
class="type">bool apply(class="kw">const class="type">class="kw">string _s = NULL)
{
ObjectsDeleteAll(class="num">0, "SCR", ChartWindowFind(), OBJ_TREND);
if(_s != NULL && _s != "") symbol = _s;
if(symbol == NULL)
{
Print("没有选择交易品种");
class="kw">return class="kw">false;
}
class="type">class="kw">string real = findrealsymbol();
if(real == NULL)
{
Print("没有找到合适的交易品种");
class="kw">return class="kw">false;
}
SymbolSelect(real, true);
if(!applyInit()) class="kw">return class="kw">false;
class="type">int selected = class="num">0;
for(class="type">int i = class="num">0; i < data.getSize(); ++i)
{
IndexMap *row = data[i];
class="type">class="kw">string s = row[getSymbolColumn()].get<class="type">class="kw">string>();
StringTrimLeft(s);
if(s == symbol)
{
selected++;
makeTrade(row);
}
}
pressed = prefix + "#" + symbol;
ObjectSetInteger(class="num">0, pressed, OBJPROP_BGCOLOR, clrGreen);
class="type">int trends = render();
Print(data.getSize(), " records in total");
Print(selected, " trades for ", symbol);
class="type">class="kw">string title = CHART_REPORTER_TITLE + " (" + symbol + ", " + (class="type">class="kw">string)selected + " records, " + (class="type">class="kw">string)trends + " trades)";
IndicatorSetString(INDICATOR_SHORTNAME, title);
ChartRedraw();
class="kw">return true;
}
class="type">void onChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long& lparam, class="kw">const class="type">class="kw">double& dparam, class="kw">const class="type">class="kw">string& sparam)
{
if(id == CHARTEVENT_OBJECT_CLICK)
{

从成交报告里拆出多空配对逻辑

MT5 的成交历史导出后通常是 HTML 报表,想做自定义复盘就得先把每一行成交解析成结构对象。下面这段把报表行映射成了 Deal 类,type 用 +1/-1 标记 buy/sell,direction 用 +1/-1/0 标记 in/out/in-out,字段直接公有化是为了让外部 Processor 能零成本读取,绕开 MQL5 对私有成员跨类访问的限制。 Deal 的构造函数里所有数值都靠 StringToDouble / StringToInteger / StringToTime 从字符串列转换,其中 profit 字段先 StringReplace 掉空格再转双精度,避免「1 234.50」这种带千分位空格的字符串直接转换得到 1.0。TimeShift 是外部定义的时区偏移常量,加载时直接叠加进 time。 配对渲染阶段用的是队列法:makeTrade 把所有成交塞进 array,render 里遍历 array,遇到 isOut() 的平仓单就去 queue 里找 isIn() 且 isOpposite 且 volume 完全相等的开仓单。这种严格等量配对在真实账户里可能漏掉部分平仓(比如部分平仓只平 0.3 手),概率上更适用于闭环测试报表。 外汇与贵金属杠杆交易高风险,下面代码仅供在 MT5 策略测试器或本地脚本环境验证类结构用,不构成任何交易建议。

MQL5 / C++
class="type">int x = StringFind(sparam, "_#");
if(x != -class="num">1)
{
   class="type">class="kw">string s = StringSubstr(sparam, x + class="num">2);
   Print(s, " ", sparam, " ", pressed);
   ObjectSetInteger(class="num">0, sparam, OBJPROP_STATE, class="kw">false);
   ObjectSetInteger(class="num">0, pressed, OBJPROP_STATE, class="kw">false);
   ObjectSetInteger(class="num">0, pressed, OBJPROP_BGCOLOR, clrGray);
   pressed = "";
   if(apply(s)) class=class="str">"cmt">// 将设置按下状态和其它属性
   {
      ChartSetSymbolPeriod(class="num">0, _Symbol, _Period);
   }
}
}
}
class ReportProcessor: class="kw">public Processor
{
class="kw">private:
   class Deal   class=class="str">"cmt">//  如果 MQL5 可以支持类的私有访问设置,
   {            class=class="str">"cmt">// 交易将无法从外部世界获得,因此
   class="kw">public:    class=class="str">"cmt">// 把栏位变为共有,就可以从 Processor 中直接访问了。
      class="type">class="kw">datetime time;
      class="type">class="kw">double price;
      class="type">int type;      class=class="str">"cmt">// +class="num">1 - buy, -class="num">1 - sell
      class="type">int direction; class=class="str">"cmt">// +class="num">1 - in, -class="num">1 - out, class="num">0 - in/out
      class="type">class="kw">double volume;
      class="type">class="kw">double profit;
      class="type">long deal;
      class="type">long order;
      class="type">class="kw">string comment;
   class="kw">public:
      Deal(class="kw">const IndexMap *row) class=class="str">"cmt">// 这是 MT5 交易
      {
         time = StringToTime(row[COLUMN_TIME].get<class="type">class="kw">string>()) + TimeShift;
         price = StringToDouble(row[COLUMN_PRICE].get<class="type">class="kw">string>());
         class="type">class="kw">string t = row[COLUMN_TYPE].get<class="type">class="kw">string>();
         type = t == "buy" ? +class="num">1 : (t == "sell" ? -class="num">1 : class="num">0);
         t = row[COLUMN_DIRECTION].get<class="type">class="kw">string>();
         direction = class="num">0;
         if(StringFind(t, "in") > -class="num">1) ++direction;
         if(StringFind(t, "out") > -class="num">1) --direction;
         volume = StringToDouble(row[COLUMN_VOLUME].get<class="type">class="kw">string>());
         t = row[COLUMN_PROFIT].get<class="type">class="kw">string>();
         StringReplace(t, " ", "");
         profit = StringToDouble(t);
         deal = StringToInteger(row[COLUMN_DEAL].get<class="type">class="kw">string>());
         order = StringToInteger(row[COLUMN_ORDER].get<class="type">class="kw">string>());
         comment = row[COLUMN_COMMENT].get<class="type">class="kw">string>();
      }
      class="type">bool isIn() class="kw">const
      {
         class="kw">return direction >= class="num">0;
      }
      class="type">bool isOut() class="kw">const
      {
         class="kw">return direction <= class="num">0;
      }
      class="type">bool isOpposite(class="kw">const Deal *t) class="kw">const
      {
         class="kw">return type * t.type < class="num">0;
      }
      class="type">bool isActive() class="kw">const
      {
         class="kw">return volume > class="num">0;
      }
   };
   RubbArray<Deal *> array;
   RubbArray<Deal *> queue;
   class="kw">virtual IndexMap *load(class="kw">const class="type">class="kw">string file) class="kw">override
   {
      class="kw">return HTMLConverter::convertReport2Map(file, true);
   }
   class="kw">virtual class="type">int getColumnCount() class="kw">override
   {
      class="kw">return COLUMNS_COUNT;
   }
   class="kw">virtual class="type">int getSymbolColumn() class="kw">override
   {
      class="kw">return COLUMN_SYMBOL;
   }
   class="macro">#define COLUMNS_COUNT class="num">13
   class="macro">#define COLUMN_TIME class="num">0
   class="macro">#define COLUMN_DEAL class="num">1
   class="macro">#define COLUMN_SYMBOL class="num">2
   ...
   class="kw">virtual class="type">bool applyInit() class="kw">override
   {
      ((BaseArray<Deal *>&)queue).clear();
      array.clear();
      class="kw">return true;
   }
   class="kw">virtual class="type">void makeTrade(IndexMap *row) class="kw">override
   {
      array << new Deal(row);
   }
   class="kw">virtual class="type">int render() class="kw">override
   {
      class="type">int count = class="num">0;
      for(class="type">int i = class="num">0; i < array.size(); ++i)
      {
         Deal *current = array[i];
         if(!current.isActive()) class="kw">continue;
         if(current.isOut())
         {
            class=class="str">"cmt">// 首先尝试完全匹配
            for(class="type">int j = class="num">0; j < queue.size(); ++j)
            {
               if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].volume == current.volume)
               {

「成对成交与部分平仓的队列清理」

这段逻辑处理的是成交队列里「反向单互吃」的两种情况:整笔平掉和仅平部分。第一次循环里若当前持仓 volume 直接归零,就从队列里用 queue >> j 删掉那条记录,并 ++count 记一次配对成功。 第二次尝试专门处理部分平仓:当 current.volume 大于等于队列项 volume 时,减掉对方 volume 并把对方置 0;反之则减自己,对方留余量,随后 break 跳出内层循环。两种分支都调用 createTrend 写趋势线描述,描述串里带了 deal/order 编号与盈亏正负号。 循环结束后有一段倒序清扫:从 queue.size()-1 到 0 检查 isActive(),非活动项直接 queue >> j 移除,避免残留空单污染后续匹配。若 current 仍 active 且是入场单,就重新 queue << current 压回队列等下次事件。 OnInit 里按扩展名分流处理器:.htm 建 ReportProcessor,.csv 建 HistoryProcessor;Symbol 为空则退回 _Symbol,否则 SymbolSelect 激活。OnChartEvent 只在 CHARTEVENT_CHART_CHANGE 时跑本地刷新,其余事件丢给 processor。 最后用 iBars(Symbol, _Period) 比对 lastAvailable,若相等且未初始化则 Print 出「Updated 品种 N bars」并置 initialized,同时若 ReportFile 非空进入文件重载分支——外汇与贵金属品种点值跳变频繁,这类计数错位可能引发Indicator重绘异常,建议开 MT5 用真实回放文件断点验证。

MQL5 / C++
class="type">class="kw">string description;
StringConcatenate(description, (class="type">class="kw">float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < class="num">0 ? "-" : ""), current.profit, " ", current.comment);
createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
current.volume = class="num">0;
queue >> j; class=class="str">"cmt">// 从队列中删除
++count;
class="kw">break;
}
}
if(!current.isActive()) class="kw">continue;
class=class="str">"cmt">// second try to perform partial close
for(class="type">int j = class="num">0; j < queue.size(); ++j)
{
if(queue[j].isIn() && queue[j].isOpposite(current))
{
class="type">class="kw">string description;
if(current.volume >= queue[j].volume)
{
StringConcatenate(description, (class="type">class="kw">float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < class="num">0 ? "-" : ""), current.profit, " ", current.comment);
createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
current.volume -= queue[j].volume;
queue[j].volume = class="num">0;
++count;
}
else
{
StringConcatenate(description, (class="type">class="kw">float)current.volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < class="num">0 ? "-" : ""), current.profit, " ", current.comment);
createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
queue[j].volume -= current.volume;
current.volume = class="num">0;
++count;
class="kw">break;
}
}
}
class=class="str">"cmt">// 从队列中清除非活动的部分
for(class="type">int j = queue.size() - class="num">1; j >= class="num">0; --j)
{
if(!queue[j].isActive())
{
queue >> j;
}
}
}
if(current.isActive()) class=class="str">"cmt">// is _still_ active
{
if(current.isIn())
{
queue << current;
}
}
}
if(!isQueueEmpty())
{
Print("Warning: not all deals are processed(probably, open positions left).");
}
class="kw">return count;
}
Processor *processor = NULL;
class="type">int OnInit()
{
if(StringFind(ReportFile, ".htm") > class="num">0)
{
processor = new ReportProcessor();
}
else if(StringFind(ReportFile, ".csv") > class="num">0)
{
processor = new HistoryProcessor();
}
class="type">class="kw">string Symbol = SubSymbol;
if(Symbol == "") Symbol = _Symbol;
else SymbolSelect(Symbol, true);
processor.apply(Symbol);
...
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
if(processor != NULL) class="kw">delete processor;
}
class="type">void OnChartEvent(class="kw">const class="type">int id,
class="kw">const class="type">long& lparam,
class="kw">const class="type">class="kw">double& dparam,
class="kw">const class="type">class="kw">string& sparam)
{
if(id == CHARTEVENT_CHART_CHANGE)
{
... class=class="str">"cmt">// 相同的代码
}
else
{
processor.onChartEvent(id, lparam, dparam, sparam);
}
}
class="type">class="kw">string Symbol = processor._realsymbol();
if(Symbol == NULL) Symbol = _Symbol;
if(lastSymbol != Symbol)
{
_prev_calculated = class="num">0;
lastAvailable = class="num">0;
initialized = class="kw">false;
IndicatorSetInteger(INDICATOR_DIGITS, (class="type">int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
}
if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != class="num">0)
{
if(!initialized)
{
Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
initialized = true;
if(ReportFile != "") class=class="str">"cmt">//
{                    class=class="str">"cmt">//

◍ 用 OnTimer 给报表只加载一次

这段 MQL5 片段解决了一个常见坑:指标或 EA 在每次 tick 重算时反复读外部报表文件,既拖慢 MT5 又容易把内存搞乱。 它借助 EventSetTimer(1) 启动 1 秒定时器,在 OnTimer 里先用 EventKillTimer 停掉定时,再用 processor.isEmpty() 判断——空才 attach(ReportFile) 并 apply,否则跳过,保证文件只载入一次。 若 attach 返回失败,Print 会抛出 "File loading failed: " 加文件名,开 MT5 跑这段代码时,能在专家日志直接看到路径是不是写错。 外汇与贵金属行情高波动、滑点频发,这类一次性加载逻辑能降低回测与实盘切换时的资源抖动,但任何自动化加载都不消除品种本身的爆仓风险。

MQL5 / C++
EventSetTimer(class="num">1);  class=class="str">"cmt">//
}                    class=class="str">"cmt">//
}
class="kw">return rates_total;
}
class="type">void OnTimer()
{
EventKillTimer();
if(processor.isEmpty()) class=class="str">"cmt">// 只载入一次文件
{
if(processor.attach(ReportFile))
{
processor.apply(class=class="str">"cmt">/*keep already selected symbol*/);
}
else
{
Print("File loading failed: ", ReportFile);
}
}
}

用脚本把多品种报表铺进子窗口

SubChartsBuilder 脚本和 SubChartReporter 指标共用一套参数,区别在启动方式:它把参数塞进 MqlParam 数组,再调 IndicatorCreate 从资源里拉指标,因此不依赖用户本地是否装了那个 ex5。资源路径里带「:Indicators\」时,脚本就判定这是被整体程序拉起的实例,顺手把控制按钮隐藏掉,避免多品种生成时界面被按钮刷屏。 MQL 本身没法区分指标是用户手动挂的还是代码里 Create 出来的,塞进资源算是个绕开限制的办法。脚本跑空报告名会触发 cleanUpChart,把之前建的子窗口全清掉,适合反复验证报表后重置图表。 实测我下了带信号历史的 CSV 跑脚本,主图最小化后能看到多个 SubChartReporter 实例并列子窗,对象描述里含交易编号、交易量、利润和评论——记得在图表设置开「显示对象描述」。品种一多子窗就变窄,细看单笔吃力;真要抠每笔,就把指标改成往主图画的 MainChartReporter,别死磕子窗。 下面这段是脚本核心:createIndicator 拼 18 个参数调 IndicatorCreate,OnStart 解析 htm/csv 报告提取品种集合后逐个建实例,空报告名走清理分支。

MQL5 / C++
class="type">bool createIndicator(class="kw">const class="type">class="kw">string symbol)
{
class="type">MqlParam params[class="num">18] =
{
{TYPE_STRING, class="num">0, class="num">0.0, "::Indicators\\SubChartReporter.ex5"}, class=class="str">"cmt">// 资源内指标路径,决定按钮隐显
{TYPE_INT, class="num">0, class="num">0.0, NULL}, class=class="str">"cmt">// 图表设置
{TYPE_STRING, class="num">0, class="num">0.0, "XYZ"},
{TYPE_BOOL, class="num">1, class="num">0.0, NULL},
{TYPE_INT, class="num">0, class="num">0.0, NULL}, class=class="str">"cmt">// 通用设置
{TYPE_STRING, class="num">0, class="num">0.0, "HTMLCSV"},
{TYPE_STRING, class="num">0, class="num">0.0, "PREFIX"},
{TYPE_STRING, class="num">0, class="num">0.0, "SUFFIX"},
{TYPE_INT, class="num">0, class="num">0.0, NULL}, class=class="str">"cmt">// 时间偏移
{TYPE_INT, class="num">0, class="num">0.0, NULL}, class=class="str">"cmt">// html 设置
{TYPE_STRING, class="num">0, class="num">0.0, "ROW"},
{TYPE_STRING, class="num">0, class="num">0.0, "COLUMNS"},
{TYPE_STRING, class="num">0, class="num">0.0, "SUBST"},
{TYPE_BOOL, class="num">0, class="num">0.0, NULL},
{TYPE_BOOL, class="num">0, class="num">0.0, NULL},
{TYPE_BOOL, class="num">0, class="num">0.0, NULL},
{TYPE_INT, class="num">0, class="num">0.0, NULL}, class=class="str">"cmt">// csv 设置
{TYPE_STRING, class="num">0, class="num">0.0, ";"}
};
params[class="num">2].string_value = symbol; class=class="str">"cmt">// 注入当前品种
params[class="num">5].string_value = ReportFile; class=class="str">"cmt">// 报告文件
params[class="num">6].string_value = Prefix;
params[class="num">7].string_value = Suffix;
params[class="num">8].integer_value = TimeShift; class=class="str">"cmt">// 时间偏移量
params[class="num">10].string_value = RowSelector;
params[class="num">11].string_value = ColumnSettingsFile;
params[class="num">12].string_value = SubstitutionSettingsFile;
params[class="num">17].string_value = CSVDelimiter; class=class="str">"cmt">// CSV 分隔符
class="type">int handle = IndicatorCreate(_Symbol, _Period, IND_CUSTOM, class="num">18, params); class=class="str">"cmt">// 建指标句柄
if(handle == INVALID_HANDLE)
{
Print("无法创建 SubChartReporter 为 ", symbol, ": ", GetLastError());
class="kw">return class="kw">false;
}
else
{
if(!ChartIndicatorAdd(class="num">0, (class="type">int)ChartGetInteger(class="num">0, CHART_WINDOWS_TOTAL), handle)) class=class="str">"cmt">// 挂到新子窗
{
Print("无法附加 SubChartReporter 为", symbol, ": ", GetLastError());
class="kw">return class="kw">false;
}
}
class="kw">return true;
}
class="macro">#resource "\Indicators\SubChartReporter.ex5"
class="type">int OnStart()
{
IndexMap *data = NULL;
class="type">int columnsCount = class="num">0, symbolColumn = class="num">0;
if(ReportFile == "")
{
Print("cleanUpChart");
class="kw">return cleanUpChart(); class=class="str">"cmt">// 空报告名清场
}
else if(StringFind(ReportFile, ".htm") > class="num">0)
{
data = HTMLConverter::convertReport2Map(ReportFile, true);
columnsCount = COLUMNS_COUNT;
symbolColumn = COLUMN_SYMBOL;
}
else if(StringFind(ReportFile, ".csv") > class="num">0)
{
data = CSVConverter::ReadCSV(ReportFile);
if(data != NULL && data.getSize() > class="num">0)
{
IndexMap *row = data[class="num">0];
columnsCount = row.getSize();
symbolColumn = CSV_COLUMN_SYMBOL;
}
}
if(data != NULL)
{
IndexMap symbols;
for(class="type">int i = class="num">0; i < data.getSize(); ++i)
{
IndexMap *row = data[i];
if(CheckPointer(row) == POINTER_INVALID || row.getSize() != columnsCount) class="kw">break;
class="type">class="kw">string s = row[symbolColumn].get<class="type">class="kw">string>();
StringTrimLeft(s);
if(StringLen(s) > class="num">0) symbols.set(s); class=class="str">"cmt">// 收集去重品种
}
for(class="type">int i = class="num">0; i < symbols.getSize(); ++i)
{
createIndicator(symbols.getKey(i)); class=class="str">"cmt">// 逐品种建子窗
}
class="kw">delete data;
}
class="kw">return class="num">0;
}
class="type">bool cleanUpChart()
{
class="type">bool result = true;
class="type">int n = (class="type">int)ChartGetInteger(class="num">0, CHART_WINDOWS_TOTAL);
for(class="type">int i = n - class="num">1; i > class="num">0; --i)
{
把多实例挂图交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到子窗口报价与订单线同步状态,把重复劳动交给小布,你专注决策。

常见问题

测试器默认只渲染设置中选中的那个品种图表,其余品种成交需另做解析与绘制,本篇SubChart指标就是为补这个缺口。
小布盯盘内置的AIGC可识别报表结构并生成品种页视图,省去手动挂多个指标实例,但仍需您授权对应文件。
指标通过ENUM_CHART_MODE读取当前窗口模式,动态选用DRAW_CANDLES、DRAW_BARS或折线渲染,无需重编译。
内置MQL函数分别支持.history.csv与.positions.csv,字段映射略有不同,但提取订单趋势线的逻辑可复用。