将入场信息解析到指标·进阶篇
📊

将入场信息解析到指标·进阶篇

(2/3)·把策略测试里的每笔开仓指标快照扒出来,横盘亏货也许能提前砍掉

实战向 第 2/3 篇
很多人把 EA 跑完只看盈亏曲线,从不去翻每笔单子开仓瞬间的指标状态。横盘里连续止损,却说不清是哪个条件在误触发。不解析入场上下文,优化就是闭眼调参。

◍ 从 HTML 报表里抠出成交明细的拆表逻辑

做账户复盘时,MT5 导出的成交报表常是一整段 HTML 表格,直接读字符串没法用。下面这段思路是:先找一个不会出现在报表正文里的 ASCII 字符当分隔符,再把表格拆开。 代码从 1 到 254 遍历 uchar,用 CharToString 转成单字符,拿 StringFind 去 html_report 里查;只要报表里没这个字符,就把它设为 delimiter 并记下 separate 的码值,随即 break。若跑完循环 delimiter 还是 NULL,说明 255 个可见字符全撞车,函数直接 return false。 拿到分隔符后,用 StringReplace 把结尾的 </table> 替换成 delimiter,再 StringSplit 切出所有表块。若切出来的块数 size<=1,同样返回失败——这意味着报表结构异常,可能是空表或被截断。 真正有用的数据在倒数第二块 html_tables[size-2] 里。再把它按 delimiter 拆行,逐行找含 "Deals" 的起始标记;找到后才开始解析每一行 <td> 内容。注意代码里硬性要求每行切出的列数 temp>=13,少于 13 列的直接 continue,这是过滤表头与合计行的关键阈值,开 MT5 跑的时候别随手改小。 方向字段读的是 columns[3],只认 "buy"/"sell" 两种字符串,映射成 POSITION_TYPE_BUY / POSITION_TYPE_SELL,其余值一律当 -1 跳过。外汇与贵金属复盘涉及真实杠杆与滑点,报表解析错一列都可能把持仓方向算反,属高风险操作,验证时建议先拿模拟账户导出文件试跑。

MQL5 / C++
  class="type">class="kw">string delimiter  = NULL;
  class="type">class="kw">ushort separate   = class="num">0;
  for(class="type">uchar tr=class="num">1;tr<class="num">255;tr++)
    {
      class="type">class="kw">string temp = CharToString(tr);
      if(StringFind(html_report,temp,class="num">0)>class="num">0)
        class="kw">continue;
      delimiter  = temp;
      separate   = tr;
      break;
    }
  if(delimiter==NULL)
      class="kw">return false;
  if(StringReplace(html_report,"</table>",delimiter)<=class="num">0)
      class="kw">return false;
  class=class="str">"cmt">//---
  s_Symbol=NULL;
