图表上的历史仓位及其盈利/亏损图指标·综合运用
📊

图表上的历史仓位及其盈利/亏损图指标·综合运用

(3/3)·净额账户有现成函数,对冲平仓后却无历史仓位接口,这篇补齐整条链路

进阶 第 3/3 篇
很多交易者以为平仓后订单就只剩在报表里翻,代码里没法按仓位重绘盈亏曲线。其实每笔成交都带着仓位唯一 ID,顺着 HistoryDeal 抓 ID 就能还原已平仓位,只是多数人没把这套对象链搭起来。

「成交记录类的排序与构造细节」

在自建的成交封装类里,比较逻辑决定了历史成交如何被遍历和归并。当排序模式设为按品种时,直接比对 Symbol() 字符串,大于返回 1、小于返回 -1、相等返回 0;其余情况一律回退到 TimeMsc() 毫秒级时间戳做同样的三态比较,保证同毫秒内的成交也有稳定次序。 Print() 方法用 PrintFormat 把每笔成交的入口描述、类型描述、票据号和时间戳拼成一行日志,方便在 MT5 终端里对照策略实际触发点。时间字段特意调用 TimeMSCtoString 转成可读串,避免肉眼去算毫秒偏移。 构造函数只接收一个 deal_ticket,随后通过 HistoryDealGetInteger 把 Magic、PositionID、TimeMsc、Time 和 Type 一次性拉进成员变量。外汇与贵金属交易的高杠杆特性下,这类毫秒级成交归档对复盘滑点和重算权益曲线尤为关键,建议在策略退出前先跑一遍该类确认历史抓取无遗漏。

MQL5 / C++
    case DEAL_SORT_MODE_SYMBOL     :  class="kw">return(this.Symbol()>compared_obj.Symbol()        ?  class="num">1  :  this.Symbol()<compared_obj.Symbol()          ?  -class="num">1 :  class="num">0);
    class="kw">default                          :  class="kw">return(this.TimeMsc()>compared_obj.TimeMsc()     ?  class="num">1  :  this.TimeMsc()<compared_obj.TimeMsc()        ?  -class="num">1 :  class="num">0);
   }
  }

class=class="str">"cmt">//--- Print deal properties in the journal
   class="type">void         Print(class="type">void)
      {
         ::PrintFormat("  Deal: %s type %s #%lld at %s",this.EntryDescription(),this.TypeDescription(),this.Ticket(),this.TimeMSCtoString(this.TimeMsc()));
      }
class=class="str">"cmt">//--- Constructor
            CDeal(class="kw">const class="type">long deal_ticket)
      {
         this.m_ticket=deal_ticket;
         this.m_magic=::HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);                class=class="str">"cmt">// Magic number for a deal
         this.m_position_id=::HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);    class=class="str">"cmt">// Position ID
         this.m_time_msc=::HistoryDealGetInteger(deal_ticket,DEAL_TIME_MSC);           class=class="str">"cmt">// Deal execution time in milliseconds
         this.m_time=(class="type">class="kw">datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);         class=class="str">"cmt">// Deal execution time
         this.m_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(deal_ticket,DEAL_TYPE);   class=class="str">"cmt">// Deal type

◍ 从成交单号抓取逐笔字段

做历史回测或复盘时,单笔成交的全部成本结构都要从 HistoryDealGet 系列函数里抠出来,而不是只看一个平仓盈亏。下面这段构造函数把一张 deal_ticket 对应的关键字段一次性读进对象成员,省得后面反复查历史库。 this.m_entry=(ENUM_DEAL_ENTRY)::HistoryDealGetInteger(deal_ticket,DEAL_ENTRY); // 成交方向:开仓、平仓还是反手 this.m_volume=::HistoryDealGetDouble(deal_ticket,DEAL_VOLUME); // 成交手数 this.m_price=::HistoryDealGetDouble(deal_ticket,DEAL_PRICE); // 成交价格 this.m_comission=::HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION); // 该笔佣金 this.m_swap=::HistoryDealGetDouble(deal_ticket,DEAL_SWAP); // 平仓时累计的库存费 this.m_profit=::HistoryDealGetDouble(deal_ticket,DEAL_PROFIT); // 该笔财务结果 this.m_fee=::HistoryDealGetDouble(deal_ticket,DEAL_FEE); // 交易费 this.m_symbol=::HistoryDealGetString(deal_ticket,DEAL_SYMBOL); // 成交品种名 注意原文里 DEAL_PRICE 的注释误写成了 Deal volume,实际取的是价格,复制代码时别被注释带偏。外汇与贵金属杠杆品种跳空频繁,swap 与 commission 在极端点差下可能吞掉短线利润,实盘前应在 MT5 策略测试器用真实历史 tick 验证字段读取是否和账户成交记录一致。

MQL5 / C++
this.m_entry=(ENUM_DEAL_ENTRY)::HistoryDealGetInteger(deal_ticket,DEAL_ENTRY); class=class="str">"cmt">// Deal entry - entry in, entry out, reverse
this.m_volume=::HistoryDealGetDouble(deal_ticket,DEAL_VOLUME); class=class="str">"cmt">// Deal volume
this.m_price=::HistoryDealGetDouble(deal_ticket,DEAL_PRICE); class=class="str">"cmt">// Deal volume
this.m_comission=::HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION); class=class="str">"cmt">// Deal commission
this.m_swap=::HistoryDealGetDouble(deal_ticket,DEAL_SWAP); class=class="str">"cmt">// Accumulated swap when closing
this.m_profit=::HistoryDealGetDouble(deal_ticket,DEAL_PROFIT); class=class="str">"cmt">// Deal financial result
this.m_fee=::HistoryDealGetDouble(deal_ticket,DEAL_FEE); class=class="str">"cmt">// Deal fee
this.m_symbol=::HistoryDealGetString(deal_ticket,DEAL_SYMBOL); class=class="str">"cmt">// Name of the symbol, for which the deal is executed
}
~CDeal(class="type">void){}
};

仓位对象里到底存了哪些字段

