如何分析图表中所选择信号的交易·进阶篇
📊

如何分析图表中所选择信号的交易·进阶篇

(2/3)·终端自带图表只能单品种翻页,跨品种重合仓位与回撤峰值根本看不穿

实战向 第 2/3 篇

很多人以为在终端里点开信号历史、在各个品种图表间来回翻页就足够评估一个信号了。可一旦信号同时跑多个品种,重合的入场和平仓在时间轴上被拆进不同窗口,你既看不到资金负荷峰值,也难判断是不是在对冲。本文接着上篇,继续把这套跨品种统计与图形化工作落到可运行的工程里。

◍ 遍历图表抓取箭头信号

做信号聚合的第一步,是把终端里所有打开的图表都扫一遍,把手动或 EA 画上去的买卖箭头对象捞出来。下面这段逻辑从 ChartFirst 拿第一个图表句柄,只要句柄大于 0 就循环,用 ChartNext 顺着链往下走,直到没有更多图表。 每个图表先数 OBJ_ARROW_BUY 和 OBJ_ARROW_SELL 的数量,两个加起来为 0 就直接跳过。这里有个细节:用 fmax(total_buy,total_sell) 决定循环上限,意味着买卖箭头数量不等时,也按多的那一侧跑满次数,另一侧靠 i<total_x 判断补抓。 图表对应的交易品种会写进 ar_Symbols 数组,找不到就 ArrayResize 扩 10 个缓冲位再塞进去。品种去重靠线性查找,品种少时够用,开几十个图表时倾向改成哈希思路。外汇和贵金属波动大,这类箭头只是某时刻的主观或规则标记,不代表后续一定走出预期方向。 买箭头的名字被取出来后,会 Trim 掉首尾空格、把 # 替换掉,再按空格 Split。代码假定名字里第 1、3、6 段分别是订单号、开价、止损这类字段,直接 StringToInteger / StringToDouble 灌进 COrder 对象。你的箭头注释格式若对不上这个顺序,跑出来就是错单或空指针,开 MT5 前先确认画线工具的命名规则。