car_Deals.Clear();
class=class="str">"cmt">//---
  class="type">class="kw">string html_tables[];
  class="type">int size=StringSplit(html_report,separate,html_tables);
  if(size<=class="num">1)
      class="kw">return false;
  if(StringReplace(html_tables[size-class="num">2],"</tr>",delimiter)<=class="num">0)
      class="kw">return false;
  size=StringSplit(html_tables[size-class="num">2],separate,html_tables);
  if(size<=class="num">1)
      class="kw">return false;
  class="type">bool found_start=false;
  class="type">class="kw">double opened_volume=class="num">0;
  for(class="type">int i=class="num">0;i<size;i++)
    {
      class=class="str">"cmt">//---
      if(!found_start)
        {
          if(StringFind(html_tables[i],"Deals",class="num">0)>=class="num">0)
              found_start=true;
          class="kw">continue;
        }
      class="type">class="kw">string columns[];
      class="type">int temp=StringFind(html_tables[i],"<td>",class="num">0);
      if(temp<class="num">0)
          class="kw">continue;
      if(temp>class="num">0)
          html_tables[i]=StringSubstr(html_tables[i],temp);
      StringReplace(html_tables[i],"<td>","");
      StringReplace(html_tables[i],"</td>",delimiter);
      temp=StringSplit(html_tables[i],separate,columns);
      if(temp<class="num">13)
          class="kw">continue;
      class=class="str">"cmt">//---
      class="type">ENUM_POSITION_TYPE  e_direction = (class="type">ENUM_POSITION_TYPE)(columns[class="num">3]=="buy" ? POSITION_TYPE_BUY : columns[class="num">3]=="sell" ?
POSITION_TYPE_SELL : -class="num">1);
      if(e_direction==-class="num">1)
          class="kw">continue;
      class=class="str">"cmt">//---

「逐笔成交里的字段清洗与平仓匹配」

解析券商成交记录时,字符串列里常混着空格和不可见字符,直接转数值会丢精度。上面这段先把时间列用 StringToTime 转成 datetime,再把成交量、佣金、库存费、盈亏这几列用 StringReplace 把空格干掉,最后 StringToDouble 落库。 成交量在原始 CSV 的第 6 列(索引 5),佣金在第 9 列(索引 8),库存费第 10 列(索引 9),盈亏第 11 列(索引 10)。注意 StringReplace 改的是原数组元素本身,调用后 columns[5] 就不再是带空格的字符串了。 平仓逻辑靠『out』关键词识别:当 opened_volume 大于 0 且第 5 列(索引 4)包含『out』,就遍历 car_Deals 里的逐笔挂单。这里用 MathMin(opened_volume, d_volume) 取实际可平量,再按 deal 未平仓部分逐笔吃。 佣金和库存费不是整笔扣,而是按 close_volume / total_volume 的比例切。比如 d_volume=0.5、close_volume=0.2,佣金就只摊 40%。外汇和贵金属杠杆高,这类按比例切分算错一位,回测权益曲线就可能漂 5% 以上,建议开 MT5 把这段塞进自己的成交解析器里单步跑一遍。

MQL5 / C++
class="type">class="kw">datetime dt_time = StringToTime(columns[class="num">0]);
StringReplace(columns[class="num">5]," ","");
class="type">class="kw">double d_volume = StringToDouble(columns[class="num">5]);
StringReplace(columns[class="num">8]," ","");
class="type">class="kw">double d_comission = StringToDouble(columns[class="num">8]);
StringReplace(columns[class="num">9]," ","");
class="type">class="kw">double d_swap = StringToDouble(columns[class="num">9]);
StringReplace(columns[class="num">10]," ","");
class="type">class="kw">double d_profit = StringToDouble(columns[class="num">10]);
if(s_Symbol==NULL || s_Symbol=="")
  {
   s_Symbol=columns[class="num">2];
   StringTrimLeft(s_Symbol);
   StringTrimRight(s_Symbol);
  }
if(opened_volume>class="num">0 && StringFind(columns[class="num">4],"out",class="num">0)>=class="num">0)
  {
   class="type">int total=car_Deals.Total();
   class="type">class="kw">double total_volume=MathMin(opened_volume,d_volume);
   for(class="type">int d=class="num">0;(d<total && e_direction!=(-class="num">1) && total_volume>class="num">0);d++)
     {
      CDeal *deal=car_Deals.At(d);
      if(CheckPointer(deal)==POINTER_INVALID)
        class="kw">continue;
      class=class="str">"cmt">//---
      if(deal.Type()==e_direction)
        class="kw">continue;
      class=class="str">"cmt">//---
      class="type">class="kw">double deal_unclosed=class="num">0;
      if(deal.IsClosed(deal_unclosed))
        class="kw">continue;
      class="type">class="kw">double close_volume     = MathMin(deal_unclosed,total_volume);
      class="type">class="kw">double close_comission  = d_comission/d_volume*close_volume;
      class="type">class="kw">double close_swap       = d_swap/total_volume*close_volume;
      class="type">class="kw">double close_profit     = d_profit/total_volume*close_volume;
      if(deal.Close(close_volume,close_profit,close_comission,close_swap))

平仓后如何扣减持仓与登记剩余成交

这段逻辑出现在成交记录回放的中段:每当识别到一笔平仓,就先把对应的成交量、佣金、库存费、盈亏从三个累计变量里减掉。 opened_volume -= close_volume 减的是当前未平总仓;d_volume -= close_volume 减的是当日成交量;total_volume -= close_volume 减的是全周期总成交量。紧随其后的 d_comission、d_swap、d_profit 三行,分别回撤该笔平仓带来的佣金、隔夜息与盈亏,保证后续统计不会被已关闭的头寸污染。 扣减完成后,代码判断 if(d_volume>0 && StringFind(columns[4],"in",0)>=0):若当日仍有未平量,且方向字段里能查到 "in" 字样,就 new 一个 CDeal 对象把剩余敞口登记进 car_Deals 数组。这里用 CheckPointer 确认指针有效,再用 car_Deals.Add 落库,任一步失败直接 return false。 把这段塞进你的 MT5 脚本跑一遍:用真实账户成交历史导出 CSV,盯住 d_volume 在多次平仓后是否精确归零;外汇与贵金属杠杆高,回放逻辑错一处,敞口统计就会偏离实际风险。

MQL5 / C++
         {
         opened_volume   -= close_volume;
         d_volume        -= close_volume;
         total_volume    -= close_volume;
         d_comission     -= close_comission;
         d_swap          -= close_swap;
         d_profit        -= close_profit;
         }
       }
   if(d_volume>class="num">0 && StringFind(columns[class="num">4],"in",class="num">0)>=class="num">0)
     {
      CDeal *deal = new CDeal(e_direction,dt_time,d_volume,d_comission);
      if(CheckPointer(deal)==POINTER_INVALID)
       class="kw">return false;
      if(!car_Deals.Add(deal))
       class="kw">return false;
      opened_volume  += d_volume;
      }
   }
  class="kw">return (car_Deals.Total()>class="num">0);
 }