做历史仓位回放前,先得把单个仓位的数据结构立住。CPosition 类在私有段声明了一组成员变量,用来缓存仓位的生命周期属性:从开平时间(毫秒级与 datetime 双存)、magic、开平成交单号、开平价格、成交量、多空类型到交易品种与小数位,全部一次性挂在对象里。 其中 m_time_in_msc 与 m_time_out_msc 用 long 存毫秒,是为了后面按任意时间框架反推柱体开启时间时不丢精度;m_list_deals 则是 CArrayObj 类型的成交列表,承载该仓位下的所有 deal 对象。 BarOpenTime() 这类辅助方法依赖这些字段:传一个时刻进去,就能算出它落在哪个图表周期的柱开盘时间,从而判断某根柱上是否含着这个仓位。当前柱盈亏则用简化近似——拿开盘价对比当前柱收盘价算点数,再乘点值,不追溯浮动盈利全过程,只为快速取已平仓位数据。 类构造函数接收仓位 ID,对象一建出来就凭 ID 从交易属性拉数据,再由历史仓位列表类把对应 deal 塞进 m_list_deals。外汇与贵金属杠杆高,这类结构只服务于复盘统计,实盘信号请自行加风控。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Position class                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPosition : class="kw">public CObject
  {
class="kw">private:
   CArrayObj         m_list_deals;                 class=class="str">"cmt">// List of position deals
   class="type">long              m_position_id;                class=class="str">"cmt">// Position ID
   class="type">long              m_time_in_msc;                class=class="str">"cmt">// Open time in milliseconds
   class="type">long              m_time_out_msc;               class=class="str">"cmt">// Close time in milliseconds
   class="type">long              m_magic;                      class=class="str">"cmt">// Position magic
   class="type">class="kw">datetime          m_time_in;                    class=class="str">"cmt">// Open time
   class="type">class="kw">datetime          m_time_out;                   class=class="str">"cmt">// Close time
   class="type">ulong             m_deal_in_ticket;             class=class="str">"cmt">// Open deal ticket
   class="type">ulong             m_deal_out_ticket;            class=class="str">"cmt">// Close deal ticket
   class="type">class="kw">double            m_price_in;                   class=class="str">"cmt">// Open price
   class="type">class="kw">double            m_price_out;                  class=class="str">"cmt">// Close price
   class="type">class="kw">double            m_volume;                     class=class="str">"cmt">// Position volume
   class="type">ENUM_POSITION_TYPE m_type;                      class=class="str">"cmt">// Position type
   class="type">class="kw">string            m_symbol;                     class=class="str">"cmt">// Position symbol
   class="type">int               m_digits;                     class=class="str">"cmt">// Symbol digits

「把品种参数和毫秒时间揉进类里」

写 EA 或指标时,把品种层面的基础变量直接挂到类成员,能少写一堆重复取数的代码。下面这几个字段就是典型:点值、合约大小、盈利币种、账户币种,一次赋值后续随调随用。 double m_point; // 品种单点价值 double m_contract_size; // 品种交易合约大小 string m_currency_profit; // 品种盈利结算币种 string m_account_currency; // 账户入金币种 MQL5 原生 TimeToString 只到秒,要做毫秒级日志就得自己拼。TimeMSCtoString 把毫秒时间戳除以 1000 丢给系统函数,余数用 IntegerToString 补三位零,输出形如 2024.01.05 13:22:01.457,回测排错时定位成交瞬间很实用。 另一处容易踩坑的是跨周期取 K 线开盘时间。BarOpenTime 对小于周线的周期直接用 time - time%PeriodSeconds(period);周线因为周一零点才对齐,要先把时间偏移 4 天再取模;月线走 MqlDateTime 分支另算。外汇和贵金属波动受数据面影响大,这类时间计算错了,统计信号就可能偏移到相邻 bar,实盘风险偏高,建议挂模拟盘先验证。

MQL5 / C++
class="type">class="kw">double m_point;                                                                       class=class="str">"cmt">// One symbol point value
class="type">class="kw">double m_contract_size;                                                                 class=class="str">"cmt">// Symbol trade contract size
class="type">class="kw">string m_currency_profit;                                                               class=class="str">"cmt">// Symbol profit currency
class="type">class="kw">string m_account_currency;                                                              class=class="str">"cmt">// Deposit currency
class=class="str">"cmt">//--- Return time with milliseconds
   class="type">class="kw">string TimeMSCtoString(class="kw">const class="type">long time_msc,class="type">int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS)
      {
       class="kw">return ::TimeToString(time_msc/class="num">1000,flags)+"."+::IntegerToString(time_msc%class="num">1000,class="num">3,&class="macro">#x27;class="num">0&class="macro">#x27;);
      }
class=class="str">"cmt">//--- Calculate and class="kw">return the open time of a known bar on a specified chart period by a specified time
class=class="str">"cmt">//--- ([MQL5官方文档]
   class="type">class="kw">datetime BarOpenTime(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time) class="kw">const
      {
       ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe);
       class=class="str">"cmt">//--- Calculate the bar open time on periods less than W1
       if(period<PERIOD_W1)
         class="kw">return time-time%::PeriodSeconds(period);
       class=class="str">"cmt">//--- Calculate the bar open time on W1
       if(period==PERIOD_W1)
         class="kw">return time-(time+class="num">4*class="num">24*class="num">60*class="num">60)%::PeriodSeconds(period);
       class=class="str">"cmt">//--- Calculate the bar open time on MN1
       else
         {
          class="type">MqlDateTime dt;
          ::ResetLastError();

◍ 把时间折算与品种可见性塞进工具类

这段代码把两个底层脏活包进了类方法:一是把 datetime 折算成「当月零点的秒数」,二是确保某个交易品种在服务器上存在并被加进 MarketWatch。前者靠 TimeToStruct 拆结构,失败就打印错误码并返 0,成功则用 time 减去当日已过秒数、再减去 (day-1) 天的秒数,24*60*60 这个常量在 MT5 里就是一天的秒数。 SymbolIsExist 先用 SymbolExist 判断自定义品种是否存在,不存在直接返 false;存在则调 SymbolSelect(symbol,true) 把它推到行情窗口,返回该调用的结果。注意 custom 变量虽声明但没被读取,属于冗余写法,复制时可删。 GetOnePointPrice 给了点值计算的第一层逻辑:若传入 time 为 0 直接返 0;若品种盈利货币与账户货币相同,直接返 m_point*m_contract_size。否则留了个 array[1] 的尾巴,准备去查「账户币+盈利币」交叉报价,这一段没写完,接下去要自己补汇率抓取。外汇与贵金属杠杆高,点值算错会放大仓位风险,建议开 MT5 把这几段贴进 EA 模板逐步断点。

MQL5 / C++
   if(!::TimeToStruct(time,dt))
      {
      ::PrintFormat("%s: TimeToStruct failed. Error %lu",__FUNCTION__,::GetLastError());
      class="kw">return class="num">0;
      }
   class="kw">return time-(time%(class="num">24*class="num">60*class="num">60))-(dt.day-class="num">1)*(class="num">24*class="num">60*class="num">60);
   }
class=class="str">"cmt">//--- Return the symbol availability flag on the server. Add a symbol to MarketWatch window
   class="type">bool            SymbolIsExist(class="kw">const class="type">class="kw">string symbol) class="kw">const
      {
      class="type">bool custom=false;
      if(!::SymbolExist(symbol,custom))
         class="kw">return false;
      class="kw">return ::SymbolSelect(symbol,true);
      }
class=class="str">"cmt">//--- Return the price of one point
class="type">class="kw">double            GetOnePointPrice(class="kw">const class="type">class="kw">datetime time) class="kw">const
      {
      if(time==class="num">0)
         class="kw">return class="num">0;
      class=class="str">"cmt">//--- If the symbol&class="macro">#x27;s profit currency matches the account currency, class="kw">return the contract size * Point of the symbol
      if(this.m_currency_profit==this.m_account_currency)
         class="kw">return this.m_point*this.m_contract_size;
      class=class="str">"cmt">//--- Otherwise, check for the presence of a symbol with the name "Account currency" + "Symbol profit currency" 
      class="type">class="kw">double array[class="num">1];

交叉币种盈亏的合约换算逻辑

当持仓品种的盈利币种与账户币种不一致时,MT5 不会自动给你折算成账户净值,得靠代码去市场报价里找对应的交叉符号。上面这段实现先拼出「账户币+盈利币」的反向符号名,比如账户是 USD、盈利币是 JPY,就拼出 USDJPY。 若反向符号存在于市场报价且能取到指定时间的收盘价,返回合约价值 = 点值 × 合约规模 ÷ 该收盘价;取不到就返回 0。接着再拼「盈利币+账户币」的正向符号,如 JPYUSD,存在则返回值 = 点值 × 合约规模 × 收盘价。 两种符号都查不到时直接返回 0,说明当前环境缺交叉报价,手动加品种到市场观察列表通常能解决。外汇与贵金属杠杆高,这类换算误差会放大净值波动,上机前建议在策略测试器用 2023 年 EURUSD 与 USDJPY 历史 Tick 跑一遍核对。

MQL5 / C++
class="type">class="kw">string reverse=this.m_account_currency+this.m_currency_profit;
class=class="str">"cmt">//--- If such a symbol exists and is added to the Market Watch
if(this.SymbolIsExist(reverse))
  {
   class=class="str">"cmt">//--- If the closing price of the bar by &class="macro">#x27;time&class="macro">#x27; is received, class="kw">return the contract size * Point of the symbol divided by the Close price of the bar 
   if(::CopyClose(reverse,PERIOD_CURRENT,time,class="num">1,array)==class="num">1 && array[class="num">0]>class="num">0)
     class="kw">return this.m_point*this.m_contract_size/array[class="num">0];
   class=class="str">"cmt">//--- If failed to get the closing price of the bar by &class="macro">#x27;time&class="macro">#x27;, class="kw">return zero
   else
     class="kw">return class="num">0;
  }
class=class="str">"cmt">//--- Check for the presence of a symbol with the name "Symbol profit currency" + "Account currency"
class="type">class="kw">string direct=this.m_currency_profit+this.m_account_currency;
class=class="str">"cmt">//--- If such a symbol exists and is added to the Market Watch
if(this.SymbolIsExist(direct))
  {
   class=class="str">"cmt">//--- If the closing price of the bar by &class="macro">#x27;time&class="macro">#x27; is received, class="kw">return the contract size * Point of the symbol multiplied by the Close price of the bar
   if(::CopyClose(direct,PERIOD_CURRENT,time,class="num">1,array)==class="num">1)
     class="kw">return this.m_point*this.m_contract_size*array[class="num">0];
  }
class=class="str">"cmt">//--- Failed to get symbols whose profit currency does not match the account currency, neither reverse nor direct - class="kw">return zero
class="kw">return class="num">0;

「持仓对象的只读属性接口」

在自建的持仓封装类里,这类方法只做一件事:把私有成员变量暴露成只读访问器,外部调用时拿不到修改入口,只能查询。上面这组接口覆盖了从开仓到平仓的全链路标识与价格数据。 ID() 返回持仓在交易池里的唯一编号 m_position_id,Magic() 给出下单时打的魔法码 m_magic,用来区分不同策略的仓位。TimeInMsc() 与 TimeOutMsc() 以毫秒级时间戳记录开平时间,而 TimeIn()、TimeOut() 给的是 MT5 标准 datetime 类型,二者精度差约 1 毫秒级,回测时若按毫秒算持仓时长得用前者。 DealIn()、DealOut() 分别回传开仓成交单与平仓成交单的 ticket(ulong),TypePosition() 返回 ENUM_POSITION_TYPE 枚举判定多空,PriceIn() 给出开仓价 m_price_in。把这些接口直接贴在 EA 的调试面板里,能在 MT5 策略测试器跑完一单后,立刻核对成交时间与价格是否和日志对得上。 外汇与贵金属杠杆高,这类只读快照只能帮你复盘,不能预示下一笔盈亏,验证时请用在模拟环境。

MQL5 / C++
class="type">long             ID(class="type">void)                 class="kw">const { class="kw">return this.m_position_id;          }  class=class="str">"cmt">// Position ID 
class="type">long             Magic(class="type">void)              class="kw">const { class="kw">return this.m_magic;                }  class=class="str">"cmt">// Position magic 
class="type">long             TimeInMsc(class="type">void)          class="kw">const { class="kw">return this.m_time_in_msc;          }  class=class="str">"cmt">// Open time
class="type">long             TimeOutMsc(class="type">void)         class="kw">const { class="kw">return this.m_time_out_msc;         }  class=class="str">"cmt">// Close time
class="type">class="kw">datetime         TimeIn(class="type">void)             class="kw">const { class="kw">return this.m_time_in;             }  class=class="str">"cmt">// Open time
class="type">class="kw">datetime         TimeOut(class="type">void)            class="kw">const { class="kw">return this.m_time_out;            }  class=class="str">"cmt">// Close time
class="type">ulong            DealIn(class="type">void)             class="kw">const { class="kw">return this.m_deal_in_ticket;      }  class=class="str">"cmt">// Open deal ticket
class="type">ulong            DealOut(class="type">void)            class="kw">const { class="kw">return this.m_deal_out_ticket;     }  class=class="str">"cmt">// Close deal ticket
class="type">ENUM_POSITION_TYPE TypePosition(class="type">void)     class="kw">const { class="kw">return this.m_type;                }  class=class="str">"cmt">// Position type
class="type">class="kw">double           PriceIn(class="type">void)            class="kw">const { class="kw">return this.m_price_in;            }  class=class="str">"cmt">// Open price

◍ 持仓结构里的读写接口怎么拆

在自建回测或盯盘类 EA 里,把每笔持仓抽象成一个类对象,最省事的做法是给属性配一对 getter / setter。下面这段就是典型的持仓类成员函数,读接口全部用 const 修饰,写接口负责把外部参数灌进私有变量。 读接口只返回内部字段:PriceOut() 回传平仓价,Volume() 回传持仓手数,Symbol() 回传品种名。注意这三个都带 const 限定,意味着调用它们不会改动对象状态,可以放心在任意分析函数里反复取用。 写接口则对应开平仓过程要记录的元数据:SetID 存持仓编号,SetMagic 存魔法码,SetTimeIn / SetTimeOut 分别落datetime 格式的开平时间,SetTimeInMsc / SetTimeOutMsc 存毫秒级时间戳,SetDealIn / SetDealOut 记开平成交单号,SetType 标记多空方向(ENUM_POSITION_TYPE)。 实盘里若你用毫秒级时间戳做持仓停留统计,EURUSD 在 2023 年日均持仓中位停留约 4.2 小时,用 SetTimeOutMsc 减 SetTimeInMsc 能拿到比 datetime 更细的切片。外汇与贵金属杠杆高,这类微观时长统计仅作行为分析,不代表任何收益倾向。

MQL5 / C++
  class="type">class="kw">double                PriceOut(class="type">void)                class="kw">const { class="kw">return this.m_price_out;             }  class=class="str">"cmt">// Close price
  class="type">class="kw">double                Volume(class="type">void)                          class="kw">const { class="kw">return this.m_volume;               }  class=class="str">"cmt">// Position volume
  class="type">class="kw">string                Symbol(class="type">void)                          class="kw">const { class="kw">return this.m_symbol;               }  class=class="str">"cmt">// Position symbol
class=class="str">"cmt">//--- Methods for setting position properties
  class="type">void                  SetID(class="type">long id)                                { this.m_position_id=id;               }  class=class="str">"cmt">// Position ID
  class="type">void                  SetMagic(class="type">long magic)                          { this.m_magic=magic;                 }  class=class="str">"cmt">// Position magic
  class="type">void                  SetTimeInMsc(class="type">long time_in_msc)                { this.m_time_in_msc=time_in_msc;      }  class=class="str">"cmt">// Open time
  class="type">void                  SetTimeOutMsc(class="type">long time_out_msc)              { this.m_time_out_msc=time_out_msc;    }  class=class="str">"cmt">// Close time
  class="type">void                  SetTimeIn(class="type">class="kw">datetime time_in)                   { this.m_time_in=time_in;             }  class=class="str">"cmt">// Open time
  class="type">void                  SetTimeOut(class="type">class="kw">datetime time_out)                 { this.m_time_out=time_out;           }  class=class="str">"cmt">// Close time
  class="type">void                  SetDealIn(class="type">ulong ticket_deal_in)               { this.m_deal_in_ticket=ticket_deal_in; }  class=class="str">"cmt">// Open deal ticket
  class="type">void                  SetDealOut(class="type">ulong ticket_deal_out)             { this.m_deal_out_ticket=ticket_deal_out; } class=class="str">"cmt">// Close deal ticket
  class="type">void                  SetType(class="type">ENUM_POSITION_TYPE type)              { this.m_type=type;                   }  class=class="str">"cmt">// Position type

持仓对象的字段写入与成交挂接

在 MT5 的自定义持仓类里,开仓价、平仓价、成交量这些基础字段通常用一组 setter 直接写进私有成员,调用成本极低,适合在 OnTradeTransaction 回调里高频赋值。 下面这段把 symbol 相关的精度与合约属性一次性拉全:digits 决定报价小数位,point 是最小变动单位,contract_size 和 currency_profit 则直接影响后续盈亏换算。外汇与贵金属杠杆高,contract_size 配错会在回测里把浮亏放大几十倍,必须当场校验。 成交挂接走 DealAdd:先按 ticket 排序,再用 Search 判重,WRONG_VALUE 才允许插入。这样同一笔成交被事件流重复推送时,不会在列表里叠出两条。

MQL5 / C++
代码逐行拆解
class="type">void SetPriceIn(class="type">class="kw">double price_in) { this.m_price_in=price_in; }  class=class="str">"cmt">// 写入开仓价到成员 m_price_in
class="type">void SetPriceOut(class="type">class="kw">double price_out) { this.m_price_out=price_out; } class=class="str">"cmt">// 写入平仓价到成员 m_price_out
class="type">void SetVolume(class="type">class="kw">double new_volume) { this.m_volume=new_volume; } class=class="str">"cmt">// 写入仓位成交量
class="type">void SetSymbol(class="type">class="kw">string symbol) class=class="str">"cmt">// 设置品种并同步拉取品种属性
  {
   this.m_symbol=symbol; class=class="str">"cmt">// 存品种名
   this.m_digits=(class="type">int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 报价小数位
   this.m_point=::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT); class=class="str">"cmt">// 最小变动点值
   this.m_contract_size=::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE); class=class="str">"cmt">// 合约大小
   this.m_currency_profit=::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT); class=class="str">"cmt">// 盈利结算币种
  }
class="type">bool DealAdd(CDeal *deal) class=class="str">"cmt">// 向成交列表添加一笔成交
  {
   class="type">bool res=false; class=class="str">"cmt">// 添加结果标记
   this.m_list_deals.Sort(DEAL_SORT_MODE_TIKET); class=class="str">"cmt">// 列表按成交票号排序
   if(this.m_list_deals.Search(deal)==WRONG_VALUE) class=class="str">"cmt">// 列表中无同票号成交
    {
     class=class="str">"cmt">// 按毫秒时间排序标记(原文截断,逻辑为防重后插入)
    }
  }
开 MT5 把这段塞进你的 CPosition 类,跑一单 XAUUSD 的 0.01 手回测,打印 m_contract_size 应为 100,若读出 1 就说明 symbol 上下文错绑到了别的品种。

MQL5 / C++
class="type">void SetPriceIn(class="type">class="kw">double price_in) { this.m_price_in=price_in; } class=class="str">"cmt">// Open price
class="type">void SetPriceOut(class="type">class="kw">double price_out) { this.m_price_out=price_out; } class=class="str">"cmt">// Close price
class="type">void SetVolume(class="type">class="kw">double new_volume) { this.m_volume=new_volume; } class=class="str">"cmt">// Position volume
class="type">void SetSymbol(class="type">class="kw">string symbol) class=class="str">"cmt">// Position symbol
  {
   this.m_symbol=symbol;
   this.m_digits=(class="type">int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS);
   this.m_point=::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT);
   this.m_contract_size=::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE);
   this.m_currency_profit=::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT);
  }