MQL5 / C++
class="type">long chart=ChartFirst();
while(chart>class="num">0)
  {
  class="type">int total_buy=ObjectsTotal(chart,class="num">0,OBJ_ARROW_BUY);
  class="type">int total_sell=ObjectsTotal(chart,class="num">0,OBJ_ARROW_SELL);
  if((total_buy+total_sell)<=class="num">0)
    {
    chart=ChartNext(chart);
    class="kw">continue;
    }
  class="type">int symb=ArraySize(ar_Symbols);
  class="type">class="kw">string symbol=ChartSymbol(chart);
  class="type">bool found=false;
  for(class="type">int i=class="num">0;(i<symb && !found);i++)
    if(ar_Symbols[i]==symbol)
      {
      found=true;
      symb=i;
      break;
      }
  if(!found)
    {
    if(ArrayResize(ar_Symbols,symb+class="num">1,class="num">10)<=class="num">0)
      class="kw">return false;
    ar_Symbols[symb]=symbol;
    }
  class="type">int total=fmax(total_buy,total_sell);
  for(class="type">int i=class="num">0;i<total;i++)
    {
    if(i<total_buy)
      {
      class="type">class="kw">string name=ObjectName(chart,i,class="num">0,OBJ_ARROW_BUY);
      class="type">class="kw">datetime time=(class="type">class="kw">datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
      StringTrimLeft(name);
      StringTrimRight(name);
      StringReplace(name,"#","");
      class="type">class="kw">string split[];
      StringSplit(name,&class="macro">#x27; &class="macro">#x27;,split);
      Temp=new COrder;
      if(CheckPointer(Temp)!=POINTER_INVALID)
        {
        if(Temp.Create(ar_Symbols[symb],StringToInteger(split[class="num">1]),StringToDouble(split[class="num">3]),StringToDouble(split[class="num">6]),time,POSITION_TYPE_BUY))
          Add(Temp);
        }
      }
class=class="str">"cmt">//---
    if(i<total_sell)
      {
      class="type">class="kw">string name=ObjectName(chart,i,class="num">0,OBJ_ARROW_SELL);

「从图表对象反推卖单与净仓逻辑」

这段 MQL5 片段干了两件事:把图表上带 '#' 标记的文本对象解析成一笔卖单塞进集合,再按品种归并出净流入/出方向。外汇与贵金属市场杠杆高、滑点随机,这类从图形反推仓位的做法只能当辅助,不能单独作为下单依据。 对象名先被截掉首尾空格、去掉 '#',再用空格切分。split[1] 是 ticket 整数,split[3] 是开仓价,split[6] 是成交量,time 取自对象的时间属性。若同 ticket 已存在则跳过,避免重复添加。 SetDealsEntry 里先按 0 列排序,再双层循环:同品种下若当前 volume 为 0 或方向一致,标记为 DEAL_ENTRY_IN 并累加(保留 2 位小数);若方向相反且已有量大于等于当前量,则标记为 DEAL_ENTRY_OUT 并扣减。这样能粗略看出某品种净仓是建还是平。 开 MT5 把下面代码丢进 EA 的调试分支,打印 split 数组和 volume 变化,就能验证你图上标的 '#' 文本是否被正确识别成卖单。

MQL5 / C++
class="type">class="kw">datetime time=(class="type">class="kw">datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
StringTrimLeft(name);
StringTrimRight(name);
StringReplace(name,"#","");
class="type">class="kw">string split[];
StringSplit(name,&class="macro">#x27; &class="macro">#x27;,split);
Temp=new COrder;
if(CheckPointer(Temp)!=POINTER_INVALID)
  {
   if(Temp.Create(ar_Symbols[symb],StringToInteger(split[class="num">1]),StringToDouble(split[class="num">3]),StringToDouble(split[class="num">6]),time,POSITION_TYPE_SELL))
     Add(Temp);
  }
 }
   }
 chart=ChartNext(chart);
  }
 SetDealsEntry();
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool COrdersCollection::Add(COrder *element)
  {
  for(class="type">int i=class="num">0;i<m_data_total;i++)
   {
   Temp=m_data[i];
   if(Temp.Ticket()==element.Ticket())
     class="kw">return true;
   }
class=class="str">"cmt">//---
  class="kw">return CArrayObj::Add(element);
  }
COrdersCollection::SetDealsEntry(class="type">void)
  {
  Sort(class="num">0);
class=class="str">"cmt">//---
  class="type">int symbols=ArraySize(ar_Symbols);
  for(class="type">int symb=class="num">0;symb<symbols;symb++)
   {
   class="type">class="kw">double volume=class="num">0;
   class="type">ENUM_POSITION_TYPE type=-class="num">1;
   for(class="type">int ord=class="num">0;ord<m_data_total;ord++)
    {
    Temp=m_data[ord];
    if(Temp.Symbol()!=ar_Symbols[symb])
      class="kw">continue;
class=class="str">"cmt">//---
    if(volume==class="num">0 || type==Temp.Type())
     {
     Temp.DealEntry(DEAL_ENTRY_IN);
     volume=NormalizeDouble(volume+Temp.Volume(),class="num">2);
     type=Temp.Type();
     }
    else
     {
     if(volume>=Temp.Volume())
      {
       Temp.DealEntry(DEAL_ENTRY_OUT);
       volume=NormalizeDouble(volume-Temp.Volume(),class="num">2);
      }
     else

用 M5 切片还原账户资金曲线

要把一堆历史成交按时间轴摊开成资金曲线,核心是先锁定品种与时间窗,再借 M5 周期把每根 K 线当作切片边界。下面这段逻辑里,tick_value 取 SYMBOL_TRADE_TICK_VALUE 除以 SYMBOL_POINT,若为 0 直接返 false,说明点值配置异常时没必要继续算。 时间轴用 CopyRates 拉 M5 的 rates 数组,count 就是切片数量;若 count<=0 且未触发重载,会发一个 EventChartCustom 自定义事件 1222 并退出,避免空数据污染曲线。数组 balance / equity / time 都按 count 用 ArrayResize 撑开,再用 ArrayInitialize 把 balance 清 0。 成交遍历用 do-while 跳过无效指针,直到捞到第一条有效 Temp 记录;随后 for 循环按 i 走每个 M5 切片,内部 while 把落在该切片时间窗(rates[i].time + PeriodSeconds(PERIOD_M5))之前的同品种成交累加进 volume 与浮盈浮亏统计。long_trades / short_trades 也在这过程里分别计数,外汇与贵金属品种点值波动大,实盘回测前务必确认 tick_value 非零,否则统计会直接中断。

MQL5 / C++
  {
               Temp.DealEntry(DEAL_ENTRY_INOUT);
               volume=NormalizeDouble(volume-Temp.Volume(),class="num">2);
               type=Temp.Type();
               }
            }
         }
      }
class="type">bool COrdersCollection::GetTimeSeries(const class="type">class="kw">string symbol,const class="type">class="kw">datetime start_time,const class="type">class="kw">datetime end_time,const class="type">int direct,class="type">class="kw">double &balance[],class="type">class="kw">double &equity[], class="type">class="kw">double &time[], class="type">class="kw">double &profit, class="type">class="kw">double &loss,class="type">int &long_trades, class="type">int &short_trades)
  {
   profit=loss=class="num">0;
   long_trades=short_trades=class="num">0;
class=class="str">"cmt">//---
   if(symbol==NULL)
      class="kw">return false;
class=class="str">"cmt">//---
   class="type">class="kw">double tick_value=SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE)/SymbolInfoDouble(symbol,SYMBOL_POINT);
   if(tick_value==class="num">0)
      class="kw">return false;
   ENUM_TIMEFRAMES timeframe=PERIOD_M5;
class=class="str">"cmt">//---
   class="type">class="kw">double volume=class="num">0;
   class="type">class="kw">double price=class="num">0;
   class="type">ENUM_POSITION_TYPE type=-class="num">1;
   class="type">int order=-class="num">1;
class=class="str">"cmt">//---
   class="type">MqlRates rates[];
   class="type">int count=class="num">0;
   count=CopyRates(symbol,timeframe,start_time,end_time,rates);
   if(count<=class="num">0 && !ReloadHistory)
     {
       class=class="str">"cmt">//--- 发送通知
       ReloadHistory=EventChartCustom(CONTROLS_SELF_MESSAGE,class="num">1222,class="num">0,class="num">0.0,symbol);
       class="kw">return false;
     }
   if(ArrayResize(balance,count)<count || ArrayResize(equity,count)<count || ArrayResize(time,count)<count)
      class="kw">return false;
   ArrayInitialize(balance,class="num">0);
   do
     {
       order++;
       if(order<m_data_total)
         Temp=m_data[order];
       else
         Temp=NULL;
     }
   while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
class=class="str">"cmt">//---
   for(class="type">int i=class="num">0;i<count;i++)
     {
       while(order<m_data_total && Temp.Time()<(rates[i].time+PeriodSeconds(timeframe)))
         {
          if(Temp.Symbol()!=symbol)
            {
              do
                {
                 order++;
                 if(order<m_data_total)
                   Temp=m_data[order];

◍ 逐笔撮合里的浮盈浮亏落点

上面这段逻辑干的是持仓链表遍历后的清算动作:当 Temp 指针有效且同类型时,用成交量加权把 price 重算一遍,同时给 long_trades / short_trades 计数。加权公式 price=volume*price+Temp.Volume()*Temp.Price() 再除以合计 volume,意味着同方向加仓时均价是严格线性的,不会受报价小数位干扰。 若类型不匹配(即对向单),且 i>0、方向过滤通过,就进入撮合分支:temp=(Temp.Price()-price)*tick_value*(买?1:-1)*MathMin(volume,Temp.Volume()) 直接把这一撮手数的浮动盈亏算进 balance[i]。tick_value 没乘杠杆倍数,说明这里记录的是合约层面盈亏,要在 MT5 里跑通得先确认品种 tick_value 不为 0,否则外汇贵金属账户上 balance 数组全平。 profit 与 loss 分别累加非负和负向 temp,volume 减去 Temp.Volume() 后若转负,就取绝对值翻成对手向新仓位并重置 price 与 type。这个翻转点就是净敞口方向切换的边界,实盘里黄金 XAUUSD 在 5 位数报价下,一次 0.1 手反向吃单就可能让 volume 符号翻转两次,建议把 m_data_total 控制在 200 笔内观察数组写入延迟。

MQL5 / C++
else
   Temp=NULL;
      }
   while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
   class="kw">continue;
     }
class=class="str">"cmt">//---
   if(Temp!=NULL)
     {
      if(type==Temp.Type())
        {
         price=volume*price+Temp.Volume()*Temp.Price();
         volume+=Temp.Volume();
         price=price/volume;
         class="kw">switch(type)
           {
            case POSITION_TYPE_BUY:
               long_trades++;
               break;
            case POSITION_TYPE_SELL:
               short_trades++;
               break;
           }
        }
      else
        {
         if(i>class="num">0 && (direct<class="num">0 || direct==type))
           {
            class="type">class="kw">double temp=(Temp.Price()-price)*tick_value*(type==POSITION_TYPE_BUY ? class="num">1 : -class="num">1)*MathMin(volume,Temp.Volume());
            balance[i]+=temp;
            if(temp>=class="num">0)
               profit+=temp;
            else
               loss+=temp;
           }
         volume-=Temp.Volume();
         if(volume<class="num">0)
           {
            volume=MathAbs(volume);
            price=Temp.Price();
            type=Temp.Type();
            class="kw">switch(type)
              {
               case POSITION_TYPE_BUY:

「逐行拆开权益计算的那段循环」

下面这段是持仓遍历与权益累加的核心片段,直接贴出来逐行看逻辑。 long_trades++; 与 short_trades++; 分别在 switch 匹配到 BUY、SELL 持仓类型时计数加一,break 跳出当前 case 避免穿透。 do{ order++; if(order<m_data_total) Temp=m_data[order]; else Temp=NULL; } while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total); 这段用 do-while 向后找下一条有效数据指针,遇到空指针或越界就停。 if(i>0) balance[i]+=balance[i-1]; 把余额做前缀和,使每根 K 线的 balance 承载累计值。

if(volume>0 && (direct<0direct==type)) equity[i]=(rates[i].close-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,(Temp!=NULL ? Temp.Volume(): DBL_MAX)); 否则 equity[i]=0; 这里用收盘价减开仓价乘每跳价值,再乘方向系数与可平量,得到该 bar 浮动权益;最后 equity[i]+=balance[i]; 叠上余额,time[i]=(double)rates[i].time; 记下时间戳。

开 MT5 把这段塞进你自己的回测类里,把 tick_value 和 price 换成实际持仓成本,跑一根 EURUSD 的 M1 就能核对 equity 曲线是否和账户历史一致。外汇与贵金属杠杆高,核对结果仅反映历史逻辑,实盘可能偏离。

MQL5 / C++
                   long_trades++;
                   break;
                case POSITION_TYPE_SELL:
                   short_trades++;
                   break;
                }
             }
          }
       }
    do
       {
          order++;
          if(order<m_data_total)
             Temp=m_data[order];
          else
             Temp=NULL;
       }
    while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
    }
   if(i>class="num">0)
      {
       balance[i]+=balance[i-class="num">1];
      }
   if(volume>class="num">0 && (direct<class="num">0 || direct==type))
      equity[i]=(rates[i].close-price)*tick_value*(type==POSITION_TYPE_BUY ? class="num">1 : -class="num">1)*MathMin(volume,(Temp!=NULL ? Temp.Volume(): DBL_MAX));
   else
      equity[i]=class="num">0;
   equity[i]+=balance[i];
   time[i]=(class="type">class="kw">double)rates[i].time;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }

给统计面板套上图形外壳

图形界面由分析起止日期、显示选项复选框、文本统计块和曲线图组成。我们在继承自 CAppDialog 的 CStatisticsPanel 类里搭这套壳:日期用 CDatePicker,三组数据显示开关用 CCheckGroup(余额资金 / 多空持仓 / 品种列表),文字指标用 CLabel,绘图核心用 CGraphic,订单统计靠 COrdersCollection 实例取数。 Create 方法先调父类初始化,再把各控件按坐标摆好、给初值并加入控件集合。注意 CGraphic 不从 CWnd 派生,不能进集合,所以要重写面板最小化/展开逻辑,初始化完直接调图表刷新。 CreateGraphic 接收对象名和坐标,内部建 CGraphic 后按客户区偏移。随后对每个品种建 CCurve 余额/资金曲线,用空数组初始化并先隐藏。横坐标关掉自动缩放、绑定日期属性,标题尺寸设好再显图。 UpdateChart 负责数据刷新:取起止日,按复选状态调 GetTimeSeries 补时序,勾了的品种才显曲线。最后写分析区间、网格、刷新图,并把胜败笔数等写进 CLabel。事件端只需监听日期改动、复选变化及自定义“历史缺失”事件,触发即调 UpdateChart;最小化方法里补一句图表显隐即可。 外汇与贵金属品种的历史数据缺口很常见,跑这套面板时若某品种报自定义缺失事件,图表可能只覆盖部分符号,属正常容错而非 bug。

MQL5 / C++
class CStatisticsPanel : class="kw">public CAppDialog
  {
class="kw">private:
   CDatePicker        StartDate;
   CDatePicker        EndDate;
   CLabel             Date;
   CGraphic           Graphic;
   CLabel             ShowLabel;
   CCheckGroup        Symbols;
   CCheckGroup        BalEquit;
   CCheckGroup        Deals;
   class="type">class="kw">string             ar_Symbols[];
   CLabel             TotalProfit;
   CLabel             TotalProfitVal;
   CLabel             GrossProfit;
   CLabel             GrossProfitVal;
   CLabel             GrossLoss;
   CLabel             GrossLossVal;
   CLabel             TotalTrades;
   CLabel             TotalTradesVal;
   CLabel             LongTrades;
   CLabel             LongTradesVal;
   CLabel             ShortTrades;
   CLabel             ShortTradesVal;
   class=class="str">"cmt">//---
   COrdersCollection Orders;
class="kw">public:
                     CStatisticsPanel();
                     ~CStatisticsPanel();
   class=class="str">"cmt">//--- 主应用程序对话框的创建和注销
   class="kw">virtual class="type">bool      Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2);
   class="kw">virtual class="type">void      Destroy(const class="type">int reason=REASON_PROGRAM);
   class=class="str">"cmt">//--- 图表事件处理器
   class="kw">virtual class="type">bool      OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam);
class="kw">protected:
让小布替你跑这套
这些多品种信号历史采集与回撤峰值标注的逻辑,小布盯盘的 AIGC 诊断已内置,打开对应信号品种页就能直接看分布,不必自己写类与面板。

常见问题

终端默认按单品种分页呈现,跨品种同时开仓、资金负荷与整体回撤在时间上被割裂,人工翻页比对容易漏掉重合点,也难以量化最大缩水出现在哪一刻。
基类 CObject 的 Compare 用于对象排序,重写后按订单执行日期升序排列,方便后续把不同品种订单合并成统一时间序列再做统计。
可以,小布盯盘对应品种页内置了信号历史采集与图形化摘要,能呈现均匀性、同时开仓数与回撤时刻,省去自己搭面板的重复劳动。
动态面板主要响应品种切换与数据刷新,重绘资金曲线、持仓负荷和回撤标记,避免手动重开图表来看最新统计。
需要把同时间截面的多品种订单按方向归类,若正反方向并存且净敞口低倾向对冲,若同向累加则倾向增加资金负荷,具体看文中统计构建方式。