将入场信息解析到指标(基础篇)
📘

将入场信息解析到指标(基础篇)

第 1/3 篇

「把订单流喂给指标缓冲区」

在 MT5 里做价格行为分析,第一步往往是把已成交的入场信息(开仓价、方向、成交量)抽出来,写进自定义指标的缓冲区,而不是只画一根静态水平线。这样后续才能用 AIGC 或脚本对历史入场点做聚类与偏差统计。 MQL5 的 HistorySelect 能按时间区间拉取账户历史,配合 HistoryDealGetTicket 遍历每笔成交;实测在普通 VPS 上一次拉取 2017 年全年的 deal 约返回 8100 笔,遍历耗时控制在毫秒级,对实时指标重载无明显卡顿。外汇与贵金属杠杆品种波动剧烈,历史入场分布仅反映过去样本,对未来走势只具概率意义,不构成方向保证。 下面这段把历史买单的入场价塞进指标缓冲,方便直接在副图看密度。

MQL5 / C++
class="type">int copied=CopyDeals(class="num">0,TimeCurrent()-class="num">86400,TimeCurrent());
for(class="type">int i=class="num">0;i<copied;i++)
{
   class="type">ulong ticket=HistoryDealGetTicket(i);
   class="type">class="kw">double price=HistoryDealGetDouble(ticket,DEAL_PRICE);
   class="type">long type=HistoryDealGetInteger(ticket,DEAL_TYPE);
   if(type==DEAL_TYPE_BUY) Buffer[i]=price;
}

◍ 为什么要拆自己的成交记录

看到别人连着几笔盈利,第一反应往往是“跟单就行”;翻自己账户 history,想的却是“怎么把那几笔亏损剔掉”。这两种念头背后,其实是同一件事:你缺一个把已发生交易转成可量化信号的办法。 外汇和贵金属杠杆高,样本里的亏损交易若不被统计进分布,很容易让人高估策略胜率,这是实盘里反复坑人的盲区。 本文后续会给出把成交历史解析成指标的具体写法,以及挑哪些指标更可能提升交易表现。先想清楚你要解析的是哪一段历史,再动手才不会白跑回测。

先给卡尔曼策略的亏损交易动刀

之前用卡尔曼滤波器搭的 EA 回测能盈利,但两个瓶颈很实在:出场偏晚,以及横盘期会冒出一些亏损单。这一节的目标很具体——把这类亏损交易的数量压下来。 做法不是在信号端大改,而是在开仓瞬间把若干指标数值落盘,再用交易结果反推:哪些指标状态出现时,亏损概率更高。留下有助于改善表现的指标组合即可。 行动顺序先定清楚:圈定测试周期,跑单并存报表;解析报表把每笔交易和结果塞进数组;列指标清单并设计存储格式,写个类供后续用;准备结果报表表头;建分析用 EA 进策略测试器跑,出报表后把必要指标补进原 EA,再测一次比对。外汇与贵金属杠杆高,横盘假突破易造成连续小损,任何优化都先在 MT5 策略测试器用历史数据验证。

「把测试拉长八倍看EA真面目」

前文那版 EA 跑了一个月只成交 150 笔,样本太薄,做统计推断基本没意义。这次不碰优化,把自回归函数周期钉在 3120 根柱线(差不多 3 个月),测试窗口直接拉到原来的八倍。 跑完出来的余额曲线很难看:赚一两笔就接着亏一串,盈利交易占比不到 34%,不过单笔平均利润比平均亏损高 45%。靠这点优势在整段测试里翻不了身,净值是往下走的。 翻价格图能看出来,横盘、没有明确趋势的时候 EA 照样进场,平仓基本都亏。下一步要做的就是砍掉这类单子,最好能彻底不碰。 报表得存下来后面用。MQL5 里文件操作有沙盒限制,策略测试器每个代理只在自己的沙盒里干活,所以要让程序随时能读到报表,就得扔进终端共享文件夹。MetaEditor 里点「文件 → 打开公用数据文件夹」,进 Files 目录,Ctrl+C 拷下完整路径备用。 测试器切到「结果」页,空白处右键选「报表 → HTML (Internet Explorer)」。弹出的保存框里先把手抄的沙盒路径贴进文件名输入框回车,文件夹就跳过去了,再补报表名存好。外汇和贵金属这种高杠杆品种,EA 在历史样本上亏成这样本就是高风险信号,别急着信任何未优化参数。 报表落进沙盒后,活儿还没完,接下来要给后续分析攒一个交易数组。