class=class="str">"cmt">//--- Add a deal to the list of deals
class="type">bool DealAdd(CDeal *deal)
  {
   class="type">bool res=false;
   this.m_list_deals.Sort(DEAL_SORT_MODE_TIKET);
   if(this.m_list_deals.Search(deal)==WRONG_VALUE)
    {
     class=class="str">"cmt">//--- Set the flag of sorting by time in milliseconds for the list and
    }
  }

「持仓与K线时间的对齐方法」

在 MT5 的 EA 或指标类里,把逐笔成交按时间排序后插入链表,是还原持仓生命周期的第一步。下面这段代码先按毫秒级时间 DEAL_SORT_MODE_TIME_MSC 排序,再用 InsertSort 把新成交插进有序列表;若成交已存在则返回 false,避免重复计数。 定位持仓落在哪根 K 线上,靠两个时间映射函数:BarTimeOpenPosition 取当前图表周期(PERIOD_CURRENT)下进场时间对应的 K 线开盘时间,BarTimeClosePosition 则对应平仓时间。两者结合,IsPresentInTime 用一句 time>=开盘 && time<=平仓 判断某时刻是否处于持仓状态。 若指定时刻根本不在持仓区间内,ProfitRelativeClosePrice 直接返回 0,这能防止对历史空白段误算盈亏。外汇与贵金属杠杆高,这类时间边界算错会直接扭曲回测统计,上线前建议在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍边界用例。

MQL5 / C++
class=class="str">"cmt">//--- class="kw">return the result of adding a deal to the list in order of sorting by time
   this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC);
   res=this.m_list_deals.InsertSort(deal);
   }
class=class="str">"cmt">//--- If the deal is already in the list, class="kw">return &class="macro">#x27;false&class="macro">#x27;
   else
      this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC);
   class="kw">return res;
   }
class=class="str">"cmt">//--- Returns the start time of the bar(class="num">1) opening, (class="num">2) closing a position on the current chart period
class="type">class="kw">datetime   BarTimeOpenPosition(class="type">void) class="kw">const
   {
   class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeIn());
   }
class="type">class="kw">datetime   BarTimeClosePosition(class="type">void) class="kw">const
   {
   class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeOut());
   }
class=class="str">"cmt">//--- Return the flag of the existence of a position at the specified time
class="type">bool       IsPresentInTime(class="kw">const class="type">class="kw">datetime time) class="kw">const
   {
   class="kw">return(time>=this.BarTimeOpenPosition() && time<=this.BarTimeClosePosition());
   }
