图表上的历史仓位及其盈利/亏损图指标·进阶篇
📊

图表上的历史仓位及其盈利/亏损图指标·进阶篇

(2/3)· 净额与对冲账户之外,历史平仓数据为何没有原生函数可查,本篇给出类设计解法

偏理论进阶 第 2/3 篇
很多交易者以为 PositionSelect 能覆盖所有持仓状态,却在回测历史盈亏时卡死——MT5 根本没有直接处理已平仓位的内置函数。靠手动翻 HistoryDeals 不仅慢,还容易漏掉对冲账户里的独立仓位 ID。这篇先不写指标界面,先把底层数据链路理清楚。

「成交方向与类型的可读化映射」

在 MT5 的 deal 对象封装里,把枚举值翻译成人话是日志与面板可视化的第一步。下面这段把建仓方向(entry)和成交类型(type)转成字符串,方便直接在 Print 或自定义面板里看。 entry 字段只有三种有效状态:DEAL_ENTRY_IN 是开仓进场,DEAL_ENTRY_OUT 是平仓出场,DEAL_ENTRY_INOUT 是反向反转(Reverce)。其余情况被归为「Close a position by an opposite one」,即被反向单强平。 type 的 switch 分支覆盖了 MT5 全部成交类别:从 BUY/SELL 这种真实交易,到 BALANCE、CREDIT、BONUS 等账户事件,再到 CHARGE、CORRECTION、COMMISSION 系列费用项。回测时若只看 BUY/SELL 过滤,会漏掉 BALANCE 变动导致的权益跳变——这是多数自建权益曲线和实盘对不上的隐性原因。 开 MT5 按 F4 建个结构体把这两段抄进去,用 Print(deal.TypeDescription()) 跑历史成交,能立刻核对你的成交分类是否和券商后台一致。外汇与贵金属杠杆高,成交类型里的 CHARGE/COMMISSION 若没算准,净值回撤可能被低估。