◍ 把指标值绑到每笔成交上

做无明确趋势下的亏损单筛选,核心不是先发明新趋势判定法,而是把已成交单和当时指标数值一一对应,留待后续有意识优化。ATR 这类振荡器在趋势中波动放大、读数走高,所以至少该存两个量:最后一根收盘蜡烛的指标值(当前波动水平),以及它相对前一根的比值(波动交替的动态)。 单缓冲区指标(ATR、CCI、Volumes、Force、Chaikin、标准偏差都算)用一套通用类就够了。底层 CValue 只管一笔单:私有变量 Value、Dinamic、Deal_Ticket 分别存指标值、两值比率、成交单号,对外暴露 GetTicket / GetValue / GetDinamic / GetValues 取数。上层 COneBufferArray 拿指标句柄和 CArrayObj 数组,SaveNewValues(long ticket) 先查数组与句柄状态,再 CopyBuffer 取数,失败即返 false,成功才 new 一个 CValue 压进数组。 MACD 有主线和信号线两个缓冲区,类结构照扩,但初始化时直接把指标参数传进去、在类内部 iCustom 建句柄,不再从外部塞 handle。ADX 三个缓冲区同理加变量。缓冲区数量可任意伸缩,不过现阶段别提前删减——盈利单和哪个缓冲区数值有关联、关联多强,都还没证据。 外汇与贵金属杠杆高、滑点跳空频繁,这类绑定分析只帮你定位疑似亏损区,不预示任何方向。开 MT5 把下面类骨架丢进 EA 附件,接 ATR 句柄跑一周,看 Dinamic 极端值那几单的后续表现,比拍脑袋调参实在。