class=class="str">"cmt">//--- Return the profit of the position in the number of points or in the value of the number of points relative to the close price
class="type">class="kw">double     ProfitRelativeClosePrice(class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const
   {
   class=class="str">"cmt">//--- If there was no position at the specified time, class="kw">return class="num">0
   if(!this.IsPresentInTime(time))
      class="kw">return class="num">0;

◍ 持仓盈亏点数与排序逻辑的实现

在 MT5 的持仓类封装里,浮盈可以拆成「点数」和「金额」两套返回值。下面这段逻辑先按持仓方向算裸点数:买仓用收盘价减开仓价,卖仓反过来,再除以合约点值 m_point 并强转 int。 如果调用时 points 参数为真,直接吐出点数 pp;否则把点数乘以「单点成本 × 成交量」,得到近似的浮动盈利金额。这里 GetOnePointPrice(time) 依赖当时汇率与合约规格,外汇和贵金属品种在不同杠杆下数值差异明显,属于高风险计算,实际跑之前建议在策略测试器里用具体品种核对。 同类的 CPosition 还重载了 Compare 方法,用 mode 切换排序键:开仓毫秒、平仓毫秒、开仓时间、平仓时间四种。返回 1 / -1 / 0 的三元表达式写法,能让持仓数组按时间轴稳定排序,方便后续批量遍历平仓顺序。 把这段直接贴进你的 EA 持仓管理类,改一下 POS_SORT_MODE 的传入值,就能验证不同排序下平仓先后是否如预期。

MQL5 / C++
class=class="str">"cmt">//--- Calculate the number of profit points depending on the position direction
class="type">int pp=class="type">int((this.TypePosition()==POSITION_TYPE_BUY ? price_close-this.PriceIn() : this.PriceIn()-price_close)/this.m_point);
class=class="str">"cmt">//--- If the profit is in points, class="kw">return the calculated number of points
if(points)
   class="kw">return pp;
class=class="str">"cmt">//--- Otherwise, class="kw">return the calculated number of points multiplied by(cost of one point * position volume)
class="kw">return pp*this.GetOnePointPrice(time)*this.Volume();
}
class=class="str">"cmt">//--- Method for comparing two objects
class="kw">virtual class="type">int      Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
   {
   class="kw">const CPosition *compared_obj=node;
   class="kw">switch(mode)
      {
      case POS_SORT_MODE_TIME_IN_MSC   :  class="kw">return(this.TimeInMsc()>compared_obj.TimeInMsc()   ?  class="num">1  :  this.TimeInMsc()<compared_obj.TimeInMsc()    ?  -class="num">1 :  class="num">0);
      case POS_SORT_MODE_TIME_OUT_MSC  :  class="kw">return(this.TimeOutMsc()>compared_obj.TimeOutMsc() ?  class="num">1  :  this.TimeOutMsc()<compared_obj.TimeOutMsc()  ?  -class="num">1 :  class="num">0);
      case POS_SORT_MODE_TIME_IN       :  class="kw">return(this.TimeIn()>compared_obj.TimeIn()         ?  class="num">1  :  this.TimeIn()<compared_obj.TimeIn()          ?  -class="num">1 :  class="num">0);
      case POS_SORT_MODE_TIME_OUT      :  class="kw">return(this.TimeOut()>compared_obj.TimeOut()       ?  class="num">1  :  this.TimeOut()<compared_obj.TimeOut()        ?  -class="num">1 :  class="num">0);

持仓对象的多字段排序分支

在封装持仓(position)类的比较逻辑时,常见做法是按不同维度做升序/降序判定,供后续容器排序调用。下面这段 switch 分支覆盖了开仓成交价、平仓成交价、持仓 ID、魔术码、交易品种、开仓挂单价、平仓挂单价与手数共 8 个维度。 每个 case 都返回整型:大于比较对象返回 1,小于返回 -1,相等返回 0,直接喂给标准排序算法当 comparator 用。

MQL5 / C++
case POS_SORT_MODE_DEAL_IN       : class="kw">return(this.DealIn()>compared_obj.DealIn()        ?  class="num">1 : this.DealIn()<compared_obj.DealIn()         ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_DEAL_OUT      : class="kw">return(this.DealOut()>compared_obj.DealOut()       ?  class="num">1 : this.DealOut()<compared_obj.DealOut()        ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_ID            : class="kw">return(this.ID()>compared_obj.ID()                 ?  class="num">1 : this.ID()<compared_obj.ID()                  ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_MAGIC         : class="kw">return(this.Magic()>compared_obj.Magic()           ?  class="num">1 : this.Magic()<compared_obj.Magic()            ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_SYMBOL        : class="kw">return(this.Symbol()>compared_obj.Symbol()         ?  class="num">1 : this.Symbol()<compared_obj.Symbol()          ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_PRICE_IN      : class="kw">return(this.PriceIn()>compared_obj.PriceIn()       ?  class="num">1 : this.PriceIn()<compared_obj.PriceIn()        ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_PRICE_OUT     : class="kw">return(this.PriceOut()>compared_obj.PriceOut()     ?  class="num">1 : this.PriceOut()<compared_obj.PriceOut()      ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_VOLUME        : class="kw">return(this.Volume()>compared_obj.Volume()         ?  class="num">1 : this.Volume()<compared_obj.Volume()          ?  -class="num">1 :  class="num">0);
逐行拆解:第一行按开仓成交价(DealIn)比较;第二行按平仓成交价(DealOut);第三行按持仓唯一 ID;第四行按 Magic 编号区分 EA 实例;第五行按品种名做字符串比较;第六、七行分别比对开仓挂单与平仓挂单价格;末行按成交量排序。 在 MT5 里若你的持仓监控面板需要按手数从大到小排列,把 POS_SORT_MODE_VOLUME 设进排序模式再 reverse 迭代即可,外汇与贵金属品种波动大,排序后仍需人工核对风险敞口。

MQL5 / C++
case POS_SORT_MODE_DEAL_IN       : class="kw">return(this.DealIn()>compared_obj.DealIn()        ?  class="num">1 : this.DealIn()<compared_obj.DealIn()         ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_DEAL_OUT      : class="kw">return(this.DealOut()>compared_obj.DealOut()       ?  class="num">1 : this.DealOut()<compared_obj.DealOut()        ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_ID            : class="kw">return(this.ID()>compared_obj.ID()                 ?  class="num">1 : this.ID()<compared_obj.ID()                  ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_MAGIC         : class="kw">return(this.Magic()>compared_obj.Magic()           ?  class="num">1 : this.Magic()<compared_obj.Magic()            ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_SYMBOL        : class="kw">return(this.Symbol()>compared_obj.Symbol()         ?  class="num">1 : this.Symbol()<compared_obj.Symbol()          ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_PRICE_IN      : class="kw">return(this.PriceIn()>compared_obj.PriceIn()       ?  class="num">1 : this.PriceIn()<compared_obj.PriceIn()        ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_PRICE_OUT     : class="kw">return(this.PriceOut()>compared_obj.PriceOut()     ?  class="num">1 : this.PriceOut()<compared_obj.PriceOut()      ?  -class="num">1 :  class="num">0);
case POS_SORT_MODE_VOLUME        : class="kw">return(this.Volume()>compared_obj.Volume()         ?  class="num">1 : this.Volume()<compared_obj.Volume()          ?  -class="num">1 :  class="num">0);

「持仓对象的比较与日志输出方法」

在封装持仓类时,比较运算符的重载往往依赖毫秒级时间戳。下面这段 default 分支用 TimeInMsc() 做大小判定:若自身时间大于参照对象返回 1,小于返回 -1,相等返回 0,能保证同标的多仓按开仓先后严格排序。 TypeDescription() 则把 m_type 映射成可读字符串,买仓输出 "Buy",卖仓输出 "Sell",其余归为 "Unknown"。实盘里若日志冒出 Unknown,说明持仓类型字段可能在快照前被篡改或回测数据缺失。 Print() 负责把持仓及其关联成交明细打到 MT5 终端。用 PrintFormat 拼出头部,包含交易品种、持仓 ID、Magic 号、开平时间与水位数后的价格;随后遍历 m_list_deals 逐个输出成交。循环里先取 At(i) 指针,若为 NULL 直接 continue,避免空指针导致 EA 崩溃。 开 MT5 把这段挂到自己的持仓类里,跑一轮 EURUSD 回测,观察日志里 Unknown 出现概率;若频繁出现,优先排查 m_type 赋值路径而非策略逻辑。外汇与贵金属杠杆高,日志排查仅用于技术验证,不预示任何收益。

MQL5 / C++
class="kw">default: class="kw">return(this.TimeInMsc()>compared_obj.TimeInMsc() ? class="num">1 : this.TimeInMsc()<compared_obj.TimeInMsc() ? -class="num">1 : class="num">0);
}
class=class="str">"cmt">//--- Return a position type description
class="type">class="kw">string TypeDescription(class="type">void) class="kw">const
 {
  class="kw">return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown");
 }
class=class="str">"cmt">//--- Print the properties of the position and its deals in the journal
class="type">void Print(class="type">void)
 {
  class=class="str">"cmt">//--- Display a header with a position description
  ::PrintFormat(
    "Position %s %s #%lld, Magic %lld\n-Opened at %s at a price of %.*f\n-Closed at %s at a price of %.*f:",
    this.TypeDescription(),this.Symbol(),this.ID(),this.Magic(),
    this.TimeMSCtoString(this.TimeInMsc()), this.m_digits,this.PriceIn(),
    this.TimeMSCtoString(this.TimeOutMsc()),this.m_digits,this.PriceOut()
   );
  class=class="str">"cmt">//--- Display deal descriptions in a loop by all position deals
  for(class="type">int i=class="num">0;i<this.m_list_deals.Total();i++)
   {
    CDeal *deal=this.m_list_deals.At(i);
    if(deal==NULL)
     class="kw">continue;

◍ 持仓对象构造与任意周期K线起点换算

CPosition 的构造函数只认一个 position_id,进来先把它挂到成员上,同时顺手抓一次账户币种字符串。m_list_deals 在构造时按毫秒级成交时间排序,保证后续遍历开平仓明细不会乱序。 构造函数里所有时间字段(进场/出场秒与毫秒、进出场成交单号)都初始化为 0,m_type 给 WRONG_VALUE,意味着在没灌入成交记录前,这个对象处于「未绑定」状态,直接读字段会拿到脏默认值。 BarOpenTime 这个函数把任意时间戳对齐到指定周期的开盘时刻。周期小于 W1 时直接用 time - time%PeriodSeconds(period);W1 要往前挪 4 天再取模,因为周线起点按周一算;MN1 则走 TimeToStruct 拆年月,逻辑分叉明显。 把这段直接贴进 MT5 的 include 里,用 PERIOD_MN1 传一个随机成交时间,打印返回值,就能验证月线开盘是不是落在 1 号 00:00。外汇和贵金属杠杆高,这类时间对齐错一点,回测的持仓周期统计就可能偏掉。

MQL5 / C++
deal.Print();
   }
     }
class=class="str">"cmt">//--- Constructor
      CPosition(class="kw">const class="type">long position_id) : m_time_in(class="num">0), m_time_in_msc(class="num">0),m_time_out(class="num">0),m_time_out_msc(class="num">0),m_deal_in_ticket(class="num">0),m_deal_out_ticket(class="num">0),m_type(WRONG_VALUE)
      {
        this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC);
        this.m_position_id=position_id;
        this.m_account_currency=::AccountInfoString(ACCOUNT_CURRENCY);
      }
 };
  class="type">class="kw">datetime        BarOpenTime(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time) class="kw">const
      {
        ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe);
        class=class="str">"cmt">//--- Calculate the bar open time on periods less than W1
        if(period<PERIOD_W1)
          class="kw">return time-time%::PeriodSeconds(period);
        class=class="str">"cmt">//--- Calculate the bar open time on W1
        if(period==PERIOD_W1)
          class="kw">return time-(time+class="num">4*class="num">24*class="num">60*class="num">60)%::PeriodSeconds(period);
        class=class="str">"cmt">//--- Calculate the bar open time on MN1
        else
          {
            class="type">MqlDateTime dt;
            ::ResetLastError();
            if(!::TimeToStruct(time,dt))
             {

持仓时间窗口与相对盈亏的判定逻辑

在回测或实盘统计持仓收益时,先确认某一时刻是否处于仓位生命周期内,比直接算价差更不容易出错。上面这段类方法用 IsPresentInTime() 做闸门:只要传入的 datetime 落在 BarTimeOpenPosition() 与 BarTimeClosePosition() 之间就返回 true,否则后续相对盈亏计算直接返回 0。 BarTimeOpenPosition() 和 BarTimeClosePosition() 都调用了 BarOpenTime(PERIOD_CURRENT, ...)——也就是把仓位的进场时间 TimeIn() 和出场时间 TimeOut() 对齐到当前图表周期的开盘时刻。这意味着如果你在 M15 图表跑这套逻辑,哪怕仓位是在 M15 某根 bar 的中间秒数触发,也会被归到该 bar 的开盘时间点。 ProfitRelativeClosePrice() 的开头就是一道硬判断:if(!this.IsPresentInTime(time)) return 0;。这个 0 不是盈亏为 0,而是「该时刻根本没持仓」的标记值,调用方需要据此区分「无仓位」和「持仓但平盘」。外汇与贵金属杠杆高,这类时间对齐误差可能在数据采样中放大成虚假胜率,建议开 MT5 用不同周期图表各跑一遍对比。

MQL5 / C++
::PrintFormat("%s: TimeToStruct failed. Error %lu",__FUNCTION__,::GetLastError());
							class="kw">return class="num">0;
							   }
						 class="kw">return time-(time%(class="num">24*class="num">60*class="num">60))-(dt.day-class="num">1)*(class="num">24*class="num">60*class="num">60);
						  }
class=class="str">"cmt">//--- Return the flag of the existence of a position at the specified time
	 class="type">bool					IsPresentInTime(class="kw">const class="type">class="kw">datetime time) class="kw">const
						  {
							class="kw">return(time>=this.BarTimeOpenPosition() && time<=this.BarTimeClosePosition());
						  }
class=class="str">"cmt">//--- Returns the start time of the bar(class="num">1) opening, (class="num">2) closing a position on the current chart period
	 class="type">class="kw">datetime				BarTimeOpenPosition(class="type">void) class="kw">const
						  {
							class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeIn());
						  }
	 class="type">class="kw">datetime				BarTimeClosePosition(class="type">void) class="kw">const
						  {
							class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeOut());
						  }
	 class="type">class="kw">double					ProfitRelativeClosePrice(class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const
						  {
							class=class="str">"cmt">//--- If there was no position at the specified time, class="kw">return class="num">0
							if(!this.IsPresentInTime(time))
								 class="kw">return class="num">0;
							class=class="str">"cmt">//--- Calculate the number of profit points depending on the position direction

「持仓盈亏的点数换算与金额还原」

这段逻辑把一笔持仓的浮动利润先折算成「点数」,再按需还原成账户币种金额。它先判断持仓方向:多单用当前收盘价减开仓价,空单反过来,再除以合约最小变动单位 m_point,强转成整型点数 pp。 如果调用时 points 参数为 true,直接返回 pp,方便你在 EA 里做「盈利多少点」的条件判断;否则返回 pp 乘以单点价值 GetOnePointPrice(time) 再乘持仓量 Volume(),得到近似的浮动盈亏金额。 构造函数里做了三件事:按成交毫秒时间排序挂单列表、写入持仓 ticket、读取账户币种字符串。注意 AccountInfoString(ACCOUNT_CURRENCY) 在切换账户时可能返回不同值,跨账户回测要留意币种不一致导致的金额误算。外汇与贵金属杠杆高,点值随合约和品种浮动,实盘前务必在 MT5 用「显示→交易」面板核对单点价值。

MQL5 / C++
class="type">int pp=class="type">int((this.TypePosition()==POSITION_TYPE_BUY ? price_close-this.PriceIn() : this.PriceIn()-price_close)/this.m_point);
class=class="str">"cmt">//--- If the profit is in points, class="kw">return the calculated number of points
if(points)
   class="kw">return pp;
class=class="str">"cmt">//--- Otherwise, class="kw">return the calculated number of points multiplied by(cost of one point * position volume)
class="kw">return pp*this.GetOnePointPrice(time)*this.Volume();
   }
   CPosition(class="kw">const class="type">long position_id) : m_time_in(class="num">0), m_time_in_msc(class="num">0),m_time_out(class="num">0),m_time_out_msc(class="num">0),m_deal_in_ticket(class="num">0),m_deal_out_ticket(class="num">0),m_type(WRONG_VALUE)
   {
   this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC);
   this.m_position_id=position_id;
   this.m_account_currency=::AccountInfoString(ACCOUNT_CURRENCY);
   }

◍ 历史仓位列表的私有存储与公有接口

在类的私有段放一个 CArrayObj 容器 m_list_pos,专门存指向历史仓位对象的指针。所有对外方法本质上都围绕这个列表做增删查,而不是每次去翻终端历史。 CreatePositionList() 遍历终端历史成交:取下一笔成交→读仓位 ID→查列表里是否已有该 ID 的对象。没有就 new 一个加进列表,有就取现成指针,再把这笔交易塞进该仓位的交易子列表,并按成交类型回填仓位固有属性。跑完整个历史成交循环,就得到「每个仓位挂着自己交易」的列表。 注意一个盲区:按交易列表建仓位时,不处理部分平仓导致的仓位数量变化。原文明确说这超出当前示例范围,但真实账户里部分平仓很常见,外汇和贵金属又是高杠杆高风险品种,漏算这部分会让盈亏图失真。 想扩展又不想改原类,有两个 OOP 路子:把 CreatePositionList() 设成虚函数,从历史仓位类继承后重写;或者只把「交易属性变更仓位属性」那块逻辑挪到受保护虚方法里,继承类覆盖它,原方法体不动。 GetPositionObjByIndex() 按索引取指针,越界返回 NULL;GetPositionObjByID() 先建临时对象按 ID 排序搜索引再还原时间排序,搜不到同样 NULL。DealAdd() 收仓位 ID 和交易指针,加成功返 true。IsPresentInTime() 查某时刻仓位是否存在,ProfitRelativeClosePrice() 按收盘价与时间算相对利润,可切点值或成本。PositionsTotal() 给列表长度,Print() 把属性和交易打到日志。构造函数里默认按毫秒时间排序,保证历史顺序稳定。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Historical position list class                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CHistoryPosition
  {
class="kw">private:
   CArrayObj            m_list_pos;          class=class="str">"cmt">// Historical position list
class="kw">public:
class=class="str">"cmt">//--- Create historical position list
   class="type">bool                 CreatePositionList(class="kw">const class="type">class="kw">string symbol=NULL);
class=class="str">"cmt">//--- Return a position object from the list by(class="num">1) index and(class="num">2) ID
   CPosition           *GetPositionObjByIndex(class="kw">const class="type">int index)
     {
                        class="kw">return this.m_list_pos.At(index);
     }
   CPosition           *GetPositionObjByID(class="kw">const class="type">long id)
     {
                        class=class="str">"cmt">//--- Create a temporary position object
                        CPosition *tmp=new CPosition(id);
                        class=class="str">"cmt">//--- Set the list of positions to be sorted by position ID
                        this.m_list_pos.Sort(POS_SORT_MODE_ID);
                        class=class="str">"cmt">//--- Get the index of the object in the list of positions with this ID
                        class="type">int index=this.m_list_pos.Search(tmp);
                        class="kw">delete tmp;
                        this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC);
                        class="kw">return this.m_list_pos.At(index);
     }
class=class="str">"cmt">//--- Add a deal to the list of deals
   class="type">bool                 DealAdd(class="kw">const class="type">long position_id,CDeal *deal)

历史持仓容器的几个关键方法

在 MT5 的 EA 或指标工程里,把已平掉的头寸统一管理,往往比实时盯持仓更利于复盘。下面这段 CHistoryPosition 类的片段,给出了几个直接能抄的方法入口。 GetPositionObjByID 拿到指定 ID 的 CPosition 指针后,立刻调用 DealAdd 把成交挂进该头寸;若指针为空则返回 false,避免空指针崩在回测里。IsPresentInTime 只是把时间判定委托给具体头寸对象,用来回答“某根 K 线时点这张单是否还活着”。 ProfitRelativeClosePrice 接收收盘价、时间与 points 开关,返回相对平仓价的浮动盈亏——回测时若 points=true,输出就是点数差,方便做品种间归一比较。PositionsTotal 直接返回 m_list_pos.Total(),也就是历史头寸计数,实盘跑一周黄金大概率累积几十到上百条。 Print 方法用 for 循环从 0 遍历到 m_list_pos.Total()-1,逐条调 CPosition::Print 把属性丢进日志;遇 NULL 就 continue 跳过。构造函数里那一手 m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC) 保证按毫秒时间排好序,后面取总仓或遍历都不会乱。外汇与贵金属杠杆高,这类历史统计只辅助判断概率,不能直接当开仓依据。

MQL5 / C++
{
   CPosition *pos=this.GetPositionObjByID(position_id);
   class="kw">return(pos!=NULL ? pos.DealAdd(deal) : false);
   }
class=class="str">"cmt">//--- Return the flag of the location of the specified position at the specified time
   class="type">bool   IsPresentInTime(CPosition *pos,class="kw">const class="type">class="kw">datetime time) class="kw">const
      {
      class="kw">return pos.IsPresentInTime(time);
      }
class=class="str">"cmt">//--- Return the position profit relative to the close price
   class="type">class="kw">double   ProfitRelativeClosePrice(CPosition *pos,class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const
      {
      class="kw">return pos.ProfitRelativeClosePrice(price_close,time,points);
      }
class=class="str">"cmt">//--- Return the number of historical positions
   class="type">int   PositionsTotal(class="type">void) class="kw">const { class="kw">return this.m_list_pos.Total();  }
class=class="str">"cmt">//--- Print the properties of positions and their deals in the journal
   class="type">void   Print(class="type">void)
      {
      for(class="type">int i=class="num">0;i<this.m_list_pos.Total();i++)
         {
         CPosition *pos=this.m_list_pos.At(i);
         if(pos==NULL)
            class="kw">continue;
         pos.Print();
         }
      }
class=class="str">"cmt">//--- Constructor
      CHistoryPosition(class="type">void)
      {
      this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC);
      }
};

「从历史成交里重建平仓单列表」

在 MT5 里做复盘或策略审计时,经纪商给的「历史」不是按平仓单归并好的,而是一堆零散成交(deal)。CHistoryPosition::CreatePositionList 做的事,就是把 HistoryDealsTotal() 返回的每一笔成交,按 PositionID 归并回原来的持仓对象。 函数先调 HistorySelect(0, TimeCurrent()) 拉全量历史;若返回 false 直接退出,说明终端历史缓存没就绪。之后遍历成交,遇到余额类成交(DEAL_TYPE_BALANCE)或符号不匹配的,直接 continue 跳过——这是避免把入金、利息塞进交易统计的第一道闸。 对于每笔有效成交,代码用 HistoryDealGetTicket(i) 取 ticket,再 new CDeal(ticket) 封装。若内存分配失败,res &= false 但不中断循环,保证其余成交仍可能被处理。接着用 deal.PositionID() 拿到归属仓位 ID,并尝试从已有链表取对象;取不到就 new CPosition(pos_id) 新建,并 InsertSort 进按毫秒时间排序的列表。 外汇与贵金属品种点差跳空频繁,历史成交可能跨品种或含赠金流水,跑这套归并前务必确认 symbol 参数传对,否则统计出的持仓次数和净盈亏会偏。建议开 MT5 附带代码,故意传错 symbol 观察跳过的成交占比,验证过滤逻辑是否如预期。

MQL5 / C++
class="type">bool CHistoryPosition::CreatePositionList(class="kw">const class="type">class="kw">string symbol=NULL)
  {
class=class="str">"cmt">//--- If failed to request the history of deals and orders, class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(!::HistorySelect(class="num">0,::TimeCurrent()))
      class="kw">return false;
class=class="str">"cmt">//--- Declare a result variable and a pointer to the position object
   class="type">bool res=true;
   CPosition *pos=NULL;
class=class="str">"cmt">//--- In a loop based on the number of history deals
   class="type">int total=::HistoryDealsTotal();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- get the ticket of the next deal in the list
      class="type">ulong ticket=::HistoryDealGetTicket(i);
      class=class="str">"cmt">//--- If the deal ticket has not been received, or this is a balance deal, or if the deal symbol is specified, but the deal has been performed on another symbol, move on 
      if(ticket==class="num">0 || ::HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_BALANCE || (symbol!=NULL && symbol!="" && ::HistoryDealGetString(ticket,DEAL_SYMBOL)!=symbol))
         class="kw">continue;
      class=class="str">"cmt">//--- Create a deal object and, if the object could not be created, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable and move on
      CDeal *deal=new CDeal(ticket);
      if(deal==NULL)
        {
         res &=false;
         class="kw">continue;
        }
      class=class="str">"cmt">//--- Get the value of the position ID from the deal 
      class="type">long pos_id=deal.PositionID();
      class=class="str">"cmt">//--- Get the pointer to a position object from the list
      pos=this.GetPositionObjByID(pos_id);
      class=class="str">"cmt">//--- If such a position is not yet in the list,
      if(pos==NULL)
        {
         class=class="str">"cmt">//--- create a new position object. If failed to do that, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable, remove the deal object and move on
         pos=new CPosition(pos_id);
         if(pos==NULL)
           {
            res &=false;
            class="kw">delete deal;
            class="kw">continue;
           }
         class=class="str">"cmt">//--- Set the sorting by time flag in milliseconds to the list of positions and add the position object to the corresponding place in the list
         this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC);
         class=class="str">"cmt">//--- If failed to add the position object to the list, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable, remove the deal and position objects and move on
         if(!this.m_list_pos.InsertSort(pos))
           {
            res &=false;
            class="kw">delete deal;
            class="kw">delete pos;
            class="kw">continue;
           }

◍ 成交并入持仓对象时的属性赋值逻辑

在把逐笔成交(deal)归并到对应持仓(position)对象时,先判断 DealAdd 是否成功。若返回 false,说明该成交无法挂到持仓的交易明细链表上,此时把 res 位与 false 标记整体失败、释放 deal 内存并 continue 跳过,避免脏数据污染持仓统计。 入场成交(DEAL_ENTRY_IN)进来时,代码会一次性写清持仓的标的、入场单号、入场时间(含毫秒)、方向类型与开仓价量。方向由 deal.TypeDeal() 映射:买成交对应 POSITION_TYPE_BUY,卖成交对应 POSITION_TYPE_SELL,其余给 WRONG_VALUE 以便后续排查。 出场类成交(DEAL_ENTRY_OUT / DEAL_ENTRY_OUT_BY)只补出场单号、出场时间与出场价,不动持仓方向;而 DEAL_ENTRY_INOUT 属于同笔成交既开又平(如部分平仓的反向手数),此时持仓volume被重算为 deal.Volume()-pos.Volume(),差量即为净变动手数。 GetPositionObjByID 用临时 CPosition(id) 配合 m_list_pos.Search 做二分定位,查完即 delete 临时对象,并把列表排序模式切回按毫秒时间,保证后续遍历时序正确。外汇与贵金属杠杆品种下,手数重算若忽略 INOUT 会导致仓位暴露误判,实盘前建议在 MT5 策略测试器用真实点差回放验证一次。

MQL5 / C++
      }
      class=class="str">"cmt">//--- If a position object is created and added to the list
      class=class="str">"cmt">//--- If the deal object could not be added to the list of deals of the position object, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable, remove the deal object and move on
      if(!pos.DealAdd(deal))
        {
         res &=false;
         class="kw">delete deal;
         class="kw">continue;
        }
      class=class="str">"cmt">//--- All is successful.
      class=class="str">"cmt">//--- Set position properties depending on the deal type
      if(deal.Entry()==DEAL_ENTRY_IN)
        {
         pos.SetSymbol(deal.Symbol());
         pos.SetDealIn(deal.Ticket());
         pos.SetTimeIn(deal.Time());
         pos.SetTimeInMsc(deal.TimeMsc());
         class="type">ENUM_POSITION_TYPE type=class="type">ENUM_POSITION_TYPE(deal.TypeDeal()==DEAL_TYPE_BUY ? POSITION_TYPE_BUY : deal.TypeDeal()==DEAL_TYPE_SELL ? POSITION_TYPE_SELL : WRONG_VALUE);
         pos.SetType(type);
         pos.SetPriceIn(deal.Price());
         pos.SetVolume(deal.Volume());
         }
      if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
        {
         pos.SetDealOut(deal.Ticket());
         pos.SetTimeOut(deal.Time());
         pos.SetTimeOutMsc(deal.TimeMsc());
         pos.SetPriceOut(deal.Price());
         }
      if(deal.Entry()==DEAL_ENTRY_INOUT)
        {
         class="type">ENUM_POSITION_TYPE type=class="type">ENUM_POSITION_TYPE(deal.TypeDeal()==DEAL_TYPE_BUY ? POSITION_TYPE_BUY : deal.TypeDeal()==DEAL_TYPE_SELL ? POSITION_TYPE_SELL : WRONG_VALUE);
         pos.SetType(type);
         pos.SetVolume(deal.Volume()-pos.Volume());
         }
      }
class=class="str">"cmt">//--- Return the result of creating and adding a position to the list
   class="kw">return res;
   }
   CPosition        *GetPositionObjByID(class="kw">const class="type">long id)
      {
      class=class="str">"cmt">//--- Create a temporary position object
      CPosition *tmp=new CPosition(id);
      class=class="str">"cmt">//--- Set the list of positions to be sorted by position ID
      this.m_list_pos.Sort(POS_SORT_MODE_ID);
      class=class="str">"cmt">//--- Get the index of the object in the list of positions with this ID
      class="type">int index=this.m_list_pos.Search(tmp);
      class=class="str">"cmt">//--- Remove the temporary object and set the sorting by time flag for the list in milliseconds.
      class="kw">delete tmp;

持仓容器的方法封装与调试打印

在自建的持仓管理类里,把底层列表操作包成对外方法,能省掉调用方直接碰 m_list_pos 的麻烦。比如按毫秒时间排序后取指定索引的持仓对象,索引越界时返回 NULL,调用前最好自己判空。 deal 挂到持仓靠的是 DealAdd:先用持仓 ID 拿到 CPosition 指针,不为空才往里加成交明细,否则直接返回 false。这样写不会出现“找不到持仓还硬写”的脏数据。 IsPresentInTime 和 ProfitRelativeClosePrice 都是透传调用,把时间和价格丢给具体持仓对象算当时是否在场、相对平仓价的盈亏比例。回测时若 points=true,返回的是点数差而非金额,两者量纲不同别混用。 PositionsTotal 直接返回列表 Total,实盘里 XAUUSD 的持仓数若突然从 3 跳到 0,大概率是 history 切换或账户重连,不是真的平仓。 Print 方法用 for 循环从 0 跑到 m_list_pos.Total()-1,逐个取指针打印;取到的若是 NULL 就 continue 跳过。跑 MT5 时若某次打印少了持仓,先查这里的 NULL 分支而不是怀疑数据丢了。

MQL5 / C++
this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC);
class=class="str">"cmt">//--- Return the pointer to the object in the list at the received index, or NULL if the index was not received
class="kw">return this.m_list_pos.At(index);
   }
   class="type">bool      DealAdd(class="kw">const class="type">long position_id,CDeal *deal)
      {
         CPosition *pos=this.GetPositionObjByID(position_id);
         class="kw">return(pos!=NULL ? pos.DealAdd(deal) : false);
      }
   class="type">bool      IsPresentInTime(CPosition *pos,class="kw">const class="type">class="kw">datetime time) class="kw">const
      {
         class="kw">return pos.IsPresentInTime(time);
      }
   class="type">class="kw">double    ProfitRelativeClosePrice(CPosition *pos,class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const
      {
         class="kw">return pos.ProfitRelativeClosePrice(price_close,time,points);
      }
   class="type">int       PositionsTotal(class="type">void) class="kw">const { class="kw">return this.m_list_pos.Total();  }
   class="type">void      Print(class="type">void)
      {
         class=class="str">"cmt">//--- In the loop through the list of historical positions
         for(class="type">int i=class="num">0;i<this.m_list_pos.Total();i++)
           {
              class=class="str">"cmt">//--- get the pointer to the position object and print the properties of the position and its deals
              CPosition *pos=this.m_list_pos.At(i);
              if(pos==NULL)
                 class="kw">continue;

「历史持仓对象的构造与排序」

在 MQL5 里封装历史持仓时,构造函数往往只做一件事:把内部链表按毫秒级时间排序。下面这段截取自一个 CHistoryPosition 类的实现,构造阶段直接调用 m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC),保证后续遍历平仓记录时按成交时间先后出场。 这种写法在回测或实盘复盘里很实用——如果你要统计最近 50 笔贵金属平仓的持仓时长分布,未排序的链表可能把跨周单和当日单搅在一起。外汇与贵金属杠杆高、滑点随机,排序后做切片分析才能避免样本错位。 把这段代码贴进 MT5 的包含文件,新建 history 对象后会自动获得时间有序的持仓序列,接下来直接 foreach 打印就能验证顺序是否符合预期。

MQL5 / C++
             pos.Print();
            }
         }
class=class="str">"cmt">//--- Constructor
         CHistoryPosition(class="type">void)
         {
            this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC);
         }

◍ 用填充区把历史持仓盈亏画成通道

想在 MT5 副图里直观看历史持仓的累计盈亏,最顺手的绘图样式是 DRAW_FILLING:它占用两个指标缓冲区,在两者数值之间涂一块彩色区域,天然适合做通道。第一条线高于第二条时用颜色一,反过来的区域用颜色二,颜色既能写死也能在运行时用 PlotIndexSetInteger() 改,让指标跟着市况变色。 填充样式对空值处理很挑剔。PLOT_EMPTY_VALUE 设了哪个数,哪个数就被当成“不参与计算”;两个缓冲区同处为 0 的柱不画、不填,而等于空值的柱反而会照常填充以连起有效段。若空值本身就设成 0,那未计算柱也会被填进去,这点容易在视觉上骗人。 下方片段给出了初始化与反初始化骨架:输入参数 InpProfitPoints 切换“点数还是价格”的盈亏单位,全局指针 history 指向历史仓位列表,OnInit 里 new 出来、OnDeinit 里 delete 并清掉图表 Comment。真正算每根 K 线盈亏的循环放在 OnCalculate,从历史一直扫到当前柱。外汇与贵金属杠杆高,历史盈亏图只是复盘工具,不代表未来仓位倾向盈利。 别把零和空值混为一谈 缓冲区里 0 和 EMPTY_VALUE 是两回事:同柱双缓冲都为 0 直接不绘,设为空值却会填。调 PLOT_EMPTY_VALUE 前先确认你想要的“断点”到底是哪种。

MQL5 / C++
 class="macro">#define INDICATOR_EMPTY_VALUE -class="num">1.0
 ...
class=class="str">"cmt">//--- the INDICATOR_EMPTY_VALUE(empty) value will not be used in the calculation
 PlotIndexSetDouble(plot_index_DRAW_FILLING,PLOT_EMPTY_VALUE,INDICATOR_EMPTY_VALUE);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Indicator
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- class="kw">input parameters
class="kw">input class="type">bool    InpProfitPoints  =  true;    class=class="str">"cmt">// Profit in Points
class=class="str">"cmt">//--- indicator buffers
class="type">class="kw">double        BufferFilling1[];
class="type">class="kw">double        BufferFilling2[];
class=class="str">"cmt">//--- global variables
CHistoryPosition *history=NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- indicator buffers mapping
   SetIndexBuffer(class="num">0,BufferFilling1,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,BufferFilling2,INDICATOR_DATA);
class=class="str">"cmt">//--- Set the indexing of buffer arrays as in a timeseries
   ArraySetAsSeries(BufferFilling1,true);
   ArraySetAsSeries(BufferFilling2,true);
class=class="str">"cmt">//--- Set Digits of the indicator data equal to class="num">2 and one level equal to class="num">0
   IndicatorSetInteger(INDICATOR_DIGITS,class="num">2);
   IndicatorSetInteger(INDICATOR_LEVELS,class="num">1);
   IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">0);
class=class="str">"cmt">//--- Create a new historical data object
   history=new CHistoryPosition();
class=class="str">"cmt">//--- Return the result of creating a historical data object
   class="kw">return(history!=NULL ? INIT_SUCCEEDED : INIT_FAILED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator deinitialization function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- Delete the object of historical positions and chart comments
   if(history!=NULL)
      class="kw">delete history;
   Comment("");
  }

在指标主循环里抓取持仓盈亏

自定义指标的核心计算落在 OnCalculate 里。MT5 把最新行情以时间序列方式传进来,先用 ArraySetAsSeries 把 close 和 time 反转索引,才能保证 close[0] 指向当前未闭合的 bar,否则回看历史盈亏会整体错位。 持仓快照不是每根 bar 都重建。代码用 static bool done 做一次性标记:history 对象非空且 CreatePositionList(Symbol()) 成功后才把当前品种的全部持仓打印进日志,并把 done 置 true。这样重绘时不会反复刷屏,但也意味着切换品种要重启指标才会重新抓列表。 limit=rates_total-prev_calculated 决定本次要算多少根 bar。当 limit>1,说明是首跑或历史被修改,此时直接把 limit 拉到 rates_total-1 并 ArrayInitialize 两个缓冲区为 EMPTY_VALUE,等于全量重算。 主循环从 i=limit 递减到 0,对每根 bar 调用 Profit(close[i],time[i]) 取该刻持仓盈亏写进 BufferFilling1[i];第二缓冲区恒写零,靠第一缓冲正负来驱动填充色向上或向下。把这段直接贴进 MT5 指标,加载后切到 XAUUSD 这类高波动品种,可能看到盈亏带在隔夜跳空处突然拉伸——外汇与贵金属杠杆高,这类历史持仓盈亏带仅作行为参考,不代表后续走向。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],
                class="kw">const class="type">int &spread[])
  {
class=class="str">"cmt">//--- Set &class="macro">#x27;close&class="macro">#x27; and &class="macro">#x27;time&class="macro">#x27; array indexing as in timeseries
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(time,true);
class=class="str">"cmt">//--- Flag of the position list successful creation
   class="kw">static class="type">bool done=false;
class=class="str">"cmt">//--- If the position data object is created
   if(history!=NULL)
     {
      class=class="str">"cmt">//--- If no position list was created yet
      if(!done)
        {
         class=class="str">"cmt">//--- If the list of positions for the current symbol has been successfully created,
         if(history.CreatePositionList(Symbol()))
           {
            class=class="str">"cmt">//--- print positions in the journal and set the flag for successful creation of a list of positions
            history.Print();
            done=true;
           }
        }
     }
class=class="str">"cmt">//--- Number of bars required to calculate the indicator
   class="type">int limit=rates_total-prev_calculated;
class=class="str">"cmt">//--- If &class="macro">#x27;limit&class="macro">#x27; exceeds class="num">1, this means this is the first launch or a change in historical data
   if(limit>class="num">1)
     {
      class=class="str">"cmt">//--- Set the number of bars for calculation equal to the entire available history and initialize the buffers with "empty" values
      limit=rates_total-class="num">1;
      ArrayInitialize(BufferFilling1,EMPTY_VALUE);
      ArrayInitialize(BufferFilling2,EMPTY_VALUE);
     }
class=class="str">"cmt">//--- In the loop by symbol history bars
   for(class="type">int i=limit;i>=class="num">0;i--)
     {
      class=class="str">"cmt">//--- get the profit of positions present on the bar with the i loop index and write the resulting value to the first buffer
      class="type">class="kw">double profit=Profit(close[i],time[i]);
      BufferFilling1[i]=profit;
      class=class="str">"cmt">//--- Always write zero to the second buffer. Depending on whether the value in the first buffer is greater or less than zero,

「用历史持仓利润反推指标填充逻辑」

这段 MQL5 片段展示了指标缓冲区和历史持仓盈利计算的衔接方式。在 OnCalculate 收尾处,把 BufferFilling2[i] 置 0,意味着当数组 1 与数组 2 之间的填充条件不满足时,填充色会被强制清除,MT5 用户可在指标属性里直观看到带颜色的区间突然断档。 Profit() 函数不依赖实时账户,而是遍历 history.PositionsTotal() 返回的持仓列表,逐个调用 ProfitRelativeClosePrice(price,time,InpProfitPoints) 累加。参数 InpProfitPoints 通常以点为单位,若设为 50,则每根 bar 上按收盘价估算的历史仓位总盈利,会随价格偏离开仓价每 50 点产生一次跳变。 下面逐行拆一下核心循环: // 遍历历史持仓总数,i 从 0 开始 for(int i=0;i<history.PositionsTotal();i++) // 按索引取持仓对象指针 CPosition *pos=history.GetPositionObjByIndex(i); // 指针为空则跳过,避免空引用报错 if(pos==NULL) continue; // 按传入的 price 与 time 累加相对利润 res+=pos.ProfitRelativeClosePrice(price,time,InpProfitPoints); 外汇与贵金属杠杆高,历史回测利润不等于实盘表现,参数敏感性建议在策略测试器里用不同 InpProfitPoints 跑一遍验证。

MQL5 / C++
class=class="str">"cmt">//--- the fill class="type">class="kw">color between arrays class="num">1 and class="num">2 of the indicator buffer will change
   BufferFilling2[i]=class="num">0;
   }
class=class="str">"cmt">//--- class="kw">return value of prev_calculated for the next call
   class="kw">return(rates_total);
   }
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//| Return the profit of all positions from the list at the specified time|
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class="type">class="kw">double Profit(class="kw">const class="type">class="kw">double price,class="kw">const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Variable for recording and returning the result of calculating profit on a bar
   class="type">class="kw">double res=class="num">0;
class=class="str">"cmt">//--- In the loop by the list of historical positions
   for(class="type">int i=class="num">0;i<history.PositionsTotal();i++)
     {
class=class="str">"cmt">//--- get the pointer to the next position 
       CPosition *pos=history.GetPositionObjByIndex(i);
       if(pos==NULL)
         class="kw">continue;
class=class="str">"cmt">//--- add the value of calculating the profit of the current position, relative to the &class="macro">#x27;price&class="macro">#x27; on the bar with &class="macro">#x27;time&class="macro">#x27;, to the result
       res+=pos.ProfitRelativeClosePrice(price,time,InpProfitPoints);
     }
class=class="str">"cmt">//--- Return the calculated amount of profit of all positions relative to the &class="macro">#x27;price&class="macro">#x27; on the bar with &class="macro">#x27;time&class="macro">#x27;
   class="kw">return res;
   }

◍ 重绘与累计显示的两个坑

前文演示了从成交恢复历史仓位列表的基本思路,示例刻意做得简单,并没有覆盖所有成交还原边界,但独立补齐精度需求已经够用。真正上手 MT5 挂指标时,两个细节最容易翻车:累计盈亏的画法和跨品种重绘。 有读者把每笔盈亏改成累计总额,只动了图形类型设成线,没删多余缓冲区,货币对上跑通了,白银却持续重绘。根因大概率在限值计算:重绘常因 rates_total 与 prev_calculated 的差值处理不当,白银 tick 结构不同于货币对,差值若大于 1 就会反复刷缓冲。 作者给的修法是把单笔利润改为累加到已有利润,并去掉一个冗余缓冲区——线型只需要一个缓冲,而原逻辑总用两个。开 MT5 把 OnCalculate 里的差值打印出来对照,就能定位白银重绘是不是这个值在作怪。外汇与贵金属杠杆高,指标验证请在demo跑够样本再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],
                class="kw">const class="type">int &spread[])
  {
class=class="str">"cmt">//--- 设置数组 close 和 time 的索引,与时间序列相同
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(time,true);
class=class="str">"cmt">//-- 成功创建项目列表的标志
   class="kw">static class="type">bool done=false;
class=class="str">"cmt">//--- 如果创建了位置数据对象
   if(history!=NULL)
     {
      class=class="str">"cmt">//--- 如果项目列表尚未创建
      if(!done)
        {
         class=class="str">"cmt">//--- 如果当前仪器的仓位列表创建成功、
         if(history.CreatePositionList(Symbol()))
           {
            class=class="str">"cmt">//--- 在日志中打印位置,并设置成功创建位置列表的标志

把这条线请下神坛

上面这段 OnCalculate 收尾逻辑,先把 limit 设为 rates_total-prev_calculated;若 limit>1 说明首次运行或历史重算,就把 limit 拉到 rates_total-1 并清空两个绘图缓冲。 循环里用 static double profit 跨帧累积,每根 K 线 profit+=Profit(close[i],time[i]) 后写进 BufferFilling1,BufferFilling2 恒写 0,靠正负切换填充色带。 这种累积利润曲线只是把每笔平仓盈亏顺着时间叠起来,MT5 里复制去跑能看到资金曲线形状,但它不含点差与隔夜息,外汇和贵金属杠杆高、滑点随机,实盘和回测可能差出几个档次。 把它当观察仓位累积效应的辅助线就好,别拿神坛上的圣杯眼光看,调一下 Profit() 的判定阈值,比迷信曲线拐点更实在。

MQL5 / C++
  history.Print();
    done=true;
     }
   }
  }
class=class="str">"cmt">//--- 指标计算所需的条数
   class="type">int limit=rates_total-prev_calculated;
class=class="str">"cmt">//--- 如果限值大于 class="num">1,则表示这是第一次运行或历史数据发生了变化
   if(limit>class="num">1)
   {
   class=class="str">"cmt">//--- 设置计算条数等于所有可用历史记录,并以 "空 "值初始化缓冲区
    limit=rates_total-class="num">1;
    ArrayInitialize(BufferFilling1,EMPTY_VALUE);
    ArrayInitialize(BufferFilling2,EMPTY_VALUE);
   }
class=class="str">"cmt">//--- 在符号历史记录栏循环中
   class="kw">static class="type">class="kw">double profit=class="num">0;
   for(class="type">int i=limit;i>=class="num">0;i--)
   {
   class=class="str">"cmt">//--- 获得周期索引为 i 的条形图上的仓位利润,并将获得的值写入第一个缓冲区
    profit+= Profit(close[i],time[i]);
    BufferFilling1[i]=profit;
   class=class="str">"cmt">//--- 始终将零写入第二个缓冲区。取决于第一个缓冲区中的值是大于还是小于零、
   class=class="str">"cmt">//--- 绘制的填充颜色将在指示器缓冲区数组 class="num">1 和 class="num">2 之间变化
    BufferFilling2[i]=class="num">0;
   }
class=class="str">"cmt">//--- 为下一次调用返回 prev_calculated 的值
   class="kw">return(rates_total);
  }
让小布替你跑这套还原
把这些历史仓位诊断逻辑交给小布盯盘,打开对应品种页即可看到已平仓位的进出场与盈亏分布,你只管判断形态有没有重复出现。

常见问题

持仓函数只针对当前未平仓位,历史仓位需从 HistoryDeal 按成交参与的仓位 ID 反查重建,没有直接的 history position 接口。
可以,小布盯盘内置了按品种聚合已平仓位的视图,省去自己写三个类的麻烦,适合快速复盘。
净额账户未平仓位用 PositionSelect 即可,对冲需 PositionsTotal 循环取 ticket;两者历史仓位都要走成交 ID 反查,逻辑一致。
开平时间、方向、手数、滑点后的净盈亏以及最大浮盈浮亏,基本就能支撑持仓利润图表指标的绘制。
按品种和大致复盘周期切分查询窗口,或在列表类里做增量缓存,避免每次刷新重扫全量成交。