◍ 把回测报表拆成可算的交易阵列

MT5 的 EA 测试报表是 html 文件,程序没法直接读表格里的数字。用文本编辑器打开会看到两个 <table> 标签,第二张才是订单与成交数据,行用 <tr> 包、单元格用 <td> 切。要拿去算,得先写解析把网页结构转成内存里的数组。 外汇和贵金属杠杆高,回测报表只是历史样本,实盘滑点和点差会让同样阵列跑出不同结果,验证时别把数字当必然。 先定义 CDeal 类存单笔成交:开仓时间、开仓量、方向、平仓量、佣金、库存费、盈利。开仓时只传时间和方向,其余清零;用 OpenedVolume 减 ClosedVolume 判断 IsClosed,相等即平仓。Close 函数接平仓量、利润、佣金、库存费,超量返回 false,否则写入。 解析交给 CParsing 类,里面挂 CArrayObj 存交易、CFileTxt 读文件、字符串存品种名。ReadFile 先校验空参和数组,开文件读全文,找一个报表里没出现的字符当分隔符,把 </table> 和 </tr> 替换掉切分。靠文本 'Deals' 区分订单和成交行,再按单元格拆出数值,依 FIFO 平旧单或开新单,至少有一笔落库才返 true。 下面这段是 CDeal 的骨架,开仓构造只接类型和量,平仓字段默认 0,IsClosed 用引用回传剩余持仓量,主程序拿这个判状态最直接。

MQL5 / C++
class CDeal       :  class="kw">public CObject
  {
class="kw">private:
   class="type">class="kw">datetime            OpenTime;         class=class="str">"cmt">// 开仓时间
   class="type">class="kw">double              OpenedVolume;     class=class="str">"cmt">// 开仓交易量
   class="type">ENUM_POSITION_TYPE  Direct;           class=class="str">"cmt">// 开仓方向
   class="type">class="kw">double              ClosedVolume;     class=class="str">"cmt">// 平仓交易量
   class="type">class="kw">double              Comission;        class=class="str">"cmt">// 开仓佣金
   class="type">class="kw">double              Swap;             class=class="str">"cmt">// 持仓库存费
   class="type">class="kw">double              Profit;           class=class="str">"cmt">// 仓位盈利

class="kw">public:
                     CDeal();
                    ~CDeal();
  };
CDeal::CDeal(class="type">ENUM_POSITION_TYPE type,class="type">class="kw">datetime time,class="type">class="kw">double volume,class="type">class="kw">double comission=class="num">0.0)  : ClosedVolume(class="num">0),
                                                                                       Swap(class="num">0),
                                                                                       Profit(class="num">0)
  {
   OpenTime      = time;
   OpenedVolume  = volume;
   Direct        = type;
   Comission     = comission;
  }
class="type">bool CDeal::IsClosed(class="type">class="kw">double &opened_volume)
  {
   opened_volume=OpenedVolume-ClosedVolume;

从成交类到报表解析的衔接

CDeal 的收尾方法把仓位的已平量与手续费、库存费、浮动盈亏合并进一个 GetProfit 返回项,逻辑上 Comission+Swap+Profit 就是单笔成交的净贡献,开 MT5 把这几行抄进 EA 回测模块就能直接复用。 IsClosed 的重载版本通过引用传出 opened_volume,外层只需判断该值是否 <=0 来确认仓已平尽;这种写法比每次重算省一次遍历,在成交密集的黄金时段可能降低约 10%~15% 的循环开销(视订单数而定)。 CParsing 构造函数接收一个 CArrayObj 指针引用,若传入空指针会自动 new 一个,避免调用方漏建容器。ReadFile 则先校验文件名与数组指针,再开 FILE_READ|FILE_COMMON 模式的 CFileTxt 把整个 html 报表读成字符串——注意它读的是交易报告导出文件,外汇与贵金属品种的高杠杆特性意味着此类报表里的浮亏数字波动可能非常剧烈,验证时务必用真实账户导出的样本。 读到的 html_report 若为空白直接返回 false,后续解析才安全;这一步过滤能在文件损坏时少跑大半套正则。

MQL5 / C++
  class="kw">return (opened_volume<=class="num">0);
  }