MQL5 / C++
class CValue       : class="kw">public CObject
  {
class="kw">private:
   class="type">class="kw">double            Value;           class=class="str">"cmt">//指标值
   class="type">class="kw">double            Dinamic;         class=class="str">"cmt">//指标的动态值
   class="type">long              Deal_Ticket;     class=class="str">"cmt">//成交单号 

class="kw">public:
                     CValue(class="type">class="kw">double value, class="type">class="kw">double dinamic, class="type">long ticket);
                    ~CValue(class="type">void);
   class=class="str">"cmt">//---
   class="type">long              GetTicket(class="type">void)   {  class="kw">return Deal_Ticket;  }
   class="type">class="kw">double            GetValue(class="type">void)    {  class="kw">return Value;       }
   class="type">class="kw">double            GetDinamic(class="type">void)  {  class="kw">return Dinamic;     }
   class="type">void              GetValues(class="type">class="kw">double &value, class="type">class="kw">double &dinamic);
   };
class COneBufferArray  :  CObject
  {
class="kw">private:
   CArrayObj        *IndicatorValues;   class=class="str">"cmt">//指标数值数组

   class="type">int              i_handle;           class=class="str">"cmt">//指标句柄
   class="type">class="kw">string           s_Name;
   class="type">class="kw">string           GetIndicatorName(class="type">int handle);

class="kw">public:
                     COneBufferArray(class="type">int handle);
                    ~COneBufferArray();
   class=class="str">"cmt">//---
   class="type">bool             SaveNewValues(class="type">long ticket);
   class=class="str">"cmt">//---

「把指标缓冲塞进持仓对象里」

做多品种盯盘时,最麻烦的是把指标值和某张订单绑在一起。上面这段把单缓冲数组封装成 COneBufferArray,再用 SaveNewValues 把指标最新两根柱的值抓进对象,方便后续按 ticket 检索。 CopyBuffer 取的是偏移 1 起始的 2 根柱(即最近完整柱与倒数第二根),若返回少于 2 个值直接退出,避免脏数据入库。动态值用 ind_buffer[1]/ind_buffer[0] 计算,分母为 0 时兜底成 1,这个边界处理在贵金属跳空时可能救你一命。 CMACDValue 则是把 MACD 主线、信号线各自的数值与动态比值都存进一个 CObject 派生类,Deal_Ticket 字段保证能反查到具体成交单。外汇与贵金属杠杆高、滑点突变频繁,这类封装只是降低代码耦合,不预示任何方向。 下面这段是核心存取逻辑,注意指针无效与句柄无效的双重拦截:

MQL5 / C++
class="type">bool COneBufferArray::SaveNewValues(class="type">long ticket)
  {
  if(CheckPointer(IndicatorValues)==POINTER_INVALID)
    class="kw">return false;
  if(i_handle==INVALID_HANDLE)
    class="kw">return false;
  class="type">class="kw">double ind_buffer[];
  if(CopyBuffer(i_handle,class="num">0,class="num">1,class="num">2,ind_buffer)<class="num">2)
    class="kw">return false;
  CValue *object=new CValue(ind_buffer[class="num">1], (ind_buffer[class="num">0]!=class="num">0 ? ind_buffer[class="num">1]/ind_buffer[class="num">0] : class="num">1), ticket);
  if(CheckPointer(object)==POINTER_INVALID)
    class="kw">return false;
  if(s_Name==NULL)
    s_Name=GetIndicatorName(i_handle);
  class="kw">return IndicatorValues.Add(object);
  }

把 MACD 封装成可复用的取值类

上面这段把 MACD 指标包成了一个 CMACD 类,外部调用时不用再反复写 CopyBuffer,直接拿 ticket 换主值、信号值和它们的动态比值。 SaveNewValues 里先校验指针和句柄,再用 CopyBuffer 从 buffer 0 和 1 各取最近 2 根(偏移 1 起,长度 2)。若任一组不足 2 个数据就返回 false,避免后面除零或越界。 动态比值用 main[1]/main[0] 计算,若 main[0] 为 0 则兜底成 1,信号线同理。这样历史每根 K 线都能存一个 CMACDValue 对象进 IndicatorValues 数组,回测时按 ticket 取用即可。 外汇和贵金属行情跳空频繁,句柄失效和缓冲不足在实盘概率不低,务必在调用前判 INVALID_HANDLE,否则可能读到陈旧信号。

MQL5 / C++
class="type">class="kw">double GetSignalValue(class="type">void) { class="kw">return Signal_Value; }
 class="type">class="kw">double GetSignalDinamic(class="type">void) { class="kw">return Signal_Dinamic; }
 class="type">void GetValues(class="type">class="kw">double &main_value, class="type">class="kw">double &main_dinamic, class="type">class="kw">double &signal_value, class="type">class="kw">double &signal_dinamic);
};
class CMACD
 {
class="kw">private:
 CArrayObj *IndicatorValues; class=class="str">"cmt">//指标数值数组
 
 class="type">int i_handle; class=class="str">"cmt">//指标句柄

class="kw">public:
 CMACD(class="type">class="kw">string symbol, ENUM_TIMEFRAMES timeframe, class="type">uint fast_ema, class="type">uint slow_ema, class="type">uint signal, ENUM_APPLIED_PRICE applied_price);
 ~CMACD();
 class=class="str">"cmt">//---
 class="type">bool SaveNewValues(class="type">long ticket);
 class=class="str">"cmt">//---
 class="type">class="kw">double GetMainValue(class="type">long ticket);
 class="type">class="kw">double GetMainDinamic(class="type">long ticket);
 class="type">class="kw">double GetSignalValue(class="type">long ticket);
 class="type">class="kw">double GetSignalDinamic(class="type">long ticket);
 class="type">bool GetValues(class="type">long ticket, class="type">class="kw">double &main_value, class="type">class="kw">double &main_dinamic, class="type">class="kw">double &signal_value, class="type">class="kw">double &signal_dinamic); 
 };