MQL5 / C++
class="kw">return(this.m_entry==DEAL_ENTRY_IN ? "Entry In" : this.m_entry==DEAL_ENTRY_OUT ? "Entry Out" : this.m_entry==DEAL_ENTRY_INOUT ? "Reverce" : "Close a position by an opposite one");
class=class="str">"cmt">//--- Return the deal type description
   class="type">class="kw">string         TypeDescription(class="type">void) class="kw">const
      {
       class="kw">switch(this.m_type)
         {
          case DEAL_TYPE_BUY                  :  class="kw">return "Buy";
          case DEAL_TYPE_SELL                 :  class="kw">return "Sell";
          case DEAL_TYPE_BALANCE              :  class="kw">return "Balance";
          case DEAL_TYPE_CREDIT               :  class="kw">return "Credit";
          case DEAL_TYPE_CHARGE               :  class="kw">return "Additional charge";
          case DEAL_TYPE_CORRECTION           :  class="kw">return "Correction";
          case DEAL_TYPE_BONUS                :  class="kw">return "Bonus";
          case DEAL_TYPE_COMMISSION           :  class="kw">return "Additional commission";
          case DEAL_TYPE_COMMISSION_DAILY     :  class="kw">return "Daily commission";
          case DEAL_TYPE_COMMISSION_MONTHLY   :  class="kw">return "Monthly commission";

成交枚举与毫秒级时间的代码落地

在 MT5 的成交封装类里,把枚举值翻译成可读字符串是排查代理佣金、利息和分红的第一步。下面这段 switch 覆盖了日常不会被注意到的非交易类成交:日结/月结代理佣金、账户利息、买卖撤单、股息及含税股息、税务扣减,default 分支则把未识别类型连同原始 m_type 数值抛出来,方便你发现券商自定义的成交种类。 case DEAL_TYPE_COMMISSION_AGENT_DAILY : return "Daily agent commission"; case DEAL_TYPE_COMMISSION_AGENT_MONTHLY: return "Monthly agent commission"; case DEAL_TYPE_INTEREST : return "Interest rate"; case DEAL_TYPE_BUY_CANCELED : return "Canceled buy deal"; case DEAL_TYPE_SELL_CANCELED : return "Canceled sell deal"; case DEAL_DIVIDEND : return "Dividend operations"; case DEAL_DIVIDEND_FRANKED : return "Franked (non-taxable) dividend operations"; case DEAL_TAX : return "Tax charges"; default : return "Unknown: "+(string)this.m_type; 行情回放和剥头皮策略对时间戳精度敏感,自带 TimeToString 只到秒。TimeMSCtoString 用 time_msc/1000 取整拿秒级时间,time_msc%1000 取余补三位毫秒,拼出来就是「2024.05.13 09:30:01.247」这种格式,复盘滑点时有用。 Ticket() 直接返回 m_ticket,是后续用 HistoryDealGetTicket 做逐笔核对的主键。外汇和贵金属杠杆高,这类非方向性成交(佣金、利息、税)会悄悄吃掉净值,建议在自己的成交监听 EA 里把上面枚举全打出来再统计。

MQL5 / C++
case DEAL_TYPE_COMMISSION_AGENT_DAILY   : class="kw">return "Daily agent commission";
case DEAL_TYPE_COMMISSION_AGENT_MONTHLY: class="kw">return "Monthly agent commission";
case DEAL_TYPE_INTEREST                : class="kw">return "Interest rate";
case DEAL_TYPE_BUY_CANCELED            : class="kw">return "Canceled buy deal";
case DEAL_TYPE_SELL_CANCELED           : class="kw">return "Canceled sell deal";
case DEAL_DIVIDEND                     : class="kw">return "Dividend operations";
case DEAL_DIVIDEND_FRANKED             : class="kw">return "Franked(non-taxable) dividend operations";
case DEAL_TAX                          : class="kw">return "Tax charges";
class="kw">default                                : class="kw">return "Unknown: "+(class="type">class="kw">string)this.m_type;

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="kw">public:
class=class="str">"cmt">//--- Methods for returning deal properties
  class="type">long Ticket(class="type">void) class="kw">const { class="kw">return this.m_ticket; }  class=class="str">"cmt">// Deal ticket

◍ 成交记录类的只读访问器

在 MT5 的自定义交易类里,成交(deal)对象的元数据通常通过一组 const 成员函数暴露,调用方只能取不能改,避免历史成交被意外覆盖。 下面这段声明了 13 个只读接口:Magic 返回订单魔数,PositionID 返回关联持仓 ID,TimeMsc 与 Time 分别给出毫秒级和秒级成交时间,TypeDeal、Entry 描述成交类型与开平/反转方向。 Volume、Price 取成交量与成交价;Comission(原文拼写)、Swap、Profit、Fee 对应手续费、平仓时累计库存费、盈亏与额外费用。外汇与贵金属杠杆高,库存费在隔夜持仓上可能显著侵蚀 Profit,回测时建议把 Swap 单独打印出来核对。 把这些访问器直接贴进你的 CTrade 派生类,就能在 EA 里用 deal.Profit() 而非直接碰私有字段,降低出错概率。

MQL5 / C++
class="type">long Magic(class="type">void) class="kw">const { class="kw">return this.m_magic; } class=class="str">"cmt">// Magic number for a deal
class="type">long PositionID(class="type">void) class="kw">const { class="kw">return this.m_position_id; } class=class="str">"cmt">// Position ID
class="type">long TimeMsc(class="type">void) class="kw">const { class="kw">return this.m_time_msc; } class=class="str">"cmt">// Deal execution time in milliseconds
class="type">class="kw">datetime Time(class="type">void) class="kw">const { class="kw">return this.m_time; } class=class="str">"cmt">// Deal execution time
ENUM_DEAL_TYPE TypeDeal(class="type">void) class="kw">const { class="kw">return this.m_type; } class=class="str">"cmt">// Deal type
ENUM_DEAL_ENTRY Entry(class="type">void) class="kw">const { class="kw">return this.m_entry; } class=class="str">"cmt">// Deal entry - entry in, entry out, reverse
class="type">class="kw">double Volume(class="type">void) class="kw">const { class="kw">return this.m_volume; } class=class="str">"cmt">// Deal volume
class="type">class="kw">double Price(class="type">void) class="kw">const { class="kw">return this.m_price; } class=class="str">"cmt">// Deal price
class="type">class="kw">double Comission(class="type">void) class="kw">const { class="kw">return this.m_comission; } class=class="str">"cmt">// Deal commission
class="type">class="kw">double Swap(class="type">void) class="kw">const { class="kw">return this.m_swap; } class=class="str">"cmt">// Accumulated swap when closing
class="type">class="kw">double Profit(class="type">void) class="kw">const { class="kw">return this.m_profit; } class=class="str">"cmt">// Deal financial result
class="type">class="kw">double Fee(class="type">void) class="kw">const { class="kw">return this.m_fee; } class=class="str">"cmt">// Deal fee

「成交对象的字段读写接口」

在 MT5 的成交封装类里,Symbol() 以 const 方法返回当前成交所属品种名,内部直接取 this.m_symbol,这意味着一笔 deal 对象绑定后就不再换品种。 下方这批 Set 方法负责把历史成交中心的原始字段写进实例:SetTicket 存成交单号,SetMagic 存魔术码,SetPositionID 关联持仓 ID,SetTimeMsc / SetTime 分别落毫秒与秒级执行时间。 类型与方向由 SetType(ENUM_DEAL_TYPE) 和 SetEntry(ENUM_DEAL_ENTRY) 接管,后者区分进场、出场与反转三种动作。SetVolume、SetPrice 管量价,SetComission(原文拼写如此)、SetSwap、SetProfit 覆盖费用与盈亏——注意 Commission 拼错不影响编译,但复制时最好顺手修正。 开 MT5 新建 EA 时,若用 CTrade 或历史回放自己填 deal 结构,这套 Setter 可直接套用;外汇与贵金属杠杆高,回测里漏填 Swap 会令盈亏偏离实盘。

MQL5 / C++
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; } class=class="str">"cmt">// Name of the symbol, for which the deal is executed
class=class="str">"cmt">//--- Methods for setting deal properties
class="type">void SetTicket(class="kw">const class="type">long ticket) { this.m_ticket=ticket; } class=class="str">"cmt">// Deal ticket
class="type">void SetMagic(class="kw">const class="type">long magic) { this.m_magic=magic; } class=class="str">"cmt">// Magic number for a deal
class="type">void SetPositionID(class="kw">const class="type">long id) { this.m_position_id=id; } class=class="str">"cmt">// Position ID
class="type">void SetTimeMsc(class="kw">const class="type">long time_msc) { this.m_time_msc=time_msc; } class=class="str">"cmt">// Deal execution time in milliseconds
class="type">void SetTime(class="kw">const class="type">class="kw">datetime time) { this.m_time=time; } class=class="str">"cmt">// Deal execution time
class="type">void SetType(class="kw">const ENUM_DEAL_TYPE type) { this.m_type=type; } class=class="str">"cmt">// Deal type
class="type">void SetEntry(class="kw">const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; } class=class="str">"cmt">// Deal entry - entry in, entry out, reverse
class="type">void SetVolume(class="kw">const class="type">class="kw">double volume) { this.m_volume=volume; } class=class="str">"cmt">// Deal volume
class="type">void SetPrice(class="kw">const class="type">class="kw">double price) { this.m_price=price; } class=class="str">"cmt">// Deal price
class="type">void SetComission(class="kw">const class="type">class="kw">double comission) { this.m_comission=comission; } class=class="str">"cmt">// Deal commission
class="type">void SetSwap(class="kw">const class="type">class="kw">double swap) { this.m_swap=swap; } class=class="str">"cmt">// Accumulated swap when closing
class="type">void SetProfit(class="kw">const class="type">class="kw">double profit) { this.m_profit=profit; } class=class="str">"cmt">// Deal financial result

成交对象的比较与排序接口

在自建的成交记录类里,比较逻辑直接决定了后续按哪种维度做排序。下面这段实现把时间、毫秒时间、订单号、魔法码、持仓 ID 都做成了可切换的对比键,调用方只需传不同 mode 就能拿到升序或降序结果。 SetFee 和 SetSymbol 是两个极轻量的 setter,分别写入单笔手续费和该成交对应的交易品种名。注意 m_fee 和 m_symbol 都是对象内部成员,外部不直接暴露,避免历史成交被误改。 Compare 方法返回 1 / -1 / 0 三态:大于返回 1,小于返回 -1,相等返回 0。例如用 DEAL_SORT_MODE_TIME 时,若 this 的时间戳大于传入节点,返回 1,否则按大小落 -1 或 0。 外汇与贵金属杠杆高、滑点随机,回测里用 TimeMsc 做毫秒级排序,比只用秒级 Time 更不容易把同秒多单排错顺序,实盘核对成交流时建议优先走这个 mode。

MQL5 / C++
class="type">void                SetFee(class="kw">const class="type">class="kw">double fee)                { this.m_fee=fee;            }  class=class="str">"cmt">// Deal fee
class="type">void                SetSymbol(class="kw">const class="type">class="kw">string symbol)            { this.m_symbol=symbol;      }  class=class="str">"cmt">// Name of the symbol, for which the deal is executed
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 CDeal *compared_obj=node;
         class="kw">switch(mode)
            {
             case DEAL_SORT_MODE_TIME       :  class="kw">return(this.Time()>compared_obj.Time()           ?  class="num">1  :  this.Time()<compared_obj.Time()            ?  -class="num">1 :  class="num">0);
             case DEAL_SORT_MODE_TIME_MSC   :  class="kw">return(this.TimeMsc()>compared_obj.TimeMsc()     ?  class="num">1  :  this.TimeMsc()<compared_obj.TimeMsc()        ?  -class="num">1 :  class="num">0);
             case DEAL_SORT_MODE_TIKET      :  class="kw">return(this.Ticket()>compared_obj.Ticket()        ?  class="num">1  :  this.Ticket()<compared_obj.Ticket()          ?  -class="num">1 :  class="num">0);
             case DEAL_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 DEAL_SORT_MODE_POS_ID     :  class="kw">return(this.PositionID()>compared_obj.PositionID() ?  class="num">1  :  this.PositionID()<compared_obj.PositionID() ?  -class="num">1 :  class="num">0);

◍ 成交记录的多字段排序分支

在 MT5 的成交类封装里,Compare 方法常靠一个枚举切换排序维度。下面这段分支覆盖了类型、开仓方向、手数、价格、佣金、库存费、盈亏与手续费共 8 个字段,返回值固定为 1 / -1 / 0 三态。 返回 1 表示当前对象该字段大于比较对象,返回 -1 为小于,相等则回 0,和标准库容器的排序契约一致。实盘跑历史成交统计时,换成 DEAL_SORT_MODE_PROFIT 就能直接按盈亏降序排,省掉自己写比较器。 外汇与贵金属杠杆高,成交滑点会让 Price() 比较结果和实际填单有偏差,排序只作复盘参考,别当成入场信号。

MQL5 / C++
case DEAL_SORT_MODE_TYPE      : class="kw">return(this.TypeDeal()>compared_obj.TypeDeal()      ?  class="num">1 : this.TypeDeal()<compared_obj.TypeDeal()       ?  -class="num">1 :  class="num">0);
case DEAL_SORT_MODE_ENTRY     : class="kw">return(this.Entry()>compared_obj.Entry()           ?  class="num">1 : this.Entry()<compared_obj.Entry()            ?  -class="num">1 :  class="num">0);
case DEAL_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);
case DEAL_SORT_MODE_PRICE     : class="kw">return(this.Price()>compared_obj.Price()           ?  class="num">1 : this.Price()<compared_obj.Price()           ?  -class="num">1 :  class="num">0);
case DEAL_SORT_MODE_COMISSION : class="kw">return(this.Comission()>compared_obj.Comission()   ?  class="num">1 : this.Comission()<compared_obj.Comission()   ?  -class="num">1 :  class="num">0);
case DEAL_SORT_MODE_SWAP      : class="kw">return(this.Swap()>compared_obj.Swap()             ?  class="num">1 : this.Swap()<compared_obj.Swap()             ?  -class="num">1 :  class="num">0);
case DEAL_SORT_MODE_PROFIT    : class="kw">return(this.Profit()>compared_obj.Profit()         ?  class="num">1 : this.Profit()<compared_obj.Profit()         ?  -class="num">1 :  class="num">0);
case DEAL_SORT_MODE_FEE       : class="kw">return(this.Fee()>compared_obj.Fee()               ?  class="num">1 : this.Fee()<compared_obj.Fee()               ?  -class="num">1 :  class="num">0);
让小布替你跑这套类遍历
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史仓位盈亏的聚合视图,不必自己编译 EA 验证类逻辑。

常见问题

HistorySelect 只给交易流水,仓位需由成交中的仓位 ID 聚合而成;净额账户和对冲账户的取值路径不同,需遍历成交按 ID 归并。
是的,对冲账户允许多个独立仓位,先按 PositionsTotal 取数量,循环里用 PositionGetTicket 拿编号再 SelectByTicket 选中,才能逐个读取。
可以,小布盯盘的品种页已内置历史仓位聚合,省去自己写历史仓位列表类的步骤,你只管看分布。
至少需按交易品种、时间段、盈亏正负来过滤;进阶可加按魔术码或手续费区间筛,便于做策略归因。
开仓、修改、平仓三类成交都带仓位 ID,从任一笔都能反查所属仓位,这也是重建历史仓位对象的唯一稳定锚点。