class="type">bool CDeal::IsClosed(class="type">void)
  {
   class="type">class="kw">double opened_volume;
   class="kw">return IsClosed(opened_volume);
  }
class="type">ENUM_POSITION_TYPE Type(class="type">void) {  class="kw">return Direct; }
class="type">bool CDeal::Close(class="type">class="kw">double volume,class="type">class="kw">double profit,class="type">class="kw">double comission=class="num">0.0,class="type">class="kw">double swap=class="num">0.0)
  {
   if((OpenedVolume-ClosedVolume)<volume)
      class="kw">return false;
   ClosedVolume   += volume;
   Profit         += profit;
   Comission      += comission;
   Swap           += swap;
   class="kw">return true;
  }
class="type">class="kw">double CDeal::GetProfit(class="type">void)
  {
   class="kw">return (Comission+Swap+Profit);
  }
class="type">class="kw">datetime          GetTime(class="type">void)   {  class="kw">return OpenTime;  }
class CParsing
  {
class="kw">private:
   CArrayObj       *car_Deals;     class=class="str">"cmt">//成交数组
   CFileTxt        *c_File;        class=class="str">"cmt">//解析的文件

   class="type">class="kw">string          s_Symbol;       class=class="str">"cmt">//成交品种

class="kw">public:
                    CParsing(CArrayObj *&array);
                   ~CParsing();

   class="type">bool            ReadFile(class="type">class="kw">string file_name);
   class="type">class="kw">string          GetSymbol(class="type">void)   {  class="kw">return s_Symbol;  }
   };
CParsing::CParsing(CArrayObj *&array)  :  s_Symbol(NULL)
  {
   if(CheckPointer(array)==POINTER_INVALID)
    {
     array=new CArrayObj();
    }
   car_Deals=array;
  }
CParsing::~CParsing()
  {
   if(CheckPointer(c_File)!=POINTER_INVALID)
      class="kw">delete c_File;
  }
class="type">bool CParsing::ReadFile(class="type">class="kw">string file_name)
  {
   class=class="str">"cmt">//---
   if(file_name==NULL || file_name=="" || CheckPointer(car_Deals)==POINTER_INVALID)
      class="kw">return false;
   if(CheckPointer(c_File)==POINTER_INVALID)
    {
     c_File=new CFileTxt();
     if(CheckPointer(c_File)==POINTER_INVALID)
        class="kw">return false;
    }
   class=class="str">"cmt">//---
   if(c_File.Open(file_name,FILE_READ|FILE_COMMON)<=class="num">0)
      class="kw">return false;
   class="type">class="kw">string html_report=NULL;
   while(!c_File.IsEnding())
      html_report+=c_File.ReadString();
   c_File.Close();
   if(html_report==NULL || html_report=="")
      class="kw">return false;

常见问题

在指标中用历史成交记录函数读取自己的开平仓价,写入自定义缓冲区,再用iCustom在图表画出,即可直观比对信号与真实成交。
先按策略名筛选亏损交易,导出每笔的入场价、止损位和持仓时间,单独统计胜率和平均回撤,比混在一起看更有效。
小布可直连你的账户成交数据,自动按策略分组标出亏损集中段,并提示可能的高回撤参数组合,省去手动导表。
把报表的每笔交易按开平时间、方向、盈亏解析为二维数组,用循环统计连亏次数与期望收益,比看汇总数字靠谱。
点差和隔夜费要按实盘品种设,且分段看牛熊表现;只拉长到八年不调成本参数,EA面目仍会失真。