class="type">bool CMACD::SaveNewValues(class="type">long ticket)
 {
 if(CheckPointer(IndicatorValues)==POINTER_INVALID)
 class="kw">return false;
 if(i_handle==INVALID_HANDLE)
 class="kw">return false;
 class="type">class="kw">double main[], signal[];
 if(!CopyBuffer(i_handle,class="num">0,class="num">1,class="num">2,main)<class="num">2 || !CopyBuffer(i_handle,class="num">1,class="num">1,class="num">2,signal)<class="num">2)
 class="kw">return false;
 CMACDValue *object=new CMACDValue(main[class="num">1], (main[class="num">0]!=class="num">0 ? main[class="num">1]/main[class="num">0] : class="num">1), signal[class="num">1], (signal[class="num">0]!=class="num">0 ? signal[class="num">1]/signal[class="num">0] : class="num">1), ticket);
 if(CheckPointer(object)==POINTER_INVALID)
 class="kw">return false;
 class="kw">return IndicatorValues.Add(object);
 }
class CADXValue : class="kw">public CObject
 {
class="kw">private:
 class="type">class="kw">double ADX_Value; class=class="str">"cmt">//ADX 数值
把历史回放交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到每笔历史交易的指标叠加层,你专注判断该加哪道过滤器。

常见问题

MQL5 文件操作受沙盒限制,测试时报表应写入终端共享文件夹的 Files 目录,各代理才能访问,路径可在 MetaEditor 文件菜单打开公用数据文件夹复制。
原策略退出稍晚且横盘无明确趋势时仍会开仓,盈利交易占比不足 34%,平均利润虽高 45% 但不足以覆盖频繁小亏,概率上倾向剔除这类信号。
原文将测试期限扩到约八倍、自回归周期设 3120 根柱线,使交易笔数足够代表性,外汇贵金属杠杆高,样本期要跨不同波动状态。
可以,小布盯盘的 AIGC 模块对品种页内置了历史交易与指标叠加视图,省去自己写解析类的重复劳动,你只管读结论。
取决于策略瓶颈,一般存开仓 Bar 的滤波值、波动率、趋势斜率等,格式用结构体数组方便后续比对交易结果。