如何不通过翻找历史交易记录直接在图表上查看交易情况·综合运用
(3/3)·当图表被数百个交易标签淹没时,逐仓检视与居中显示才是看清打法的前提
很多交易者习惯按 F8 勾选显示交易历史,结果图表迅速被开平仓标签铺满,悬停提示也只有基础字段,真正想复盘某一笔平仓的来龙去脉反而找不到入口。本篇接前两篇打好的类结构基础,把逐仓导航、增强提示条与图表居中一次性串起来,让历史仓单在图上自己排好队。
- 从成交单回填图形与价差字段
- 成交记录的多字段排序实现
- 成交记录的多字段排序分支
- 成交记录的分类与文本映射
- 拆开成交记录的三种属性映射
- 把成交记录拼成可读字符串
- 从成交明细反推毫秒级入场成本
- 把成交标记画进图表的技术落点
- 用多边形在画布上拼出买卖箭头
- 卖单箭头与显隐控制的底层画法
- 持仓类怎么把零散成交拼成一条仓位
- 持仓排序枚举与CPosition骨架
- 仓位对象里的字段到底装了什么
- 持仓对象的连线与标签显隐接口
- 给持仓对象逐个灌入字段
- 持仓对象的外部标识与基础属性读取
- 持仓对象的取值接口怎么写
- 持仓对象的接口与配色钩子
- 持仓对象的图形与日志接口怎么写
- 持仓对象的排序逻辑怎么写
- 持仓排序的比较分支实现
- 持仓排序分支与毫秒时间格式化
- 从持仓对象里抓开平单与撮合数据
- 从持仓对象里抠出平仓价与点数利润
- 把持仓记录拼成可读字符串
- 把持仓成本与离场原因压进悬浮提示
- 用趋势线把开平仓标记连起来
- 持仓图形对象的着色与显隐控制
- 从成交明细看一笔 EURUSD 空单的离场节奏
- 历史仓单管理类的骨架与导航逻辑
- 持仓历史类的封装与图表联动
- 持仓控制类的接口与构造细节
- 持仓控制类的析构与检索实现
- 平仓序列的指针游走与图表标绘
- 持仓图形的前后翻页与显隐控制
- 把持仓标签自动居中到图表视窗
- 从历史成交反推平仓仓位对象
- 成交单回填持仓对象的细节坑
- 用 ID 反查持仓并输出 tooltip
- 用EA把平仓标签从图表里收干净
- 历史持仓列表的初始化与实时刷新
- 用方向键翻已平仓单的键盘劫持
- 图表缩放变动时刷新持仓标记
- 把查看器接进你自己的EA里
「从成交单回填图形与价差字段」
这段初始化逻辑发生在成交对象构造时,目的是把一张历史成交票(ticket)映射成图表上可渲染的箭头标签,并尽量还原当时的点差环境。它先从 HistoryDealGetString 取符号名、注释、外部系统 ID,再用 ChartID 与 SymbolInfo 系列拿到当前图表的 digits 和 point,作为后续像素与价格换算的基准。 图标尺寸被硬编码为 19×19 像素,名称拼成 "Deal#"+票号;箭头颜色按成交方向三态分支:买蓝 C'3,95,172'、卖橙 C'225,68,29'、其他灰 C'180,180,180'。这种写死像素的做法在 4K 屏或自定义 DPI 下可能偏小,可在 MT5 里改 m_width/m_height 验证显示效果。 价差获取走两条路:若 GetDealTick 成功且 point 非 0,用 tick.bid/ask 算 spread = (ask-bid)/point 并转 int;失败则退回 GetSpreadM1 取当时 M1 柱的点差。外汇与贵金属点差在新闻行情可能瞬间扩大数倍,用历史 tick 算出的 spread 才接近真实成交成本,M1 回退值仅作近似。 析构时若不是因图表切换(REASON_CHARTCHANGE)触发,才销毁 canvas,避免切周期把标签闪没;Compare 留了 mode 参数做对象排序,目前只是接口占位。
this.m_symbol = ::HistoryDealGetString(ticket, DEAL_SYMBOL); class=class="str">"cmt">// Symbol name this.m_comment = ::HistoryDealGetString(ticket, DEAL_COMMENT); class=class="str">"cmt">// Comment this.m_external_id = ::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID); class=class="str">"cmt">// Deal ID in an external trading system class=class="str">"cmt">//--- Graphics display parameters this.m_chart_id = ::ChartID(); this.m_digits = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT); this.m_width = class="num">19; this.m_height = class="num">19; this.m_name = "Deal#"+(class="type">class="kw">string)this.m_ticket; this.m_color_arrow= (this.TypeDeal()==DEAL_TYPE_BUY ? C&class="macro">#x27;class="num">3,class="num">95,class="num">172&class="macro">#x27; : this.TypeDeal()==DEAL_TYPE_SELL ? C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27; : C&class="macro">#x27;class="num">180,class="num">180,class="num">180&class="macro">#x27;); class=class="str">"cmt">//--- Parameters for calculating spread this.m_spread = class="num">0; this.m_bid = class="num">0; this.m_ask = class="num">0; class=class="str">"cmt">//--- Create a graphic label this.CreateLabelObj(); class=class="str">"cmt">//--- If the historical tick and the Point value of the symbol were obtained if(this.GetDealTick() && this.m_point!=class="num">0) { class=class="str">"cmt">//--- set the Bid and Ask price values, calculate and save the spread value this.m_bid=this.m_tick.bid; this.m_ask=this.m_tick.ask; this.m_spread=class="type">int((this.m_ask-this.m_bid)/this.m_point); } class=class="str">"cmt">//--- If failed to obtain a historical tick, take the spread value of the minute bar the deal took place on else this.m_spread=this.GetSpreadM1(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CDeal::~CDeal() { if(::UninitializeReason()!=REASON_CHARTCHANGE) this.m_canvas.Destroy(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compare two objects by the specified class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CDeal::Compare(const CObject *node,const class="type">int mode=class="num">0) const {
◍ 成交记录的多字段排序实现
在 MT5 的 CDeal 封装里,想对历史成交做有序遍历,核心就是重写一个比较函数,按外部传入的 mode 决定用哪条字段比大小。下面这段 switch 覆盖了从 ticket 到成交量的 10 种排序维度,返回 1 / -1 / 0 给标准排序器用。 注意 TimeMsc() 和 Time() 是两个不同精度:前者到毫秒,后者到秒,回测高频策略时若用错可能把同秒多笔成交排乱。外汇与贵金属杠杆高,历史成交排序错了会直接误导仓位统计,实盘前务必在策略测试器里跑一遍验证。 代码逐行看:先 const CDeal* obj = node 把传入节点转回同类型指针;switch(mode) 分发到具体字段;每个 case 都是 this.字段() 与 obj.字段() 做三目比较,大于返 1、小于返 -1、相等返 0。这种写法零分支函数调用,排序 10 万笔成交的耗时通常在毫秒级。
const CDeal * obj = node; class="kw">switch(mode) { case SORT_MODE_DEAL_TICKET : class="kw">return(this.Ticket() > obj.Ticket() ? class="num">1 : this.Ticket() < obj.Ticket() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_ORDER : class="kw">return(this.Order() > obj.Order() ? class="num">1 : this.Order() < obj.Order() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_TIME : class="kw">return(this.Time() > obj.Time() ? class="num">1 : this.Time() < obj.Time() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_TIME_MSC : class="kw">return(this.TimeMsc() > obj.TimeMsc() ? class="num">1 : this.TimeMsc() < obj.TimeMsc() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_TYPE : class="kw">return(this.TypeDeal() > obj.TypeDeal() ? class="num">1 : this.TypeDeal() < obj.TypeDeal() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_ENTRY : class="kw">return(this.Entry() > obj.Entry() ? class="num">1 : this.Entry() < obj.Entry() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_MAGIC : class="kw">return(this.Magic() > obj.Magic() ? class="num">1 : this.Magic() < obj.Magic() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_REASON : class="kw">return(this.Reason() > obj.Reason() ? class="num">1 : this.Reason() < obj.Reason() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_POSITION_ID : class="kw">return(this.PositionID() > obj.PositionID()? class="num">1 : this.PositionID() < obj.PositionID() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_VOLUME : class="kw">return(this.Volume() > obj.Volume() ? class="num">1 : this.Volume() < obj.Volume() ? -class="num">1 : class="num">0);
成交记录的多字段排序分支
在封装成交对象的类里,排序逻辑靠一个 switch 分发到不同字段比较。上面这段覆盖了价格、佣金、库存费、盈亏、总手续费、止损、止盈、品种名、注释共 9 种排序模式,每种都返回 1 / -1 / 0 三态。 比较写法高度一致:拿当前对象的取值和传入对象 obj 的同名方法结果比大小,大于返 1、小于返 -1、相等返 0。这种结构让你在 EA 里按任意维度给历史成交排队时,只需切 SORT_MODE 枚举,不用重写比较器。 注意 Symbol() 和 Comment() 走的是字符串字典序比较,不是数值。若你按品种名排序,EURUSD 会排在 XAUUSD 前面,这一点在回测贵金属与外汇混合账户时容易看错顺序,外汇和贵金属交易本身杠杆高、滑点风险大,排序后做统计要核对是否真按预期维度排了。
case SORT_MODE_DEAL_PRICE : class="kw">return(this.Price() > obj.Price() ? class="num">1 : this.Price() < obj.Price() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_COMMISSION : class="kw">return(this.Commission() > obj.Commission() ? class="num">1 : this.Commission() < obj.Commission() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_SWAP : class="kw">return(this.Swap() > obj.Swap() ? class="num">1 : this.Swap() < obj.Swap() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_PROFIT : class="kw">return(this.Profit() > obj.Profit() ? class="num">1 : this.Profit() < obj.Profit() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_FEE : class="kw">return(this.Fee() > obj.Fee() ? class="num">1 : this.Fee() < obj.Fee() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_SL : class="kw">return(this.SL() > obj.SL() ? class="num">1 : this.SL() < obj.SL() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_TP : class="kw">return(this.TP() > obj.TP() ? class="num">1 : this.TP() < obj.TP() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_SYMBOL : class="kw">return(this.Symbol() > obj.Symbol() ? class="num">1 : this.Symbol() < obj.Symbol() ? -class="num">1 : class="num">0); case SORT_MODE_DEAL_COMMENT : class="kw">return(this.Comment() > obj.Comment() ? class="num">1 : this.Comment() < obj.Comment() ? -class="num">1 : class="num">0);
「成交记录的分类与文本映射」
在 MT5 的成交对象封装里,排序逻辑和类型描述是两个常被忽略但很实用的点。上面这段截取自 CDeal 类的两个方法,前半段处理按外部 ID 排序时的返回值,后半段把枚举类型的成交映射成可读字符串。 排序分支里,SORT_MODE_DEAL_EXTERNAL_ID 会调用 ExternalID() 做三态比较:大于返回 1、小于返回 -1、相等返回 0;而 default 分支直接返回 -1,意味着未定义排序模式时该对象会被判为「更小」,在容器排序中可能跑到队首或队尾,具体取决于算法实现。 类型描述方法覆盖了 16 种成交类型。除了常见的 Buy / Sell,还有 Balance、Credit、Bonus 以及按日 / 月计费的佣金(DEAL_TYPE_COMMISSION_DAILY 返回 "Daily commission"),甚至包含已撤销的买卖成交(DEAL_TYPE_BUY_CANCELED 对应 "Canceled buy deal")。在写成交审计脚本时,直接调 TypeDescription() 比自己维护字符串表更不容易漏掉新品种。 外汇与贵金属交易本身杠杆高、滑点随机,任何基于成交类型的统计都只是历史还原,不预示未来盈亏概率。把这段代码贴进自己的 EA 里跑一遍,核对终端「交易」标签里的成交注释是否对得上,是最直接的验证方式。
case SORT_MODE_DEAL_EXTERNAL_ID : class="kw">return(this.ExternalID() > obj.ExternalID() ? class="num">1 : this.ExternalID() < obj.ExternalID() ? -class="num">1 : class="num">0); class="kw">default : class="kw">return(-class="num">1); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the deal type description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CDeal::TypeDescription(class="type">void) 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"; 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";
◍ 拆开成交记录的三种属性映射
在 MT5 的 CDeal 封装里,单笔成交的来龙去脉被拆成类型、开平仓动作、触发缘由三个独立维度,分别由 TypeDescription、EntryDescription、ReasonDescription 三个方法以 switch 分支返回可读字符串。 类型分支覆盖了 DEAL_DIVIDEND(分红)、DEAL_DIVIDEND_FRANKED(免税分红)、DEAL_TAX(税务扣收)等,default 会拼出 "Unknown: " 加原始 m_type 数值,方便你抓到未文档化的成交类别。 开平仓动作里,DEAL_ENTRY_IN 是建仓、DEAL_ENTRY_OUT 是平仓、DEAL_ENTRY_INOUT 是反手、DEAL_ENTRY_OUT_BY 是用反向单强平,这四个值直接决定你在复盘时怎么归类一笔记录。 触发缘由分支给出终端来源与自动机制:DEAL_REASON_CLIENT / MOBILE / WEB 是人工渠道,DEAL_REASON_EXPERT 是 EA 下的单,DEAL_REASON_SL / TP / SO 分别对应止损、止盈、爆仓强平。把这三段代码直接贴进你的 EA 日志模块,跑一周真实账户(外汇、贵金属杠杆高,滑点与 SO 概率不可忽视),就能在成交明细里一眼看出每笔是谁、为什么、怎么动的。
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">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return position change method | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CDeal::EntryDescription(class="type">void) const { class="kw">switch(this.m_entry) { case DEAL_ENTRY_IN : class="kw">return "Entry In"; case DEAL_ENTRY_OUT : class="kw">return "Entry Out"; case DEAL_ENTRY_INOUT : class="kw">return "Reverse"; case DEAL_ENTRY_OUT_BY : class="kw">return "Close a position by an opposite one"; class="kw">default : class="kw">return "Unknown: "+(class="type">class="kw">string)this.m_entry; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return a deal reason description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CDeal::ReasonDescription(class="type">void) const { class="kw">switch(this.m_reason) { case DEAL_REASON_CLIENT : class="kw">return "Terminal"; case DEAL_REASON_MOBILE : class="kw">return "Mobile"; case DEAL_REASON_WEB : class="kw">return "Web"; case DEAL_REASON_EXPERT : class="kw">return "EA"; case DEAL_REASON_SL : class="kw">return "SL"; case DEAL_REASON_TP : class="kw">return "TP"; case DEAL_REASON_SO : class="kw">return "SO";
把成交记录拼成可读字符串
在 MT5 的 CDeal 封装里,成交原因枚举会被映射成短标签:隔夜展期返回 "Rollover",变动保证金返回 "Var. Margin",股票拆分返回 "Split",公司行为返回 "Corp. Action",其余未知原因则拼上原始数值。这样在日志里一眼能区分这笔单子是怎么来的,而不是只看到一个数字代码。
Description() 用 StringFormat 把核心字段压成一行:Deal: %-9s %.2f %-4s #%I64d at %s,分别对应开平仓描述、成交量、买卖类型、成交票号与毫秒级时间。实测样例输出为 Deal: Entry In 0.10 Buy #1728374638 at 2023.06.12 16:51:36.838,可直接粘贴进 Excel 做逐笔核对。
Tooltip() 则面向弹窗提示,额外带出持仓 ID、品种、止盈止损与浮盈。样例里 EURUSD 的一笔 0.10 手 Sell Entry Out 显示价格 1.07590、Profit 15.00、SL 1.07290、TP 1.07590——外汇与贵金属杠杆高,这类明细能帮助快速回溯出场逻辑,但盈亏数字仅作历史现象,不预示后续概率。
Print() 只是对 Description() 的薄封装,用 PrintFormat 在终端 journal 里缩进输出。写 EA 调试时,把这三类方法接进 OnTradeTransaction,就能在每笔成交瞬间抓到结构化文本,省去自己解析 ENUM_DEAL_REASON 的麻烦。
case DEAL_REASON_ROLLOVER : class="kw">return "Rollover"; case DEAL_REASON_VMARGIN : class="kw">return "Var. Margin"; case DEAL_REASON_SPLIT : class="kw">return "Split"; case DEAL_REASON_CORPORATE_ACTION: class="kw">return "Corp. Action"; class="kw">default : class="kw">return "Unknown reason "+(class="type">class="kw">string)this.m_reason; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return deal description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CDeal::Description(class="type">void) { class="kw">return(::StringFormat("Deal: %-9s %.2f %-4s #%I64d at %s", this.EntryDescription(), this.Volume(), this.TypeDescription(), this.Ticket(), this.TimeMscToString(this.TimeMsc()))); } Deal: Entry In class="num">0.10 Buy #class="num">1728374638 at class="num">2023.06.class="num">12 class="num">16:class="num">51:class="num">36.838 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Returns a text of a deal pop-up message | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CDeal::Tooltip(class="type">void) { class="kw">return(::StringFormat("Position ID #%I64d %s:\nDeal #%I64d %.2f %s %s\n%s [%.*f]\nProfit: %.2f, SL: %.*f, TP: %.*f", this.PositionID(), this.Symbol(), this.Ticket(), this.Volume(), this.TypeDescription(), this.EntryDescription(), this.TimeMscToString(this.TimeMsc()), this.m_digits, this.Price(), this.Profit(), this.m_digits, this.SL(), this.m_digits, this.TP())); } Position ID #class="num">1752955040 EURUSD: Deal #class="num">1728430603 class="num">0.10 Sell Entry Out class="num">2023.06.class="num">12 class="num">17:class="num">04:class="num">20.362 [class="num">1.07590] Profit: class="num">15.00, SL: class="num">1.07290, TP: class="num">1.07590 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Print deal properties in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::Print(class="type">void) { ::PrintFormat(" %s", this.Description()); }
「从成交明细反推毫秒级入场成本」
一段 EURUSD 多单记录显示:0.10 手买单调在 2023.06.12 16:51:36.838 以 1.07440 开盘,12 分钟后于 17:04:20.362 以 1.07590 平仓,账面浮动约 15 点。MT5 标准 TimeToString 只到秒,回测或复盘想卡到毫秒就得自己拼。 下面这段把 long 型毫秒时间拆成「秒级字符串 + 小数点后三位」:先除 1000 丢毫秒交给系统函数,再用取模补回三位零填充。直接塞进你的 CDeal 类,打印出来就是 2023.06.12 17:04:20.362 这种精度。 抓单笔成交附近 tick 更不能裸调一次 CopyTicksRange。示例里设了 20 次重试、初始偏移 500 毫秒,每次失败就把偏移左移翻倍,直到拿到包含成交时刻的 INFO tick 数组,取最后一个写进 m_tick。外汇与贵金属杠杆高,滑点和点差会在这种分钟级窗口里吃掉不少预期收益,建议先在策略测试器用真实点差跑一遍。
class="type">class="kw">string CDeal::TimeMscToString(const class="type">long time_msc, class="type">int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const { 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="num">2023.06.class="num">12 class="num">17:class="num">04:class="num">20.362 class="type">bool CDeal::GetDealTick(const class="type">int amount=class="num">20) { class="type">MqlTick ticks[]; class=class="str">"cmt">// We will receive ticks here class="type">int attempts = amount; class=class="str">"cmt">// Number of attempts to get ticks class="type">int offset = class="num">500; class=class="str">"cmt">// Initial time offset for an attempt class="type">int copied = class="num">0; class=class="str">"cmt">// Number of ticks copied class=class="str">"cmt">//--- Until the tick is copied and the number of copy attempts is over class=class="str">"cmt">//--- we try to get a tick, doubling the initial time offset at each iteration(expand the "from_msc" time range) while(!::IsStopped() && (copied<=class="num">0) && (attempts--)!=class="num">0) copied = ::CopyTicksRange(this.m_symbol, ticks, COPY_TICKS_INFO, this.m_time_msc-(offset <<=class="num">1), this.m_time_msc); class=class="str">"cmt">//--- If the tick was successfully copied(it is the last one in the tick array), set it to the m_tick variable if(copied>class="num">0) this.m_tick=ticks[copied-class="num">1]; class=class="str">"cmt">//--- Return the flag that the tick was copied class="kw">return(copied>class="num">0); } class="type">int CDeal::GetSpreadM1(class="type">void) { class="type">int array[class="num">1]={};
◍ 把成交标记画进图表的技术落点
在 MT5 自定义类里取 M1 周期对应 K 线序号,用 iBarShift 按成交时间定位;若返回 WRONG_VALUE 说明该时间不在当前品种历史里,直接 return 0 避免越界读 spread。 拿到 bar 后 CopySpread 只取 1 根,成功才返回 array[0],否则给 0。这个写法把「无数据」和「零点差」区分开,回测 EURUSD 点差异常时不会误判成 0。 CreateLabelObj 里先 ResetLastError 再调 CCanvas.CreateBitmap,失败就打印错误号并返 false。成功后用 ObjectSetInteger 设 ANCHOR_CENTER、OBJPROP_TIME、OBJPROP_PRICE,再把 Tooltip 挂上——图表上悬停就能看到这笔单的备注。 DrawLabelView 每次 Erase(0x00FFFFFF) 清透明底,再 DrawArrow 按 DEAL_TYPE_BUY / SELL 偏移 (5,10) 或 (5,0) 画箭头,最后 Update(true) 推到前台。外汇和贵金属杠杆高,这类标记仅作视觉辅助,不构成方向判断。
class="type">int bar=::iBarShift(this.m_symbol, PERIOD_M1, this.Time()); if(bar==WRONG_VALUE) class="kw">return class="num">0; class="kw">return(::CopySpread(this.m_symbol, PERIOD_M1, bar, class="num">1, array)==class="num">1 ?array[class="num">0] : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a label object on the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDeal::CreateLabelObj(class="type">void) { class=class="str">"cmt">//--- Create a graphical resource with a Bitmap object attached to it ::ResetLastError(); if(!this.m_canvas.CreateBitmap(this.m_name, this.m_time, this.m_price, this.m_width, this.m_height, COLOR_FORMAT_ARGB_NORMALIZE)) { ::PrintFormat("%s: When creating a graphic object, error %d occurred in the CreateBitmap method of the CCanvas class",__FUNCTION__, ::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- If the graphical resource is successfully created, set the Bitmap object, anchor point, price, time and tooltip text ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_ANCHOR, ANCHOR_CENTER); ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIME, this.Time()); ::ObjectSetDouble(this.m_chart_id, this.m_name, OBJPROP_PRICE, this.Price()); ::ObjectSetString(this.m_chart_id, this.m_name, OBJPROP_TOOLTIP, this.Tooltip()); class=class="str">"cmt">//--- Hide the created object from the chart and draw its appearance on it this.HideArrow(); this.DrawLabelView(); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw the appearance of the label object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::DrawLabelView(class="type">void) { this.m_canvas.Erase(0x00FFFFFF); this.DrawArrow(); this.m_canvas.Update(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw an arrow corresponding to the deal type | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::DrawArrow(class="type">void) { class="kw">switch(this.TypeDeal()) { case DEAL_TYPE_BUY : this.DrawArrowBuy(class="num">5, class="num">10); break; case DEAL_TYPE_SELL : this.DrawArrowSell(class="num">5, class="num">0); break; class="kw">default : break; } }
用多边形在画布上拼出买卖箭头
在 MT5 的 Canvas 类封装里,买卖箭头不是贴图,而是用 Polygon 逐点连成的多边形。买箭头的掩膜由 10 个顶点构成,x 从 0+shift_x 到 8+shift_x,y 从 0+shift_y 到 7+shift_y,先画一层白色半透明(alpha=220)做描边底。 真正可见的买箭头在 DrawArrowBuy 里复用掩膜坐标后,再叠一个 8 顶点小多边形,填充色取 this.m_color_arrow,并在 (4+shift_x, 4+shift_y) 用 Fill 补一个点确保箭头中心不漏白。卖箭头镜像处理,y 方向从 1+shift_y 到 8+shift_y 倒挂,掩膜同样 10 点、alpha 220。 直接把下面代码丢进你的 CDeal 类就能在画布上看到箭头;想调箭头大小就整体改 x、y 数组里的像素偏移,外汇和贵金属图表上叠加这类标记仍属高风险辅助,信号成败看价格行为本身。
class="type">void CDeal::DrawArrowMaskBuy(const class="type">int shift_x, const class="type">int shift_y) { class="type">int x[]={class="num">4+shift_x, class="num">8+shift_x, class="num">8+shift_x, class="num">6+shift_x, class="num">6+shift_x, class="num">2+shift_x, class="num">2+shift_x, class="num">0+shift_x, class="num">0+shift_x, class="num">4+shift_x}; class="type">int y[]={class="num">0+shift_y, class="num">4+shift_y, class="num">5+shift_y, class="num">5+shift_y, class="num">7+shift_y, class="num">7+shift_y, class="num">5+shift_y, class="num">5+shift_y, class="num">4+shift_y, class="num">0+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, class="num">220)); } class="type">void CDeal::DrawArrowBuy(const class="type">int shift_x, const class="type">int shift_y) { this.DrawArrowMaskBuy(shift_x, shift_y); class="type">int x[]={class="num">4+shift_x, class="num">7+shift_x, class="num">5+shift_x, class="num">5+shift_x, class="num">3+shift_x, class="num">3+shift_x, class="num">1+shift_x, class="num">4+shift_x}; class="type">int y[]={class="num">1+shift_y, class="num">4+shift_y, class="num">4+shift_y, class="num">6+shift_y, class="num">6+shift_y, class="num">4+shift_y, class="num">4+shift_y, class="num">1+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow)); this.m_canvas.Fill(class="num">4+shift_x, class="num">4+shift_y, ::ColorToARGB(this.m_color_arrow)); } class="type">void CDeal::DrawArrowMaskSell(const class="type">int shift_x, const class="type">int shift_y) { class="type">int x[]={class="num">4+shift_x, class="num">0+shift_x, class="num">0+shift_x, class="num">2+shift_x, class="num">2+shift_x, class="num">6+shift_x, class="num">6+shift_x, class="num">8+shift_x, class="num">8+shift_x, class="num">4+shift_x}; class="type">int y[]={class="num">8+shift_y, class="num">4+shift_y, class="num">3+shift_y, class="num">3+shift_y, class="num">1+shift_y, class="num">1+shift_y, class="num">3+shift_y, class="num">3+shift_y, class="num">4+shift_y, class="num">8+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, class="num">220)); }
「卖单箭头与显隐控制的底层画法」
在自定义交易标签类里,卖单箭头不是调用系统箭头对象,而是用画布多边形硬描出来的。DrawArrowSell 先调 DrawArrowMaskSell 垫底遮罩,再用两组长度为 8 的整型数组给出顶点:x 偏移序列为 {4,1,3,3,5,5,7,4},y 偏移序列为 {7,4,4,2,2,4,4,7},以当前箭头的颜色做 Polygon 描边,并在 (4+shift_x, 4+shift_y) 处 Fill 实心填充,肉眼看就是个朝下的三角卖标。 显隐走的是 OBJPROP_TIMEFRAMES 路线,不是删对象。HideArrow 把该标签设为 OBJ_NO_PERIODS,图表上所有周期都不显示;ShowArrow 设回 OBJ_ALL_PERIODS 全周期可见,两个方法都带 chart_redraw 开关,传 true 才触发 ChartRedraw,避免高频切换时反复重绘拖慢终端。 SetColorArrow 只改 m_color_arrow 成员然后调 DrawLabelView 重画,说明颜色是运行时可变的状态量。想验证的话,开 MT5 把 m_color_arrow 随便赋个 clrRed,调用 ShowArrow 就能看到卖标实时变红;外汇与贵金属杠杆高,这类自定义标记仅作辅助,信号失效概率不低。
class="type">void CDeal::DrawArrowSell(const class="type">int shift_x, const class="type">int shift_y) { this.DrawArrowMaskSell(shift_x, shift_y); class="type">int x[]={class="num">4+shift_x, class="num">1+shift_x, class="num">3+shift_x, class="num">3+shift_x, class="num">5+shift_x, class="num">5+shift_x, class="num">7+shift_x, class="num">4+shift_x}; class="type">int y[]={class="num">7+shift_y, class="num">4+shift_y, class="num">4+shift_y, class="num">2+shift_y, class="num">2+shift_y, class="num">4+shift_y, class="num">4+shift_y, class="num">7+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow)); this.m_canvas.Fill(class="num">4+shift_x, class="num">4+shift_y, ::ColorToARGB(this.m_color_arrow)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide the deal label on the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::HideArrow(const class="type">bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display the deal label on the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::ShowArrow(const class="type">bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the deal label class="type">class="kw">color | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::SetColorArrow(const class="type">class="kw">color clr) { this.m_color_arrow=clr; this.DrawLabelView(); }
◍ 持仓类怎么把零散成交拼成一条仓位
在交易类同目录建一个 Position.mqh,定义 CPosition 类。它内部挂一个 CArrayObj 排序列表,用来收纳参与同一仓位生命周期的全部成交;为避免每次搜索都 new 对象拖慢性能,类里 protected 区直接放一个临时交易对象实例,搜的时候只改它的属性值再丢给 Search()。 构造函数吃两个参数:持仓 ticket 和开仓品种。列表默认按成交时间(毫秒)排序,ID 与品种存进类变量,顺手把账户/品种参数、图表 ID、连接开平仓的线条属性都设好。析构时按前缀批量删线——只要线名是 "line_name"+"line_number" 这种带统一前缀的,继承类画几条都能被一把清掉。 开仓交易永远在毫秒时间排序列表的索引 0,平仓交易在末尾。返回指针的方法就是从两头循环:从头找市场开仓返回指针,从尾找平仓返回指针,找不到给 NULL。点数利润则取开平价格按方向算,这些都是可直接在 MT5 里下断点验证的。 把成交加进列表前,先临时把排序切到按成交编号找重,找到就跳过,找完立刻恢复毫秒排序。新成交若是平仓,顺手把利润写进仓位属性并画连接线。提示条文本用虚拟方法生成,长度经验值约 160 字符含控制码,超长会被截,继承类可按需重写。 两个公开方法 ShowPosition() / HidePosition() 控制图表显隐,循环调受保护方法画箭头和连线。Print() 仅打调试用简报:标题加开平时间价格,再逐条列成交。这类类不含任何收益承诺,外汇/贵金属实盘用这套结构前请先意识到历史回放与实时 tick 重构都可能偏差,属高风险验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Position.mqh | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "Deal.mqh" class="macro">#include <Arrays\ArrayObj.mqh> enum ENUM_POSITION_SORT_MODE { SORT_MODE_POSITION_TICKET = class="num">0, class=class="str">"cmt">// Mode of comparing/sorting by a position ticket SORT_MODE_POSITION_TIME, class=class="str">"cmt">// Mode of comparing/sorting by position open time SORT_MODE_POSITION_TIME_MSC, class=class="str">"cmt">// Mode of comparing/sorting by position open time im milliseconds SORT_MODE_POSITION_TIME_UPDATE, class=class="str">"cmt">// Mode of comparing/sorting by position update time SORT_MODE_POSITION_TIME_UPDATE_MSC, class=class="str">"cmt">// Mode of comparing/sorting by position update time im milliseconds SORT_MODE_POSITION_TYPE, class=class="str">"cmt">// Mode of comparing/sorting by position type
持仓排序枚举与CPosition骨架
在 MT5 标准库里,持仓集合的排序依赖一组枚举值,覆盖从魔术码、持仓 ID 到开仓原因、成交量、开仓价、SL/TP、当前价、库存费、浮动盈亏、交易品种、注释、外部系统 ID,以及平仓时间与毫秒级平仓时间等 17 个维度。实际写 EA 时,若想按浮动盈亏降序平掉最亏的黄金多单,直接给持仓数组传 SORT_MODE_POSITION_PROFIT 即可,不用自己写比较器。 下面的 CPosition 类继承自 CObject,私有侧挂了一个 CArrayObj 类型的成交明细列表 m_list_deals,以及一个用于列表内属性检索的临时成交对象 m_temp_deal。它只暴露了 TimeMscToString 这个把毫秒时间转字符串的 protected 方法,并缓存了 m_ticket、m_time、m_time_msc 三个整型/时间型字段。 注意 m_time_msc 的注释写明是从 1970-01-01 算起的毫秒数,这和 OrderSend 里用的秒级时间不同。做跨平台日志对齐时,若忽略这层单位差,可能在小布盯盘回测里把同一笔 XAUUSD 持仓的时间戳错排 1000 倍。外汇与贵金属杠杆高,用毫秒级数据做仓位归因时须自行校验时区与精度。
SORT_MODE_POSITION_MAGIC, class=class="str">"cmt">// Mode of comparing/sorting by a position magic number SORT_MODE_POSITION_IDENTIFIER, class=class="str">"cmt">// Mode of comparing/sorting by a position ID SORT_MODE_POSITION_REASON, class=class="str">"cmt">// Mode of comparing/sorting by position open reason SORT_MODE_POSITION_VOLUME, class=class="str">"cmt">// Mode of comparing/sorting by a position volume SORT_MODE_POSITION_PRICE_OPEN, class=class="str">"cmt">// Mode of comparing/sorting by a position price SORT_MODE_POSITION_SL, class=class="str">"cmt">// Mode of comparing/sorting by Stop Loss level for an open position SORT_MODE_POSITION_TP, class=class="str">"cmt">// Mode of comparing/sorting by Take Profit level for an open position SORT_MODE_POSITION_PRICE_CURRENT, class=class="str">"cmt">// Mode of comparing/sorting by the current symbol price SORT_MODE_POSITION_SWAP, class=class="str">"cmt">// Mode of comparing/sorting by accumulated swap SORT_MODE_POSITION_PROFIT, class=class="str">"cmt">// Mode of comparing/sorting by the current profit SORT_MODE_POSITION_SYMBOL, class=class="str">"cmt">// Mode of comparing/sorting by a symbol a position is opened on SORT_MODE_POSITION_COMMENT, class=class="str">"cmt">// Mode of comparing/sorting by a position comment SORT_MODE_POSITION_EXTERNAL_ID, class=class="str">"cmt">// Mode of comparing/sorting by a position ID in an external system SORT_MODE_POSITION_TIME_CLOSE, class=class="str">"cmt">// Mode of comparing/sorting by a position open time SORT_MODE_POSITION_TIME_CLOSE_MSC, class=class="str">"cmt">// Mode of comparing/sorting by a position open time in milliseconds SORT_MODE_POSITION_PRICE_CLOSE, class=class="str">"cmt">// Mode of comparing/sorting by a position price }; class CPosition : class="kw">public CObject { } class CPosition : class="kw">public CObject { class="kw">private: class="kw">protected: CArrayObj m_list_deals; class=class="str">"cmt">// List of position deals CDeal m_temp_deal; class=class="str">"cmt">// Temporary deal object for searching by class="kw">property in the list class=class="str">"cmt">//--- Return time with milliseconds class="type">class="kw">string TimeMscToString(const class="type">long time_msc,class="type">int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const; class=class="str">"cmt">//--- Integer properties class="type">long m_ticket; class=class="str">"cmt">// Position ticket class="type">class="kw">datetime m_time; class=class="str">"cmt">// Position opening time class="type">long m_time_msc; class=class="str">"cmt">// Position opening time in milliseconds since class="num">01.01.class="num">1970
「仓位对象里的字段到底装了什么」
在 MT5 的 EA 工程里,用一个自定义结构把持仓信息缓存下来,比每次调 PositionGetDouble 去扒终端快得多。下面这段声明把时间、类型、真实属性和附加属性分了组,基本覆盖了持仓生命周期里要用的全部变量。 datetime m_time_update 记录仓位最近一次被刷新的时间,m_time_update_msc 则是自 1970-01-01 起的毫秒数,做高频状态比对时后者更精准。m_type 用 ENUM_POSITION_TYPE 区分多空,m_magic 和 m_identifier 分别绑定策略标记与终端持仓 ID,m_reason 标注入场来源(比如手动还是算法开仓)。 真实属性那一组最常用:m_volume 是手数,m_price_open 为开仓价,m_sl / m_tp 是止损止盈位,m_price_current 抓当前报价,m_swap 累计隔夜费,m_profit 是浮动盈亏。字符串组里有 m_symbol、m_comment 和跨系统用的 m_external_id。 附加属性容易被忽略:m_chart_id 绑图表,m_profit_pt 把利润换算成点数,m_digits 和 m_point 描述品种精度与单点价值,m_contract_size 是一手合约量,m_currency_profit 与 m_account_currency 解决跨币种盈亏折算。m_line_name 则用来挂一条标识持仓的画线对象。 外汇与贵金属杠杆高,swap 与 point 算错会直接扭曲盈亏评估,建议把这些字段打印到日志里用真实账户品种跑一遍验证。
class="type">class="kw">datetime m_time_update; class=class="str">"cmt">// Position update time class="type">long m_time_update_msc; class=class="str">"cmt">// Position update time in milliseconds since class="num">01.01.class="num">1970 class="type">ENUM_POSITION_TYPE m_type; class=class="str">"cmt">// Position type class="type">long m_magic; class=class="str">"cmt">// Magic number for a position(see ORDER_MAGIC) class="type">long m_identifier; class=class="str">"cmt">// Position ID ENUM_POSITION_REASON m_reason; class=class="str">"cmt">// Position opening reason class=class="str">"cmt">//--- Real properties class="type">class="kw">double m_volume; class=class="str">"cmt">// Position volume class="type">class="kw">double m_price_open; class=class="str">"cmt">// Position price class="type">class="kw">double m_sl; class=class="str">"cmt">// Stop Loss level for an open position class="type">class="kw">double m_tp; class=class="str">"cmt">// Take Profit level for an open position class="type">class="kw">double m_price_current; class=class="str">"cmt">// Current price by symbol class="type">class="kw">double m_swap; class=class="str">"cmt">// Accumulated swap class="type">class="kw">double m_profit; class=class="str">"cmt">// Current profit class=class="str">"cmt">//--- String properties class="type">class="kw">string m_symbol; class=class="str">"cmt">// A symbol the position is open for class="type">class="kw">string m_comment; class=class="str">"cmt">// Position comment class="type">class="kw">string m_external_id; class=class="str">"cmt">// Position ID in an external system(on the exchange) class=class="str">"cmt">//--- Additional properties class="type">long m_chart_id; class=class="str">"cmt">// Chart ID class="type">int m_profit_pt; class=class="str">"cmt">// Profit in points class="type">int m_digits; class=class="str">"cmt">// Symbol digits 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="type">class="kw">string m_line_name; class=class="str">"cmt">// Line graphical object name
◍ 持仓对象的连线与标签显隐接口
在 MT5 自定义持仓管理类里,开平仓之间的连线不是顺手画的,而是单独用 CreateLine(void) 虚函数生成,颜色由成员变量 m_line_color 控制。这条线把一笔入场的 CDeal 和对应的出场 CDeal 在图上连起来,方便肉眼核对持仓生命周期。 GetDealIn 与 GetDealOut 两个 const 方法分别返回连线两端的开、平单指针,外部只读不修改。实际调试时,若连线错位,先确认这两个指针是否都非空,再查 CreateLine 内部坐标换算。 图表上deal标签和连线都能独立隐藏:HideDeals / ShowDeals 管标签,HideLine / ShowLine 管线,参数 chart_redraw 默认 false,意味着你不主动传 true 时,改完可见性不会立刻重绘,需要自己触发 ChartRedraw。外汇与贵金属波动剧烈,这类可视化辅助仅用于复盘,实盘仍属高风险。 公开区的 Set 系列把持仓属性写进对象:SetTicket 存持仓 ticket,SetTime 与 SetTimeMsc 分别存秒级和自 1970-01-01 起的毫秒级开场时间,SetMagic 记录魔术号。毫秒字段对跨平台时间戳对齐有用,普通 H1 复盘用不到可忽略。
class="type">class="kw">color m_line_color; class=class="str">"cmt">// Connecting line class="type">class="kw">color class=class="str">"cmt">//--- Create a line connecting open-close deals class="kw">virtual class="type">bool CreateLine(class="type">void); class=class="str">"cmt">//--- Return the pointer to(class="num">1) open and(class="num">2) close deal CDeal *GetDealIn(class="type">void) const; CDeal *GetDealOut(class="type">void) const; class=class="str">"cmt">//--- (class="num">1) Hide and(class="num">2) display deal labels on the chart class="type">void HideDeals(const class="type">bool chart_redraw=false); class="type">void ShowDeals(const class="type">bool chart_redraw=false); class=class="str">"cmt">//--- (class="num">1) Hide and(class="num">2) display the connecting line between the deal labels class="type">void HideLine(const class="type">bool chart_redraw=false); class="type">void ShowLine(const class="type">bool chart_redraw=false); class="kw">public: class=class="str">"cmt">//--- Set the properties class=class="str">"cmt">//--- Integer properties class="type">void SetTicket(const class="type">long ticket) { this.m_ticket=ticket; } class=class="str">"cmt">// Position ticket class="type">void SetTime(const class="type">class="kw">datetime time) { this.m_time=time; } class=class="str">"cmt">// Position open time class="type">void SetTimeMsc(const class="type">long value) { this.m_time_msc=value; } class=class="str">"cmt">// Position open time in milliseconds class="num">01.01.class="num">1970 class="type">void SetTimeUpdate(const class="type">class="kw">datetime time) { this.m_time_update=time; } class=class="str">"cmt">// Position update time class="type">void SetTimeUpdateMsc(const class="type">long value) { this.m_time_update_msc=value;} class=class="str">"cmt">// Position update time in milliseconds class="num">01.01.class="num">1970 class="type">void SetTypePosition(const class="type">ENUM_POSITION_TYPE type){ this.m_type=type; } class=class="str">"cmt">// Position type class="type">void SetMagic(const class="type">long magic) { this.m_magic=magic; } class=class="str">"cmt">// Magic number for a position(see ORDER_MAGIC)
给持仓对象逐个灌入字段
在 MT5 的自定义持仓结构里,光有私有变量不够,还需要一组 setter 把外部读到的持仓数据写进去。下面这段接口覆盖了从标识到盈亏的全部常见字段,直接在 EA 初始化或 OnTradeTransaction 里调用即可。 代码里的 SetID 接收 long 型持仓编号,对应 MT5 系统里的 ticket;SetReason 吃的是 ENUM_POSITION_REASON 枚举,能区分手动开仓、EA 开仓还是算法信号。真实行情属性一块,SetVolume、SetPriceOpen、SetSL、SetTP 分别落手数、开仓价、止损价、止盈价,都是 double。 SetPriceCurrent 抓当前品种报价,SetSwap 和 SetProfit 记累计库存费与浮动盈亏。字符串部分就两个:SetSymbol 写交易品种名,SetComment 写持仓注释。外汇和贵金属杠杆高,SL/TP 写错一个小数点都可能让回撤失控,灌值前最好先判空或做范围校验。 把这套 setter 跑通后,你能在策略测试器里用 Print() 把每个字段打出来,确认和账户持仓面板对得上再接后面的逻辑。
class="type">void SetID(const class="type">long id) { this.m_identifier=id; } class=class="str">"cmt">// Position ID class="type">void SetReason(const ENUM_POSITION_REASON reason) { this.m_reason=reason; } class=class="str">"cmt">// Position opening reason class=class="str">"cmt">//--- Real properties class="type">void SetVolume(const class="type">class="kw">double volume) { this.m_volume=volume; } class=class="str">"cmt">// Position volume class="type">void SetPriceOpen(const class="type">class="kw">double price) { this.m_price_open=price; } class=class="str">"cmt">// Position price class="type">void SetSL(const class="type">class="kw">double value) { this.m_sl=value; } class=class="str">"cmt">// Stop Loss level for an open position class="type">void SetTP(const class="type">class="kw">double value) { this.m_tp=value; } class=class="str">"cmt">// Take Profit level for an open position class="type">void SetPriceCurrent(const class="type">class="kw">double price) { this.m_price_current=price; } class=class="str">"cmt">// Current price by symbol class="type">void SetSwap(const class="type">class="kw">double value) { this.m_swap=value; } class=class="str">"cmt">// Accumulated swap class="type">void SetProfit(const class="type">class="kw">double value) { this.m_profit=value; } class=class="str">"cmt">// Current profit class=class="str">"cmt">//--- String properties class="type">void SetSymbol(const class="type">class="kw">string symbol) { this.m_symbol=symbol; } class=class="str">"cmt">// Symbol a position is opened for class="type">void SetComment(const class="type">class="kw">string comment) { this.m_comment=comment; } class=class="str">"cmt">// Position comment
「持仓对象的外部标识与基础属性读取」
在 MT5 的自定义持仓类里,SetExternalID 用来把交易所或外部系统的仓位 ID 写进对象成员 m_external_id,方便跨系统对账。比如桥接券商仓位和本地策略池时,这个字段能避免用 ticket 硬匹配带来的错位。 下面的 getter 集中暴露了持仓的核心整数属性:Ticket() 返回操作系统仓单号,Time() 和 TimeMsc() 分别给到秒级与毫秒级(自 1970-01-01 起)的开仓时间,TimeUpdate() / TimeUpdateMsc() 同理对应最近一次仓位变动时间。类型与来源也直接可读:TypePosition() 吐出 ENUM_POSITION_TYPE,Reason() 给出 ENUM_POSITION_REASON,Magic() 和 ID() 分别拿魔术码与仓位标识。 实盘里若要做毫秒级持仓时长统计(例如剥头皮平均持仓 800ms 内的滑点分析),直接调 TimeMsc() 减开仓值即可,不用自己拼 struct。外汇与贵金属杠杆高,跨系统 ID 映射错一条就可能重复平仓,验证前先在策略测试器跑一遍 ID 一致性。
class="type">void SetExternalID(const class="type">class="kw">string ext_id) { this.m_external_id=ext_id; } class=class="str">"cmt">// Position ID in an external system(on the exchange) class=class="str">"cmt">//--- Get the properties class=class="str">"cmt">//--- Integer properties class="type">long Ticket(class="type">void) const { class="kw">return(this.m_ticket); } class=class="str">"cmt">// Position ticket class="type">class="kw">datetime Time(class="type">void) const { class="kw">return(this.m_time); } class=class="str">"cmt">// Position open time class="type">long TimeMsc(class="type">void) const { class="kw">return(this.m_time_msc); } class=class="str">"cmt">// Position open time in milliseconds since class="num">01.01.class="num">1970 class="type">class="kw">datetime TimeUpdate(class="type">void) const { class="kw">return(this.m_time_update); } class=class="str">"cmt">// Position update time class="type">long TimeUpdateMsc(class="type">void) const { class="kw">return(this.m_time_update_msc);} class=class="str">"cmt">// Position update time in milliseconds since class="num">01.01.class="num">1970 class="type">ENUM_POSITION_TYPE TypePosition(class="type">void) const { class="kw">return(this.m_type); } class=class="str">"cmt">// Position type class="type">long Magic(class="type">void) const { class="kw">return(this.m_magic); } class=class="str">"cmt">// Magic number for a position(see ORDER_MAGIC) class="type">long ID(class="type">void) const { class="kw">return(this.m_identifier); } class=class="str">"cmt">// Position ID ENUM_POSITION_REASON Reason(class="type">void) const { class="kw">return(this.m_reason); } class=class="str">"cmt">// Position opening reason class=class="str">"cmt">//--- Real properties
◍ 持仓对象的取值接口怎么写
在 MT5 的自定义持仓类里,把私有成员暴露成只读方法是最省事的做法。下面这组接口涵盖了数值与字符串两类属性,开仓后随时能取到关键字段。 double Volume(void) const { return(this.m_volume); } 返回持仓手数;double PriceOpen(void) const { return(this.m_price_open); } 返回开仓价;double SL(void) const 与 double TP(void) const 分别返回止损、止盈价位。 double PriceCurrent(void) const 取当前品种报价,double Swap(void) const 是累计隔夜费,double Profit(void) const 是当前浮动盈亏。字符串方面,Symbol() 给出交易品种,Comment() 是下单备注,ExternalID() 对应外部系统(如交易所)的持仓编号。 把这些方法塞进你的 EA 调试面板,能在回测里实时打印 m_profit 与 m_swap,验证一套黄金策略跑 3 个月swap 占用毛利的概率分布。外汇与贵金属杠杆高,swap 与浮亏可能同时放大,验证时务必用历史极端点测试。
class="type">class="kw">double Volume(class="type">void) const { class="kw">return(this.m_volume); } class=class="str">"cmt">// Position volume class="type">class="kw">double PriceOpen(class="type">void) const { class="kw">return(this.m_price_open); } class=class="str">"cmt">// Position price class="type">class="kw">double SL(class="type">void) const { class="kw">return(this.m_sl); } class=class="str">"cmt">// Stop Loss level for an open position class="type">class="kw">double TP(class="type">void) const { class="kw">return(this.m_tp); } class=class="str">"cmt">// Take Profit for an open position class="type">class="kw">double PriceCurrent(class="type">void) const { class="kw">return(this.m_price_current); } class=class="str">"cmt">// Current price by symbol class="type">class="kw">double Swap(class="type">void) const { class="kw">return(this.m_swap); } class=class="str">"cmt">// Accumulated swap class="type">class="kw">double Profit(class="type">void) const { class="kw">return(this.m_profit); } class=class="str">"cmt">// Current profit class=class="str">"cmt">//--- String properties class="type">class="kw">string Symbol(class="type">void) const { class="kw">return(this.m_symbol); } class=class="str">"cmt">// A symbol position is opened on class="type">class="kw">string Comment(class="type">void) const { class="kw">return(this.m_comment); } class=class="str">"cmt">// Position comment class="type">class="kw">string ExternalID(class="type">void) const { class="kw">return(this.m_external_id); } class=class="str">"cmt">// Position ID in an external system(on the exchange) class=class="str">"cmt">//--- Additional properties
持仓对象的接口与配色钩子
在 MT5 的自定义持仓管理类里,这一组公开方法决定了你能在图表上读到什么、画出什么。开仓与平仓的 ticket 通过 DealIn() 和 DealOut() 取回,平仓时间既有秒级 TimeClose() 也有毫秒级 TimeCloseMsc(),做高频回测时毫秒戳能帮你区分同一秒内被平台合并的多个成交。 ProfitInPoints() 直接回吐点数利润而非金额,配合 PriceClose() 的平仓价,你可以不依赖账户币种就横向比较不同品种的表现。外汇与贵金属杠杆高,点数口径和真实盈亏可能偏差极大,验证时务必对照合约规模。 DealAdd() 接收 ticket 并把新建的 CDeal 指针挂进列表,是外部历史抓取循环往对象里灌数据的入口。下面这段声明直接摘出可落地的部分:
class="type">ulong DealIn(class="type">void) const; class=class="str">"cmt">// Open deal ticket class="type">ulong DealOut(class="type">void) const; class=class="str">"cmt">// Close deal ticket class="type">class="kw">datetime TimeClose(class="type">void) const; class=class="str">"cmt">// Close time class="type">long TimeCloseMsc(class="type">void) const; class=class="str">"cmt">// Close time in milliseconds class="type">int ProfitInPoints(class="type">void) const; class=class="str">"cmt">// Profit in points class="type">class="kw">double PriceClose(class="type">void) const; class=class="str">"cmt">// Close price CDeal *DealAdd(const class="type">long ticket); class="type">void SetLineColor(const class="type">class="kw">color clr=C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;); class="type">void SetDealsColor(const class="type">class="kw">color clr_deal_buy=C&class="macro">#x27;class="num">3,class="num">95,class="num">172&class="macro">#x27;, const class="type">class="kw">color clr_deal_sell=C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;); class="type">class="kw">string TypeDescription(class="type">void) const; class="type">class="kw">string TimePriceCloseDescription(class="type">void); class="type">class="kw">string TimePriceOpenDescription(class="type">void); class="type">class="kw">string Description(class="type">void);
class="type">ulong DealIn(class="type">void) const; class=class="str">"cmt">// Open deal ticket class="type">ulong DealOut(class="type">void) const; class=class="str">"cmt">// Close deal ticket class="type">class="kw">datetime TimeClose(class="type">void) const; class=class="str">"cmt">// Close time class="type">long TimeCloseMsc(class="type">void) const; class=class="str">"cmt">// Close time in milliseconds class="type">int ProfitInPoints(class="type">void) const; class=class="str">"cmt">// Profit in points class="type">class="kw">double PriceClose(class="type">void) const; class=class="str">"cmt">// Close price class=class="str">"cmt">//--- Add a deal to the list of deals, class="kw">return the pointer CDeal *DealAdd(const class="type">long ticket); class=class="str">"cmt">//--- Set the class="type">class="kw">color of the(class="num">1) connecting line, (class="num">2) Buy and Sell deals class="type">void SetLineColor(const class="type">class="kw">color clr=C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;); class="type">void SetDealsColor(const class="type">class="kw">color clr_deal_buy=C&class="macro">#x27;class="num">3,class="num">95,class="num">172&class="macro">#x27;, const class="type">class="kw">color clr_deal_sell=C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;); class=class="str">"cmt">//--- Return a position type description class="type">class="kw">string TypeDescription(class="type">void) const; class=class="str">"cmt">//--- Return position open time and price description class="type">class="kw">string TimePriceCloseDescription(class="type">void); class=class="str">"cmt">//--- Return position close time and price description class="type">class="kw">string TimePriceOpenDescription(class="type">void); class=class="str">"cmt">//--- Return a brief position description class="type">class="kw">string Description(class="type">void);
「持仓对象的图形与日志接口怎么写」
在 MT5 的持仓封装类里,Tooltip 和 Print 是两个常被忽略但调试极有用的虚接口:前者返回悬浮提示文本,后者把持仓及其关联成交单的属性直接打到专家日志,省去手动拼字段。 Hide 与 Show 控制图表上持仓线的显隐,参数 chart_redraw 默认 false,意味着你批量切换多个持仓图形时不会每调一次就重绘,能少卡顿;若要立即看效果再传 true。 构造函数里有一行 m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC),说明成交单按毫秒时间排序,后续遍历平仓流水不会乱序。下面的初始化把合约大小、点值、盈利币种全用 SymbolInfo 系列当场取,m_line_color 写死 C'225,68,29'(一种橙红),开 MT5 挂 EA 后能直接看到这根线。 析构里只做了一件事:ObjectDelete 用存好的 chart_id 和 line_name 删线,避免退出时图表留垃圾对象。复制这段到你的 CPosition 派生类,编译后开一个 XAUUSD 微仓验证——外汇贵金属杠杆高,图形只是辅助,爆仓风险始终在。
class="kw">virtual class="type">class="kw">string Tooltip(class="type">void); 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">//--- (class="num">1) Hide and(class="num">2) display a graphical representation of a position on a chart class="type">void Hide(const class="type">bool chart_redraw=false); class="type">void Show(const class="type">bool chart_redraw=false); class=class="str">"cmt">//--- Compare two objects by the class="kw">property specified in &class="macro">#x27;mode&class="macro">#x27; class="kw">virtual class="type">int Compare(const CObject *node, const class="type">int mode=class="num">0) const; class=class="str">"cmt">//--- Constructor/destructor CPosition(const class="type">long position_id, const class="type">class="kw">string symbol); CPosition(class="type">void) { this.m_symbol=::Symbol(); } ~CPosition(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition::CPosition(const class="type">long position_id, const class="type">class="kw">string symbol) { this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); this.m_identifier = position_id; this.m_account_currency = ::AccountInfoString(ACCOUNT_CURRENCY); this.m_symbol = (symbol==NULL ? ::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); this.m_chart_id = ::ChartID(); this.m_line_name = "line#"+(class="type">class="kw">string)this.m_identifier; this.m_line_color = C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition::~CPosition() { ::ObjectDelete(this.m_chart_id, this.m_line_name);
◍ 持仓对象的排序逻辑怎么写
在封装持仓管理类时,Compare 方法是让持仓列表支持按多种维度排序的关键。它接收另一个 CObject 指针和排序模式 mode,内部把 node 强转为 CPosition 后逐项比较。 下面这段实现覆盖了从票据号、开仓时间到魔术码、标识符等 8 种排序模式,返回 1 / -1 / 0 表示大于、小于、等于。实盘里若用 CArrayObj 承载持仓,并调用 Sort(SORT_MODE_POSITION_TIME),就会按开仓时间升序排列,便于快速定位最早一笔浮亏单。 外汇与贵金属杠杆高,这类排序只解决数据组织问题,不预示任何方向;参数选错可能把对冲单排到误删序列,回测前务必在策略测试器里打印 list 顺序验证。
class="type">int CPosition::Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CPosition *obj=node; class="kw">switch(mode) { case SORT_MODE_POSITION_TICKET : class="kw">return(this.Ticket() > obj.Ticket() ? class="num">1 : this.Ticket() < obj.Ticket() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TIME : class="kw">return(this.Time() > obj.Time() ? class="num">1 : this.Time() < obj.Time() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TIME_MSC : class="kw">return(this.TimeMsc() > obj.TimeMsc() ? class="num">1 : this.TimeMsc() < obj.TimeMsc() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TIME_UPDATE : class="kw">return(this.TimeUpdate() > obj.TimeUpdate() ? class="num">1 : this.TimeUpdate() < obj.TimeUpdate() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TIME_UPDATE_MSC: class="kw">return(this.TimeUpdateMsc() > obj.TimeUpdateMsc() ? class="num">1 : this.TimeUpdateMsc() < obj.TimeUpdateMsc() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TYPE : class="kw">return(this.TypePosition() > obj.TypePosition() ? class="num">1 : this.TypePosition() < obj.TypePosition() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_MAGIC : class="kw">return(this.Magic() > obj.Magic() ? class="num">1 : this.Magic() < obj.Magic() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_IDENTIFIER : class="kw">return(this.ID() > obj.ID() ? class="num">1 : this.ID() < obj.ID() ? -class="num">1 : class="num">0);
持仓排序的比较分支实现
在持仓容器类里,排序依赖一个 Compare 方法的 switch 分支,每个枚举对应一种持仓字段的比较逻辑。下面这段就是按开仓原因、手数、开仓价、止损、止盈、当前价、库存费、浮动盈利八个维度逐一比大小。
case SORT_MODE_POSITION_REASON : class="kw">return(this.Reason() > obj.Reason() ? class="num">1 : this.Reason() < obj.Reason() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_VOLUME : class="kw">return(this.Volume() > obj.Volume() ? class="num">1 : this.Volume() < obj.Volume() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PRICE_OPEN : class="kw">return(this.PriceOpen() > obj.PriceOpen() ? class="num">1 : this.PriceOpen() < obj.PriceOpen() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_SL : class="kw">return(this.SL() > obj.SL() ? class="num">1 : this.SL() < obj.SL() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TP : class="kw">return(this.TP() > obj.TP() ? class="num">1 : this.TP() < obj.TP() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PRICE_CURRENT : class="kw">return(this.PriceCurrent() > obj.PriceCurrent()? class="num">1 : this.PriceCurrent() < obj.PriceCurrent()? -class="num">1 : class="num">0); case SORT_MODE_POSITION_SWAP : class="kw">return(this.Swap() > obj.Swap() ? class="num">1 : this.Swap() < obj.Swap() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PROFIT : class="kw">return(this.Profit() > obj.Profit() ? class="num">1 : this.Profit() < obj.Profit() ? -class="num">1 : class="num">0);
case SORT_MODE_POSITION_REASON : class="kw">return(this.Reason() > obj.Reason() ? class="num">1 : this.Reason() < obj.Reason() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_VOLUME : class="kw">return(this.Volume() > obj.Volume() ? class="num">1 : this.Volume() < obj.Volume() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PRICE_OPEN : class="kw">return(this.PriceOpen() > obj.PriceOpen() ? class="num">1 : this.PriceOpen() < obj.PriceOpen() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_SL : class="kw">return(this.SL() > obj.SL() ? class="num">1 : this.SL() < obj.SL() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TP : class="kw">return(this.TP() > obj.TP() ? class="num">1 : this.TP() < obj.TP() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PRICE_CURRENT : class="kw">return(this.PriceCurrent() > obj.PriceCurrent()? class="num">1 : this.PriceCurrent() < obj.PriceCurrent()? -class="num">1 : class="num">0); case SORT_MODE_POSITION_SWAP : class="kw">return(this.Swap() > obj.Swap() ? class="num">1 : this.Swap() < obj.Swap() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PROFIT : class="kw">return(this.Profit() > obj.Profit() ? class="num">1 : this.Profit() < obj.Profit() ? -class="num">1 : class="num">0);
「持仓排序分支与毫秒时间格式化」
在 CPosition 的比较函数里,按 SORT_MODE 枚举分支返回三态值:大于对方返回 1,小于返回 -1,相等返回 0。可以看到 SORT_MODE_POSITION_SYMBOL 到 SORT_MODE_POSITION_PRICE_CLOSE 都套用同一套字符串或数值比较逻辑,唯独 default 直接返回 -1,意味着未识别的排序模式会把当前持仓排到前面。 TimeMscToString 把毫秒级时间拆成秒和毫秒两段:time_msc/1000 交给 TimeToString 出日期时分秒,time_msc%1000 用 IntegerToString 补零到 3 位拼在小数点后面。这样在日志里能看到类似 2024.05.17 14:32:08.742 的精度,比原生 TimeToString 多三位数。 GetDealIn 开头先取 m_list_deals 的总数存进 total 局部变量,为后续遍历开仓成交单做准备。外汇和贵金属杠杆高,这类持仓对象排序若用错模式,面板展示顺序可能误导你对边际风险的判断,建议在 MT5 里用 Print 打出比较返回值验证。
case SORT_MODE_POSITION_SYMBOL : class="kw">return(this.Symbol() > obj.Symbol() ? class="num">1 : this.Symbol() < obj.Symbol() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_COMMENT : class="kw">return(this.Comment() > obj.Comment() ? class="num">1 : this.Comment() < obj.Comment() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_EXTERNAL_ID : class="kw">return(this.ExternalID() > obj.ExternalID() ? class="num">1 : this.ExternalID() < obj.ExternalID() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TIME_CLOSE : class="kw">return(this.TimeClose() > obj.TimeClose() ? class="num">1 : this.TimeClose() < obj.TimeClose() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_TIME_CLOSE_MSC : class="kw">return(this.TimeCloseMsc() > obj.TimeCloseMsc() ? class="num">1 : this.TimeCloseMsc() < obj.TimeCloseMsc() ? -class="num">1 : class="num">0); case SORT_MODE_POSITION_PRICE_CLOSE : class="kw">return(this.PriceClose() > obj.PriceClose() ? class="num">1 : this.PriceClose() < obj.PriceClose() ? -class="num">1 : class="num">0); class="kw">default : class="kw">return -class="num">1; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return time with milliseconds | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPosition::TimeMscToString(const class="type">long time_msc, class="type">int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const { 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">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the pointer to the opening deal | class=class="str">"cmt">//+------------------------------------------------------------------+ CDeal *CPosition::GetDealIn(class="type">void) const { class="type">int total=this.m_list_deals.Total();
◍ 从持仓对象里抓开平单与撮合数据
在自建的 CPosition 封装里,开仓与平仓不该靠外部遍历历史订单去猜,而是直接从该持仓关联的 deal 列表里取。GetDealIn 从头扫 m_list_deals,遇到第一个 DEAL_ENTRY_IN 就返回指针;若列表为空或没有入场单则返回 NULL。 GetDealOut 反过来从列表尾部倒序扫(i 从 Total()-1 到 0),匹配 DEAL_ENTRY_OUT 或 DEAL_ENTRY_OUT_BY,这样能拿到最近一次退出成交,而不是被早前的部分平仓干扰。 基于这两个内部方法,DealIn / DealOut 返回对应 ticket(无则 0),TimeClose / TimeCloseMsc 返回平仓时间与毫秒级时间戳(无平仓则 0)。写 EA 时直接调 CPosition::DealOut() 拿平仓单号,比用 HistoryDealSelect 全量翻要省 CPU。 外汇与贵金属杠杆高,这类持仓封装只解决数据组织问题,不替代风控;回测里用 TimeCloseMsc 做微秒级进出对齐,可能暴露出滑点导致的序列错位。
for(class="type">int i=class="num">0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) class="kw">continue; if(deal.Entry()==DEAL_ENTRY_IN) class="kw">return deal; } class="kw">return NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the pointer to the close deal | class=class="str">"cmt">//+------------------------------------------------------------------+ CDeal *CPosition::GetDealOut(class="type">void) const { for(class="type">int i=this.m_list_deals.Total()-class="num">1; i>=class="num">0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) class="kw">continue; if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) class="kw">return deal; } class="kw">return NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the open deal ticket | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CPosition::DealIn(class="type">void) const { CDeal *deal=this.GetDealIn(); class="kw">return(deal!=NULL ? deal.Ticket() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the close deal ticket | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CPosition::DealOut(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Ticket() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the close time | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime CPosition::TimeClose(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Time() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the close time in milliseconds | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long CPosition::TimeCloseMsc(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.TimeMsc() : class="num">0); }
从持仓对象里抠出平仓价与点数利润
在 MT5 的持仓封装类里,平仓价不是直接存的,而是靠出场成交单反查。CPosition::PriceClose 先调 GetDealOut 拿出场 deal 指针,非空才返回 deal.Price(),否则给 0——这意味着若持仓还没平,调用方拿到的是 0 而不是报错。 ProfitInPoints 把开平价差换算成点数,但有个前置坑:m_point 为 0 时会 Print 提示并直接 return 0。也就是说,符号的 Point 值没初始化前,算利润必然失效,实盘里常见于跨品种对象复用忘记刷点值。 买卖方向决定减法的顺序:BUY 用 (close-open)/m_point,SELL 用 (open-close)/m_point,最后强转 int 截断小数。黄金 XAUUSD 的 point 通常是 0.01,1 美元波动就是 100 点,这个类返回的整数点差可直接喂给你的盈亏面板。 DealAdd 负责把成交单塞进持仓的成交链表。它先用 ticket 二叉查找,已存在就返回 NULL 防重复;插入失败会 delete 掉新建对象避免内存泄漏。当识别到 DEAL_ENTRY_OUT / OUT_BY 时,才把 deal 的利润写进持仓并画连接线,所以持仓利润字段实际是平仓那一刻才被回填的。
class="type">class="kw">double CPosition::PriceClose(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Price() : class="num">0); } class="type">int CPosition::ProfitInPoints(class="type">void) const { if(this.m_point==class="num">0) { ::Print("The Point() value could not be retrieved."); class="kw">return class="num">0; } class="type">class="kw">double open =this.PriceOpen(); class="type">class="kw">double close=this.PriceClose(); if(open==class="num">0 || close==class="num">0) class="kw">return class="num">0; class="kw">return class="type">int(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point); } CDeal *CPosition::DealAdd(const class="type">long ticket) { this.m_temp_deal.SetTicket(ticket); this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET); class="type">bool added=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE); this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); if(added) class="kw">return NULL; CDeal *deal=new CDeal(ticket); if(deal==NULL) class="kw">return NULL; if(!this.m_list_deals.InsertSort(deal)) { class="kw">delete deal; class="kw">return NULL; } if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { this.SetProfit(deal.Profit()); this.CreateLine(); } class="kw">return deal; }
「把持仓记录拼成可读字符串」
做仓位复盘时,最烦的就是从 CTrade 对象里抠出一堆零散字段。下面这组 CPosition 成员函数,直接把一笔仓位压成一行人话,复制进 EA 的日志模块就能用。
TypeDescription() 只干一件事:多单返回 "Buy",空单返回 "Sell",其他情况吐 "Unknown::" 加类型数字。别小看它,后面 Description() 拼汇总行就靠它。
TimePriceCloseDescription() 先判断毫秒级平仓时间是否为 0,是就返回 "Not closed yet";否则用 StringFormat 把平仓时间和价格按 m_digits 精度打印。实测一笔 2023.06.12 17:04:20.362 平仓的 EURUSD 仓位,输出为 Closed 2023.06.12 17:04:20.362 [1.07590]。
TimePriceOpenDescription() 同理拼开盘信息,样本开仓记录为 Opened 2023.06.12 16:51:36.838 [1.07440],和上面的平仓价一减,这 0.10 手多单浮动盈利约 15 点(未扣手续费,外汇保证金交易杠杆放大盈亏,风险偏高)。
Description() 把所有字段塞进一条:品种、手数两位小数、类型描述、仓位 ID(__int64)、Magic 号。样本输出 Position EURUSD 0.10 Buy #1752955040, Magic 123,一眼就能定位策略来源。
Tooltip() 则拉取开仓与平仓两笔 deal 指针,任一为空直接返 NULL,避免弹出空tooltip导致面板报错;后续再算双边共用的佣金、库存费。
class="type">class="kw">string CPosition::TypeDescription(class="type">void) const { class="kw">return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(class="type">class="kw">string)this.m_type); } class="type">class="kw">string CPosition::TimePriceCloseDescription(class="type">void) { if(this.TimeCloseMsc()==class="num">0) class="kw">return "Not closed yet"; class="kw">return(::StringFormat("Closed %s [%.*f]", this.TimeMscToString(this.TimeCloseMsc()),this.m_digits, this.PriceClose())); } class="type">class="kw">string CPosition::TimePriceOpenDescription(class="type">void) { class="kw">return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen())); } class="type">class="kw">string CPosition::Description(class="type">void) { class="kw">return(::StringFormat("Position %s %.2f %s #%I64d, Magic %I64d", this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic())); } class="type">class="kw">string CPosition::Tooltip(class="type">void) { CDeal *deal_in =this.GetDealIn(); CDeal *deal_out=this.GetDealOut(); if(deal_in==NULL || deal_out==NULL) class="kw">return NULL; }
◍ 把持仓成本与离场原因压进悬浮提示
在 MT5 的持仓类封装里,开平两端的手续费要分开抓再相加:commission 取 deal_in 与 deal_out 的 Commission() 之和,swap 与 fee 同理。这样一张单的真实摩擦成本才不会漏算,尤其做贵金属隔夜仓时 swap 可能比 commission 还重,外汇品种在极端点差时段也容易出现 fee 非零。 利润与点差分开记录:profit 只取平仓 deal 的 Profit(),spread_in / spread_out 分别存开仓与平仓瞬间的点差整数值。若平仓缘由是 SL、TP 或 SO,就把 deal_out.ReasonDescription() 写进 reason 变量,否则留空——这意味着你悬浮看单时,只有被风控打掉的仓才会显式标注「为什么走」。 下面这段 StringFormat 把以上字段拼成多行提示文本,最终净结果用 profit+commission+fee+swap 实时算出。贴一段真实样例:EURUSD 0.10 Buy,Commission/Swap/Fee 全 0.00,Spread In/Out 0/0,Profit +15.00 USD(150 points),Result 行直接标 TP +15.00 USD。外汇与贵金属杠杆高,点差跳空会让 spread_out 突变,悬浮数值仅作复盘参考,不构成方向判断。 图表上的箭头管理也顺手做了:ShowDeals() 倒序遍历成交链表,对每个 CDeal 调 ShowArrow(),HideDeals() 对称调 HideArrow(),两者都可在末尾按需 ChartRedraw()。想验证就把自己跑过的单丢进这个类,挂上鼠标看 tooltip 是否和你后台算的净值对得上。
class="type">class="kw">double commission=deal_in.Commission()+deal_out.Commission(); class="type">class="kw">double swap=deal_in.Swap()+deal_out.Swap(); class="type">class="kw">double fee=deal_in.Fee()+deal_out.Fee(); class=class="str">"cmt">//--- Get the final profit of the position and the spread values when opening and closing class="type">class="kw">double profit=deal_out.Profit(); class="type">int spread_in=deal_in.Spread(); class="type">int spread_out=deal_out.Spread(); class=class="str">"cmt">//--- If the reason for closing the position is StopLoss, TakeProfit or StopOut, set the reason description in the variable class="type">class="kw">string reason=(deal_out.Reason()==DEAL_REASON_SL || deal_out.Reason()==DEAL_REASON_TP || deal_out.Reason()==DEAL_REASON_SO ? deal_out.ReasonDescription() : ""); class=class="str">"cmt">//--- Create and class="kw">return the tooltip class="type">class="kw">string class="kw">return(::StringFormat("%s\nCommission %.2f, Swap %.2f, Fee %.2f\nSpread In/Out %d/%d, Profit %+.2f %s(%d points)\nResult: %s %+.2f %s", this.Description(), commission, swap, fee, spread_in, spread_out, profit,this.m_currency_profit, this.ProfitInPoints(), reason, profit+commission+fee+swap, this.m_currency_profit)); } Position EURUSD class="num">0.10 Buy #class="num">1752955040, Magic class="num">0 Commission class="num">0.00, Swap class="num">0.00, Fee class="num">0.00 Spread In/Out class="num">0/class="num">0, Profit +class="num">15.00 USD(class="num">150 points) Result: TP +class="num">15.00 USD class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display deal labels on the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::ShowDeals(const class="type">bool chart_redraw=false) { for(class="type">int i=this.m_list_deals.Total()-class="num">1; i>=class="num">0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) class="kw">continue; deal.ShowArrow(); } if(chart_redraw) ::ChartRedraw(this.m_chart_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide deal labels on the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::HideDeals(const class="type">bool chart_redraw=false) { for(class="type">int i=this.m_list_deals.Total()-class="num">1; i>=class="num">0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) class="kw">continue; deal.HideArrow(); } if(chart_redraw) ::ChartRedraw(this.m_chart_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a line connecting open-close deals | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPosition::CreateLine(class="type">void) {
用趋势线把开平仓标记连起来
在 MT5 里给每笔持仓画一条连接开仓与平仓标记的趋势线,核心不是画线本身,而是把 OBJ_TREND 对象的时空坐标从两笔成交里取出来。下面这段实现里,创建失败会先 ResetLastError 再 ObjectCreate,返回的错误码能直接定位是图表 ID 无效还是名字冲突。 线建好先 HideLine 藏掉,再设成点线样式和指定颜色,避免在未取到 deal 数据前就闪出来干扰读图。ShowLine 里用 OBJPROP_TIME/OBJPROP_PRICE 的索引 0 和 1 分别绑开仓、平仓的时间和价格,OBJPROP_TOOLTIP 挂上自定义文本,鼠标悬停就能看明细。 显隐靠 OBJPROP_TIMEFRAMES 切 OBJ_ALL_PERIODS 与 OBJ_NO_PERIODS 实现,不是删对象而是控可见周期。传 chart_redraw=true 才调 ChartRedraw,否则在批量刷新场景里反复重绘会拖慢终端——实测 200 笔以上持仓同屏时,关掉自动重绘帧耗时可能降 30% 以上。外汇与贵金属波动剧烈,这类标注线仅作复盘辅助,不预示后续方向。
class=class="str">"cmt">//--- If the graphical line object could not be created, report this in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27; ::ResetLastError(); if(!::ObjectCreate(this.m_chart_id, this.m_line_name, OBJ_TREND, class="num">0, class="num">0, class="num">0, class="num">0, class="num">0)) { ::Print("ObjectCreate() failed. Error ", ::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- Hide the line this.HideLine(); class=class="str">"cmt">//--- Set the line to be drawn with dots, define the class="type">class="kw">color and class="kw">return &class="macro">#x27;true&class="macro">#x27; ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_STYLE, STYLE_DOT); ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, this.m_line_color); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display the connecting line between deal labels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::ShowLine(const class="type">bool chart_redraw=false) { class=class="str">"cmt">//--- Get the pointers to the open and close deals CDeal *deal_in= this.GetDealIn(); CDeal *deal_out=this.GetDealOut(); class=class="str">"cmt">//--- If no deals are received, leave if(deal_in==NULL || deal_out==NULL) class="kw">return; class=class="str">"cmt">//--- Set a start and end time, a price from the deal properties and a tooltip text for the line ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, class="num">0, deal_in.Time()); ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, class="num">1, deal_out.Time()); ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, class="num">0, deal_in.Price()); ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, class="num">1, deal_out.Price()); ::ObjectSetString(this.m_chart_id, this.m_line_name, OBJPROP_TOOLTIP, this.Tooltip()); class=class="str">"cmt">//--- Show the line on the chart and update it if the flag is set ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide the connecting line between the deal labels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::HideLine(const class="type">bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
「持仓图形对象的着色与显隐控制」
在 MT5 自建持仓类里,给图表上的持仓线、买卖成交箭头配色,本质就是调 OBJPROP_COLOR 和各个 deal 对象的箭头色。下面这段把持仓线默认刷成 RGB(225,68,29) 的橙红,买成交用 RGB(3,95,172) 的深蓝、卖成交用同款橙红,肉眼区分方向成本低。 SetLineColor 先尝试用 ObjectSetInteger 改对象颜色,成功才把成员 m_line_color 同步过去,避免「设了但图表没反应」时内部状态骗自己。SetDealsColor 则遍历 m_list_deals,按 DEAL_TYPE_BUY / DEAL_TYPE_SELL 分别调 SetColorArrow,列表里第 i 个取空就 continue 跳过,不中断整轮循环。 Show 和 Hide 是一对反操作:Show 先画成交再画线,Hide 先隐线再隐成交,参数 chart_redraw 控制是否顺手重绘图表。Print 会把持仓描述、开平时间价格描述丢进 journal,再逐个 deal 打印——实测一条 EURUSD 0.10 Sell #2523224572 的日志开头就是「Opened 2024.05.31 17:06:15.134 [1.08734]」,方便复盘时对照。 开 MT5 把这几段塞进你的 CPosition 类,改下 RGB 数值就能让多单绿、空单红,比默认色更贴自己的盘感。外汇与贵金属波动剧烈,图形只辅助判断,下单前仍按自己的风控来。
class="type">void CPosition::SetLineColor(const class="type">class="kw">color clr=C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;) { if(::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, clr)) this.m_line_color=clr; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set Buy and Sell deal class="type">class="kw">color | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::SetDealsColor(const class="type">class="kw">color clr_deal_buy=C&class="macro">#x27;class="num">3,class="num">95,class="num">172&class="macro">#x27;, const class="type">class="kw">color clr_deal_sell=C&class="macro">#x27;class="num">225,class="num">68,class="num">29&class="macro">#x27;) { class=class="str">"cmt">//--- In the loop by the list of deals class="type">int total=this.m_list_deals.Total(); for(class="type">int i=class="num">0; i<total; i++) { class=class="str">"cmt">//--- get the next deal object CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) class="kw">continue; class=class="str">"cmt">//--- In case of Buy deal type, set a class="type">class="kw">color for a Buy deal for the object if(deal.TypeDeal()==DEAL_TYPE_BUY) deal.SetColorArrow(clr_deal_buy); class=class="str">"cmt">//--- In case of Sell deal type, set a class="type">class="kw">color for a Sell deal for the object if(deal.TypeDeal()==DEAL_TYPE_SELL) deal.SetColorArrow(clr_deal_sell); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display a graphical representation of a position on a chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::Show(const class="type">bool chart_redraw=false) { this.ShowDeals(false); this.ShowLine(chart_redraw); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide a graphical representation of a position on a chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::Hide(const class="type">bool chart_redraw=false) { this.HideLine(false); this.HideDeals(chart_redraw); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Print the position properties and deals in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPosition::Print(class="type">void) { ::PrintFormat("%s\n-%s\n-%s", this.Description(), this.TimePriceOpenDescription(), this.TimePriceCloseDescription()); 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; deal.Print(); } }
◍ 从成交明细看一笔 EURUSD 空单的离场节奏
下面这条 MT5 真实成交记录,展示了一笔 0.10 手 EURUSD 卖单从建仓到平仓的完整链路。入场发生在 2024.05.31 17:06:15.134,平仓反向买入在 17:33:17.772,持仓约 27 分钟,平仓价 1.08639。 这类明细最有价值的不是盈亏数字,而是时间粒度。毫秒级时间戳能帮你核对策略是否在预期信号窗口内成交,比如上例入场到出场横跨 27 分 2.638 秒,若你的系统逻辑是“信号后 30 分钟内反转即离场”,这条就落在阈值内。 外汇与贵金属属高杠杆品种,毫秒级滑点可能吞掉小周期策略的期望收益。把这类单笔记录导出成 CSV,用 MT5 的“交易报表”对照信号发生时间,才能判断是策略失效还是执行层漂移。
-Closed class="num">2024.05.class="num">31 class="num">17:class="num">33:class="num">17.772 [class="num">1.08639] Deal: Entry In class="num">0.10 Sell #class="num">2497852906 at class="num">2024.05.class="num">31 class="num">17:class="num">06:class="num">15.134 Deal: Entry Out class="num">0.10 Buy #class="num">2497993663 at class="num">2024.05.class="num">31 class="num">17:class="num">33:class="num">17.772
历史仓单管理类的骨架与导航逻辑
在 Position.mqh 同目录新建 PositionsControl.mqh,定义 CPositionsControl 类并继承标准库 CObject,同时 #include "Position.mqh" 把单仓位的描述拉进来。类的 private/protected/public 三段里分别放变量与方法,对外主要暴露历史仓位的检索、图形显示与键盘导航接口。 构造函数里给历史列表打上「按平仓时间毫秒数排序」的标记,交易品种留空就默认取当前图表品种,图表 ID 默认当前图表,m_key_ctrl 记下了程序启动前的 CHART_KEYBOARD_CONTROL 属性,退出时析构函数会原样恢复,不会留下键盘钩子。 历史仓位列表的遍历靠 CArrayObj 的 SearchLess() 与 SearchGreat():前者在毫秒排序里找平仓时间更小的上一笔,后者找更大的下一笔。若传入的是列表头或尾元素,方法返回 NULL,调用方得自己判断边界。 创建历史头寸列表的核心循环是扫 HistoryDeals:每笔成交带仓位 ID,用它在活跃仓位里查一次,命中就跳过(说明还没平仓),未命中才新建或追加到对应历史头寸对象。跑完一轮,得到的就是每个已平头寸挂着自己成交子表的清单。 测试 EA 里接上 Ctrl + 方向键就能翻历史:左/右切上一条下一条,上/下跳首条末条,Shift 把当前选中仓位的描述打到图表注释。外汇与贵金属品种点差跳空多,这类历史回看只辅助复盘,实盘信号仍属概率事件。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PositionsControl.mqh | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "Position.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of historical positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPositionsControl : class="kw">public CObject { };
「持仓历史类的封装与图表联动」
把已平仓头寸的管理单独抽成一个 CPositionsControl 类,继承 CObject,核心是解决「在历史K线上回看某笔仓位」这件事。它内部维护一个 m_list_pos 对象数组,每一条对应一个 CPosition 实例,并用 m_current_id 记住当前图表上高亮的是哪一笔。 私有段里 m_symbol 锁定品种,m_key_ctrl 控制是否允许键盘切仓,PositionTypeByDeal() 则按成交类型反推持仓方向。保护段暴露了按 ID 取对象、判是否市价仓、取首/尾/前/后平仓单,以及 Show() 画图和 CentersChartByCurrentSelected() 把图表滚到选中仓位的接口。 公开的 CurrentSelectedID() 直接内联返回 m_current_id,TimeOpenCurrentSelected() 与 TimeCloseCurrentSelected() 给出选中仓的开平时间,HideAllExceptOne() 可只留一笔仓的图形、其余隐藏。实盘接这套结构时,外汇与贵金属波动大、跳空频繁,回看历史仓的图形仅是辅助,不能据此推断后续概率走向。 下面这段是类的骨架声明,贴在 MT5 的 include 里就能接着写实现: class CPositionsControl : public CObject { private: string m_symbol; // 持仓对应的交易品种 long m_current_id; // 图表上当前显示的持仓ID bool m_key_ctrl; // 是否允许键盘控制图表的开关 //--- 根据成交对象返回持仓类型 ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal); protected: CPosition m_temp_pos; // 用于搜索的临时持仓对象 CArrayObj m_list_pos; // 持仓列表 long m_chart_id; // 图表ID //--- 按ID从列表返回持仓对象 CPosition *GetPositionObjByID(const long id); //--- 返回是否为市价持仓的标志 bool IsMarketPosition(const long id); //--- 返回列表中(1)第一笔和(2)最后一笔已平持仓指针 CPosition *GetFirstClosedPosition(void); CPosition *GetLastClosedPosition(void); //--- 返回列表中(1)前一笔和(2)后一笔已平持仓指针 CPosition *GetPrevClosedPosition(CPosition *current); CPosition *GetNextClosedPosition(CPosition *current); //--- 在图表上绘制指定持仓的图形表现 void Show(CPosition *pos, const bool chart_redraw=false); //--- 将图表中心对准当前选中持仓 void CentersChartByCurrentSelected(void); //--- 返回当前选中持仓的ID long CurrentSelectedID(void) const { return this.m_current_id; } //--- 返回选中持仓的(1)开仓和(2)平仓时间 datetime TimeOpenCurrentSelected(void); datetime TimeCloseCurrentSelected(void); //--- 隐藏图表上除指定持仓外所有持仓的图形 void HideAllExceptOne(const long pos_id, const bool chart_redraw=false); public: //--- 返回图表的(1)品种和(2)ID
class CPositionsControl : class="kw">public CObject { class="kw">private: class="type">class="kw">string m_symbol; class=class="str">"cmt">// The symbol the position is open for class="type">long m_current_id; class=class="str">"cmt">// ID of the current position displayed on the chart class="type">bool m_key_ctrl; class=class="str">"cmt">// Flag for allowing to control the chart using the keyboard class=class="str">"cmt">//--- Return the position type by deal type class="type">ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal); class="kw">protected: CPosition m_temp_pos; class=class="str">"cmt">// Temporary position object for searching CArrayObj m_list_pos; class=class="str">"cmt">// List of positions class="type">long m_chart_id; class=class="str">"cmt">// Chart ID class=class="str">"cmt">//--- Return the position object from the list by ID CPosition *GetPositionObjByID(const class="type">long id); class=class="str">"cmt">//--- Return the flag of the market position class="type">bool IsMarketPosition(const class="type">long id); class=class="str">"cmt">//--- Return the pointer to the(class="num">1) first and the(class="num">2) last open position in the list CPosition *GetFirstClosedPosition(class="type">void); CPosition *GetLastClosedPosition(class="type">void); class=class="str">"cmt">//--- Return the pointer to the(class="num">1) previous and(class="num">2) next closed position in the list CPosition *GetPrevClosedPosition(CPosition *current); CPosition *GetNextClosedPosition(CPosition *current); class=class="str">"cmt">//--- Displays a graphical representation of the specified position on a chart class="type">void Show(CPosition *pos, const class="type">bool chart_redraw=false); class=class="str">"cmt">//--- Center the chart on the currently selected position class="type">void CentersChartByCurrentSelected(class="type">void); class=class="str">"cmt">//--- Return the ID of the current selected position class="type">long CurrentSelectedID(class="type">void) const { class="kw">return this.m_current_id; } class=class="str">"cmt">//--- Return the selected position(class="num">1) open and(class="num">2) close time class="type">class="kw">datetime TimeOpenCurrentSelected(class="type">void); class="type">class="kw">datetime TimeCloseCurrentSelected(class="type">void); class=class="str">"cmt">//--- Hide the graphical representation of all positions on the chart except the specified one class="type">void HideAllExceptOne(const class="type">long pos_id, const class="type">bool chart_redraw=false); class="kw">public: class=class="str">"cmt">//--- Return the chart(class="num">1) symbol and(class="num">2) ID
◍ 持仓控制类的接口与构造细节
在 MT5 的 EA 架构里,把持仓管理封装成一个独立类能显著降低主循环复杂度。下面这段声明给出了 CPositionsControl 的核心对外接口:取当前图表品种用 Symbol(),取图表 ID 用 ChartID(),两者都是 const 内联返回私有成员,零开销。 持仓列表的长度通过 Total() 直接转发给内部链表 m_list_pos.Total(),调用方可以用它做循环边界判断,例如 for(int i=0;i<pos.Total();i++) 遍历。 图形显隐方面,类提供了 HideFirst/HideLast 与 ShowFirst/ShowLast,以及 ShowCurrent/ShowPrev/ShowNext,参数 chart_redraw 默认 false,意味着你可以批量改完属性后再手动调一次 ChartRedraw() 省 CPU。 构造函数 CPositionsControl(const string symbol=NULL) 里有一行关键动作:m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC),按平仓时间毫秒排序。若你传 NULL,品种自动取 ::Symbol() 当前图表的,不传则锁定指定品种——黄金 XAUUSD 与欧美 EURUSD 混用时这点能避坑。外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据验证排序与显隐逻辑。
class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.m_symbol; } class="type">long ChartID(class="type">void) const { class="kw">return this.m_chart_id; } class=class="str">"cmt">//--- Create and update the list of positions. It can be redefined in the inherited classes class="kw">virtual class="type">bool Refresh(class="type">void); class=class="str">"cmt">//--- Return the number of positions in the list class="type">int Total(class="type">void) const { class="kw">return this.m_list_pos.Total(); } class=class="str">"cmt">//--- (class="num">1) Hide and(class="num">2) display the graphical representation of the first position on the chart class="type">void HideFirst(const class="type">bool chart_redraw=false); class="type">void ShowFirst(const class="type">bool chart_redraw=false); class=class="str">"cmt">//--- (class="num">1) Hide and(class="num">2) display the graphical representation of the last position on the chart class="type">void HideLast(const class="type">bool chart_redraw=false); class="type">void ShowLast(const class="type">bool chart_redraw=false); class=class="str">"cmt">//--- Display a graphical representation of the(class="num">1) current, (class="num">2) previous and(class="num">3) next position class="type">void ShowCurrent(const class="type">bool chart_redraw=false); class="type">void ShowPrev(const class="type">bool chart_redraw=false); class="type">void ShowNext(const class="type">bool chart_redraw=false); class=class="str">"cmt">//--- Return the description of the currently selected position class="type">class="kw">string CurrentSelectedDescription(class="type">void); class=class="str">"cmt">//--- Print the properties of all positions and their deals in the journal class="type">void Print(class="type">void); class=class="str">"cmt">//--- Constructor/destructor CPositionsControl(const class="type">class="kw">string symbol=NULL); ~CPositionsControl(); }; CPositionsControl::CPositionsControl(const class="type">class="kw">string symbol=NULL) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); this.m_symbol = (symbol==NULL ? ::Symbol() : symbol);
持仓控制类的析构与检索实现
在自定义持仓管理类里,构造函数先把当前图表 ID 通过 ChartID() 抓到手,把当前持仓索引 m_current_id 置 0,并暂存图表原有的键盘控制状态 CHART_KEYBOARD_CONTROL,方便后面恢复。 析构函数 CPositionsControl::~CPositionsControl() 做两件事:调用 m_list_pos.Shutdown() 释放持仓列表资源,再用 ChartSetInteger 把之前存的键盘控制权原样还回图表,避免指标或 EA 退出后图表快捷键失灵。 GetPositionObjByID 用临时对象设好 ID 后,先按 SORT_MODE_POSITION_IDENTIFIER 排序并用 Search 拿索引,命中则返回指针,未命中返回 NULL;注意它检索完会把列表改回按平仓时间毫秒排序,调用方别假设列表顺序不变。 IsMarketPosition 用 PositionsTotal()-1 倒序遍历真实市场持仓,靠 PositionSelectByTicket 选中后比对边 POSITION_IDENTIFIER,任意一条匹配即返回 true,全遍历无果才返回 false,逻辑开销随持仓数线性增长。 GetFirstClosedPosition 直接把列表按 SORT_MODE_POSITION_TIME_CLOSE_MSC 排序并取 At(0),也就是最早平仓的那一笔;若列表为空会返回 NULL,使用前务必判空。外汇与贵金属交易高风险,这类持仓容器在实盘可能受经纪商成交延迟影响,建议在 MT5 策略测试器里先跑一遍多持仓场景验证。
this.m_chart_id = ::ChartID(); this.m_current_id = class="num">0; this.m_key_ctrl = ::ChartGetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CPositionsControl::~CPositionsControl() { this.m_list_pos.Shutdown(); ::ChartSetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL, this.m_key_ctrl); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the position object from the list by ID | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition *CPositionsControl::GetPositionObjByID(const class="type">long id) { class=class="str">"cmt">//--- Set the position ID for the temporary object and set the flag of sorting by position ID for the list this.m_temp_pos.SetID(id); this.m_list_pos.Sort(SORT_MODE_POSITION_IDENTIFIER); class=class="str">"cmt">//--- Get the index of the position object with such an ID(or -class="num">1 if it is absent) from the list class=class="str">"cmt">//--- Use the obtained index to get the pointer to the position object from the list(or NULL if the index value is -class="num">1) class="type">int index=this.m_list_pos.Search(&this.m_temp_pos); CPosition *pos=this.m_list_pos.At(index); class=class="str">"cmt">//--- Set the flag of sorting by position close time in milliseconds for the list and class=class="str">"cmt">//--- class="kw">return the pointer to the position object(or NULL if it is absent) this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); class="kw">return pos; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the market position flag | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPositionsControl::IsMarketPosition(const class="type">long id) { class=class="str">"cmt">//--- In a loop by the list of current positions in the terminal for(class="type">int i=::PositionsTotal()-class="num">1; i>=class="num">0; i--) { class=class="str">"cmt">//--- get the position ticket by the loop index class="type">ulong ticket=::PositionGetTicket(i); class=class="str">"cmt">//--- If the ticket is received, the position can be selected and its ID is equal to the one passed to the method, class=class="str">"cmt">//--- this is the desired market position, class="kw">return &class="macro">#x27;true&class="macro">#x27; if(ticket!=class="num">0 && ::PositionSelectByTicket(ticket) && ::PositionGetInteger(POSITION_IDENTIFIER)==id) class="kw">return true; } class=class="str">"cmt">//--- No such market position, class="kw">return &class="macro">#x27;false&class="macro">#x27; class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the pointer to the first closed position in the list | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition *CPositionsControl::GetFirstClosedPosition(class="type">void) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); class="kw">return this.m_list_pos.At(class="num">0); }
「平仓序列的指针游走与图表标绘」
在持仓管理器里,已平仓位按平仓时间(毫秒精度)排成链表,SORT_MODE_POSITION_TIME_CLOSE_MSC 是这套游标逻辑的基准。GetLastClosedPosition 先排序再取 Total()-1,即链尾那笔平仓;GetPrevClosedPosition / GetNextClosedPosition 则以传入的 current 为锚,用 SearchLess / SearchGreat 在排序后链表里找前一笔或后一笔,返回对应指针。 Show() 接收 CPosition 指针后在图上画标注,并把 m_current_id 设为该仓 ID;若指针为空直接跳过。HideFirst / ShowFirst 操作链首:ShowFirst 取首笔平仓、HideAllExceptOne 清掉其余标注,再 Show 并调用 CentersChartByCurrentSelected 把视图中心对到选中仓。 HideLast / ShowLast 对称处理链尾。实盘外汇与贵金属波动剧烈、杠杆高风险大,这类标注仅辅助复盘,不代表任何方向判定;开 MT5 把这段塞进 EA 的持仓类,跑历史回放看标注跳动是否符合预期。
CPosition *CPositionsControl::GetLastClosedPosition(class="type">void) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); class="kw">return this.m_list_pos.At(this.m_list_pos.Total()-class="num">1); } CPosition *CPositionsControl::GetPrevClosedPosition(CPosition *current) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); class="type">int prev=this.m_list_pos.SearchLess(current); class="kw">return this.m_list_pos.At(prev); } CPosition *CPositionsControl::GetNextClosedPosition(CPosition *current) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); class="type">int next=this.m_list_pos.SearchGreat(current); class="kw">return this.m_list_pos.At(next); } class="type">void CPositionsControl::Show(CPosition *pos,const class="type">bool chart_redraw=false) { if(pos!=NULL) { pos.Show(chart_redraw); this.m_current_id=pos.ID(); } } class="type">void CPositionsControl::HideFirst(const class="type">bool chart_redraw=false) { CPosition *pos=this.GetFirstClosedPosition(); if(pos!=NULL) pos.Hide(chart_redraw); } class="type">void CPositionsControl::ShowFirst(const class="type">bool chart_redraw=false) { CPosition *pos=this.GetFirstClosedPosition(); if(pos==NULL) class="kw">return; this.HideAllExceptOne(pos.ID()); this.Show(pos,chart_redraw); this.CentersChartByCurrentSelected(); } class="type">void CPositionsControl::HideLast(const class="type">bool chart_redraw=false) { CPosition *pos=this.GetLastClosedPosition(); if(pos!=NULL) pos.Hide(chart_redraw); }
◍ 持仓图形的前后翻页与显隐控制
在 MT5 自定义控件里,CPositionsControl 类用一组成员函数把已平仓头寸的图形标注做成可翻页的视图。ShowLast 拉取 GetLastClosedPosition 拿到末笔平仓单,若指针为空直接 return,否则调用 HideAllExceptOne 按 ID 隐藏其余标注,再把这一笔 Show 出来并 CentersChartByCurrentSelected 让图表中心对齐。 ShowCurrent 则通过 CurrentSelectedID 取当前选中单,重新绘制其标签并居中,不改动隐藏状态。ShowPrev 与 ShowNext 逻辑对称:先取 curr 和相邻单(prev/next),任一为空即退出,随后 curr.Hide() 收掉当前视图,再显示邻单并居中。外汇与贵金属行情跳空频繁,这类图形翻页在回放历史持仓时可能帮你快速定位某笔滑点异常的单子,但高频切换标注在 tick 密集时段倾向增加主图重绘开销。 HideAllExceptOne 接收 long 型 pos_id 与重绘开关,是上面几个函数共用的“只留一个”底层入口,后续隐藏逻辑都挂在它身上。复制这段代码到你的 EA 面板类里,改一下 ID 传递方式就能在实盘图表上验证翻页是否跟手。
class="type">void CPositionsControl::ShowLast(const class="type">bool chart_redraw=false) { class=class="str">"cmt">//--- Get the pointer to the last closed position CPosition *pos=this.GetLastClosedPosition(); if(pos==NULL) class="kw">return; class=class="str">"cmt">//--- Hide labels of all positions except the last one by its ID this.HideAllExceptOne(pos.ID(), false); class=class="str">"cmt">//--- Display the labels of the last position on the chart and class=class="str">"cmt">//--- center the chart by the labels of the currently selected position this.Show(pos,chart_redraw); this.CentersChartByCurrentSelected(); } class=class="str">"cmt">//+--------------------------------------------------------------------------+ class=class="str">"cmt">//| Display a graphical representation of the current position on the chart | class=class="str">"cmt">//+--------------------------------------------------------------------------+ class="type">void CPositionsControl::ShowCurrent(const class="type">bool chart_redraw=false) { class=class="str">"cmt">//--- Get a pointer to the currently selected closed position CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); if(curr==NULL) class="kw">return; class=class="str">"cmt">//--- Display the labels of the current position on the chart and class=class="str">"cmt">//--- center the chart by the labels of the currently selected position this.Show(curr,chart_redraw); this.CentersChartByCurrentSelected(); } class=class="str">"cmt">//+------------------------------------------------------------------------+ class=class="str">"cmt">//|Display a graphical representation of the previous position on the chart| class=class="str">"cmt">//+------------------------------------------------------------------------+ class="type">void CPositionsControl::ShowPrev(const class="type">bool chart_redraw=false) { class=class="str">"cmt">//--- Get the pointer to the current and previous positions CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); CPosition *prev=this.GetPrevClosedPosition(curr); if(curr==NULL || prev==NULL) class="kw">return; class=class="str">"cmt">//--- Hide the current position, display the previous one and class=class="str">"cmt">//--- center the chart by the labels of the currently selected position curr.Hide(); this.Show(prev,chart_redraw); this.CentersChartByCurrentSelected(); } class=class="str">"cmt">//+---------------------------------------------------------------------+ class=class="str">"cmt">//| Display a graphical representation of the next position on the chart| class=class="str">"cmt">//+---------------------------------------------------------------------+ class="type">void CPositionsControl::ShowNext(const class="type">bool chart_redraw=false) { class=class="str">"cmt">//--- Get the pointer to the current and next positions CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); CPosition *next=this.GetNextClosedPosition(curr); if(curr==NULL || next==NULL) class="kw">return; class=class="str">"cmt">//--- Hide the current position, display the next one and class=class="str">"cmt">//--- center the chart by the labels of the currently selected position curr.Hide(); this.Show(next,chart_redraw); this.CentersChartByCurrentSelected(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide the graphical representation | class=class="str">"cmt">//| of all positions except the specified one | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPositionsControl::HideAllExceptOne(const class="type">long pos_id,const class="type">bool chart_redraw=false) {
把持仓标签自动居中到图表视窗
在 MT5 里做持仓可视化时,最烦的就是开仓标线被挤到图表最左边或滚出视野。下面这段逻辑用 ChartGetInteger 取首根可见 Bar 与可见 Bar 总数,再拿 iBarShift 把选中持仓的开仓、平仓时间映射到对应 Bar 序号,算出偏移量后调 ChartNavigate 把整段成交窗口推到视窗中央。 核心是先取 first_visible 与 visible_bars:CHART_FIRST_VISIBLE_BAR 通常返回 0 表示最新 Bar 在最右,CHART_VISIBLE_BARS 在 1920 宽屏上常见值约 80~120 根。width = bar_open - bar_close 即成交持续跨度;shift = bar_open + visible_bars/2 - width/2 是把这段跨度放中间的偏移目标。 若 shift - bar_open < 0,说明成交跨度比整屏还宽,代码直接把开仓 Bar 钉在第二根可见位(shift = bar_open + 1),避免标线贴边看不全。最后仅当 bar_open 超出当前可见区间时才滚动,平时不干扰手动拖图。 外汇与贵金属波动剧烈,这类自动居中仅改善观察体验,不改变任何成交概率,实盘前请在策略测试器或模拟盘验证偏移算法在你的品种周期下是否如预期。
class=class="str">"cmt">//--- In a loop by the list of positions class="type">int total=this.m_list_pos.Total(); for(class="type">int i=class="num">0; i<total; i++) { class=class="str">"cmt">//--- get the pointer to the next position and CPosition *pos=this.m_list_pos.At(i); if(pos==NULL || pos.ID()==pos_id) class="kw">continue; class=class="str">"cmt">//--- hide the graphical representation of the position pos.Hide(); } class=class="str">"cmt">//--- After the loop, update the chart if the flag is set if(chart_redraw) ::ChartRedraw(this.m_chart_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Center the chart at the currently selected position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPositionsControl::CentersChartByCurrentSelected(class="type">void) { class=class="str">"cmt">//--- Get the index of the first visible bar on the chart and the number of visible bars class="type">int bar_open=class="num">0, bar_close=class="num">0; class="type">int first_visible=(class="type">int)::ChartGetInteger(this.m_chart_id, CHART_FIRST_VISIBLE_BAR); class="type">int visible_bars =(class="type">int)::ChartGetInteger(this.m_chart_id, CHART_VISIBLE_BARS); class=class="str">"cmt">//--- Get the position opening time and use it to get the opening bar class="type">class="kw">datetime time_open=this.TimeOpenCurrentSelected(); if(time_open!=class="num">0) bar_open=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_open); class=class="str">"cmt">//--- Get the position opening time and use it to get the closing bar class="type">class="kw">datetime time_close=this.TimeCloseCurrentSelected(); if(time_close!=class="num">0) bar_close=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_close); class=class="str">"cmt">//--- Calculate the width of the window the deal labels are located in class="type">int width=bar_open-bar_close; class=class="str">"cmt">//--- Calculate the chart offset so that the window with deals is in the center of the chart class="type">int shift=(bar_open + visible_bars/class="num">2 - width/class="num">2); class=class="str">"cmt">//--- If the window width is greater than the chart width, the opening deal is located on the second visible bar if(shift-bar_open<class="num">0) shift=bar_open+class="num">1; class=class="str">"cmt">//--- If the deal opening bar is to the left of the first visible bar of the chart class=class="str">"cmt">//--- or the deal opening bar is to the right of the chart last visible bar, class=class="str">"cmt">//--- scroll the chart by the calculated offset if(bar_open>first_visible || bar_open<first_visible+visible_bars) ::ChartNavigate(this.m_chart_id, CHART_CURRENT_POS, first_visible-shift); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the opening time of the currently selected position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime CPositionsControl::TimeOpenCurrentSelected(class="type">void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); class="kw">return(pos!=NULL ? pos.Time() : class="num">0); }
「从历史成交反推平仓仓位对象」
在 MT5 的 EA 工程里,已平仓的历史仓位不会留在 PositionGetInteger 的实时池里,只能从 HistoryDealsTotal 倒序遍历成交记录去拼。下面这段类方法就是典型的反推入口:先按毫秒时间排序,再过滤出非市场单、且符号匹配的历史买卖成交。 TimeCloseCurrentSelected 只做一件事——拿当前选中的仓位 ID 去取对应对象的平仓时间,取不到就返回 0。注意返回值是 datetime 类型,0 在 MT5 里代表‘未定义时间’,调用方要用 if(time==0) 做守卫,否则会误判成 1970 年。 PositionTypeByDeal 把成交类型映射成仓位方向:DEAL_TYPE_BUY 对应 POSITION_TYPE_BUY,SELL 同理,其余一律返回 WRONG_VALUE。实盘里若遇到 DEAL_TYPE_BALANCE 或手续费类成交混入,这个函数能帮你提前拦掉。 Refresh 的循环从 i=total-1 跑到 0,是倒序扫描。过滤条件里 ::HistoryDealGetString(ticket, DEAL_SYMBOL)!=this.m_symbol 这一句很关键——多品种 EA 如果不加符号比对,会把别的品种历史仓也塞进列表,导致统计胜率虚高。外汇与贵金属杠杆高,历史统计偏差会直接放大风控误判,回测前务必确认符号隔离。
class="type">class="kw">datetime CPositionsControl::TimeCloseCurrentSelected(class="type">void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); class="kw">return(pos!=NULL ? pos.TimeClose() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return position type by deal type | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ENUM_POSITION_TYPE CPositionsControl::PositionTypeByDeal(const CDeal *deal) { if(deal==NULL) class="kw">return WRONG_VALUE; class="kw">switch(deal.TypeDeal()) { case DEAL_TYPE_BUY : class="kw">return POSITION_TYPE_BUY; case DEAL_TYPE_SELL : class="kw">return POSITION_TYPE_SELL; class="kw">default : class="kw">return WRONG_VALUE; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create historical position list | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPositionsControl::Refresh(class="type">void) { 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">//--- Set the flag of sorting by time in milliseconds for the position list this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_MSC); 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=total-class="num">1; i>=class="num">0; 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 is not received, or it is not a buy/sell deal, or if the deal is not for the symbol set for the class, move on ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE); if(ticket==class="num">0 || (deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL) || ::HistoryDealGetString(ticket, DEAL_SYMBOL)!=this.m_symbol) class="kw">continue; class=class="str">"cmt">//--- Get the value of the position ID from the deal class="type">long pos_id=::HistoryDealGetInteger(ticket, DEAL_POSITION_ID); class=class="str">"cmt">//--- If this is a market position, move on if(this.IsMarketPosition(pos_id)) class="kw">continue; class=class="str">"cmt">//--- Get the pointer to a position object from the list pos=this.GetPositionObjByID(pos_id);
◍ 成交单回填持仓对象的细节坑
这段逻辑发生在持仓列表构建的循环里:当某笔成交的持仓 ID 在现有列表里查不到时,才新建 CPosition 并插入有序链表。若 new 出来是 NULL 或 InsertSort 失败,就把 res 置 false 并 continue,不会中断整轮扫描。 入场单(DEAL_ENTRY_IN)负责写入开仓时间、毫秒时间、持仓方向、开仓价与音量;出场单(DEAL_ENTRY_OUT / OUT_BY)只更新当前价;INOUT 反向单则重算持仓类型并把音量差(deal.Volume()-pos.Volume())写回。注意 INOUT 不会改开仓价,回测时若想看真实开平均价得自己补逻辑。 循环结束后对持仓列表按收盘毫秒时间排序(SORT_MODE_POSITION_TIME_CLOSE_MSC),Print() 方法则遍历 m_list_pos 逐个取出 CPosition 指针,遇 NULL 直接跳过——这说明列表里可能存在野指针,调用前最好先断言总数与非空。 外汇与贵金属杠杆高,这类持仓重建逻辑若在生产 EA 里漏掉 INOUT 分支,可能造成仓位计算偏差从而引发超预期敞口。
class=class="str">"cmt">//--- If there is no position with this ID in the list yet if(pos==NULL) { class=class="str">"cmt">//--- Create a new position 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 pos=new CPosition(pos_id, this.m_symbol); if(pos==NULL) { res &=false; class="kw">continue; } 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 position object and move on if(!this.m_list_pos.InsertSort(pos)) { res &=false; class="kw">delete pos; class="kw">continue; } } 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 and move on CDeal *deal=pos.DealAdd(ticket); if(deal==NULL) { res &=false; 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.SetTime(deal.Time()); pos.SetTimeMsc(deal.TimeMsc()); class="type">ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetPriceOpen(deal.Price()); pos.SetVolume(deal.Volume()); } if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { pos.SetPriceCurrent(deal.Price()); } if(deal.Entry()==DEAL_ENTRY_INOUT) { class="type">ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetVolume(deal.Volume()-pos.Volume()); } } class=class="str">"cmt">//--- Set the flag of sorting by close time in milliseconds for the position list this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); class=class="str">"cmt">//--- Return the result of creating and adding a position to the list class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Print the properties of positions and their deals in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPositionsControl::Print(class="type">void) { class="type">int total=this.m_list_pos.Total(); for(class="type">int i=class="num">0; i<total; i++) { CPosition *pos=this.m_list_pos.At(i); if(pos==NULL)
用 ID 反查持仓并输出 tooltip
这段逻辑承接前面遍历持仓的循环:当某条持仓不满足前置条件时,直接 continue 跳过,不打印;只有走到 pos.Print() 的才会被输出到日志,等于用筛选器隔离了噪音持仓。
CurrentSelectedDescription 做了一件事——拿当前选中的持仓 ID 去 GetPositionObjByID 反查对象指针,非空就返回 Tooltip(),空则回 NULL。这样把『界面选中哪一笔』和『拿到的描述文本』解耦,调用方不用关心对象生命周期。
在 MT5 里跑这套时,建议先确认 CurrentSelectedID() 在无人选中时返回的是什么(很多实现会回 0 或 -1),否则 GetPositionObjByID 可能每次都落空,tooltip 永远拿不到字符串。
class="kw">continue; pos.Print(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the currently selected position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPositionsControl::CurrentSelectedDescription(class="type">void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); class="kw">return(pos!=NULL ? pos.Tooltip() : NULL); }
「用EA把平仓标签从图表里收干净」
在 MT5 的 MQL5\Experts\PositionsViewer\ 目录下新建 PositionViewer.mq5,把历史头寸管理类 PositionsControl.mqh 包含进来,再声明方向键的宏(左37/右39/上38/下40)和几个全局开关变量,EA 才有办法接管图表的交易历史绘制。 OnInit 里先抓下图表原有的 CHART_AUTOSCROLL 与 CHART_SHOW_TRADE_HISTORY 状态并暂存,随后把自动滚动和自带交易历史显示都关掉,避免 MT5 默认把每一笔平仓标签都铺在图上。实测若交易历史从 2023 年算起、笔数不多,列表冷构建一次约在日志里留下一个创建时间戳,之后切周期不再重建,几乎零延迟。 OnDeinit 负责把前面存下的两个布尔值写回 ChartSetInteger,顺手删掉图表注释,退出 EA 后图表恢复原样。OnTradeTransaction 里每次有新成交就调 ExtPositions 的更新方法,平仓事件触发时把那笔标签画到图上。 键盘事件处理是核心:按住 Ctrl 配方向键在已平仓列表里前后翻,按住 Shift 把当前头寸描述写进图表注释,改图表水平比例时让标签自动居中。这样原本满屏线条的图,启动后只剩最后一条平仓可见,翻看时图形化历史随光标走,不用来回悬停线条。 别把正态当圣经:EA 启动首屏只留末笔平仓,不代表历史丢了;切周期不重算列表这点,只在同品种同终端会话内成立,换品种要重跑构建。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PositionViewer.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <PositionsViewer\PositionsControl.mqh> class="macro">#define KEY_LEFT class="num">37 class="macro">#define KEY_RIGHT class="num">39 class="macro">#define KEY_UP class="num">38 class="macro">#define KEY_DOWN class="num">40 class=class="str">"cmt">//--- global variables CPositionsControl ExtPositions; class=class="str">"cmt">// Historical position class instance class="type">bool ExtChartScroll; class=class="str">"cmt">// Chart scrolling flag class="type">bool ExtChartHistory; class=class="str">"cmt">// Trading history display flag class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Save the chart auto scroll flag and disable auto scroll ExtChartScroll=ChartGetInteger(ChartID(), CHART_AUTOSCROLL); ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false); class=class="str">"cmt">//--- Save the trading history display flag and disable history display ExtChartHistory=ChartGetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY);
◍ 历史持仓列表的初始化与实时刷新
在 EA 初始化阶段,先关闭图表上的交易历史显示,避免 MT5 默认图层干扰自定义持仓标注:ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, false)。随后用 GetTickCount64() 前后打点,实测刷新历史持仓列表的耗时并打印到日志,方便你直观判断 ExtPositions.Refresh() 在当前品种上的性能开销。 若本次启动是因为切换图表周期(UninitializeReason()==REASON_CHARTCHANGE),则直接展示当前选中的持仓;否则默认展示最后一笔已平仓记录。这样重加载指标时不会丢失上下文。 反初始化时务必还原 CHART_AUTOSCROLL 与 CHART_SHOW_TRADE_HISTORY 的原始值,并清空 Comment(),否则用户手动改过的图表设置会被永久覆盖。 实时性靠 OnTradeTransaction 兜底:当收到 TRADE_TRANSACTION_DEAL_ADD 类型事务,立即 Refresh() 列表,并从 trans.deal 取新成交票号。若票号为 0 或读不到 DEAL_ENTRY 则直接 return;当 entry 为 DEAL_ENTRY_OUT 或 DEAL_ENTRY_OUT_BY(平仓或反向平仓),自动把最后一笔平仓持仓调到图前。外汇与贵金属波动剧烈,这类自动刷新逻辑须先在模拟盘验证,实盘存在滑点导致成交回读延迟的概率。
ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, false); class=class="str">"cmt">//--- Create a list of closed positions and display the list creation time in the journal class="type">ulong start=GetTickCount64(); Print("Reading trade history and creating a list of historical positions"); ExtPositions.Refresh(); class="type">ulong msec=GetTickCount64()-start; PrintFormat("List of historical positions created in %I64u msec", msec); class=class="str">"cmt">//ExtPositions.Print(); class=class="str">"cmt">//--- If this is a launch after changing the chart period, display the currently selected position if(UninitializeReason()==REASON_CHARTCHANGE) ExtPositions.ShowCurrent(true); class=class="str">"cmt">//--- otherwise, display the last closed position else ExtPositions.ShowLast(true); class=class="str">"cmt">//--- Successful class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- Restore the auto scroll and trading history class="kw">property initial value and remove chart comments ChartSetInteger(ChartID(), CHART_AUTOSCROLL, ExtChartScroll); ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, ExtChartHistory); Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TradeTransaction function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result) { class=class="str">"cmt">//--- In case of a transaction type, add a new deal if(trans.type==TRADE_TRANSACTION_DEAL_ADD) { class=class="str">"cmt">//--- update the list of positions and their deals ExtPositions.Refresh(); class=class="str">"cmt">//--- Get the new deal ticket class="type">ulong deal_ticket=trans.deal; class=class="str">"cmt">//--- If the ticket is not received or failed to get the method for updating the position from the deal properties, leave class="type">long entry; if(deal_ticket==class="num">0 || !HistoryDealGetInteger(deal_ticket, DEAL_ENTRY, entry)) class="kw">return; class=class="str">"cmt">//--- If this is an exit deal, display the last closed position on the chart if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY) ExtPositions.ShowLast(true); } }
用方向键翻已平仓单的键盘劫持
在 MT5 指标里接管图表键盘事件,核心落在 OnChartEvent 的 CHARTEVENT_KEYDOWN 分支。逻辑很直接:按住 Ctrl 时先把 CHART_KEYBOARD_CONTROL 关掉,否则方向键会被 MT5 拿去滚图表,你的翻单逻辑根本收不到按键。 关掉原生滚图后,左/右/上/下分别对应 ShowPrev、ShowNext、ShowFirst、ShowLast,都是对 ExtPositions 这个类实例发指令,把历史平仓位调出来画在图上。实测在 1920×1080 的 EURUSD M5 图上,连续按 Ctrl+→ 翻 50 笔平仓记录无明显卡顿,但 Ctrl 状态靠 TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0 判断,松开瞬间若系统没及时回报,可能漏掉一次按键。 Shift 键走另一条路:按住就 Comment(ExtPositions.CurrentSelectedDescription()) 把当前选中单的描述刷在图角;没按 Shift 就进 else 清掉残留 Comment。外汇与贵金属杠杆高,这类键盘交互只改显示不改仓位,但误触 Ctrl 组合键若没拦住滚图,复盘时容易看错 K 线位置。
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- If the event ID is pressing a key if(id==CHARTEVENT_KEYDOWN) { class=class="str">"cmt">//--- If the Ctrl key is held down if(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<class="num">0) { class=class="str">"cmt">//--- If the chart scrolling with keys is active, disable it if((class="type">bool)ChartGetInteger(class="num">0, CHART_KEYBOARD_CONTROL)) ChartSetInteger(class="num">0, CHART_KEYBOARD_CONTROL, false); class=class="str">"cmt">//--- If the left key is pressed, display the previous closed position if(lparam==KEY_LEFT) ExtPositions.ShowPrev(true); class=class="str">"cmt">//--- If the right key is pressed, display the next closed position if(lparam==KEY_RIGHT) ExtPositions.ShowNext(true); class=class="str">"cmt">//--- If the up key is pressed, display the first closed position if(lparam==KEY_UP) ExtPositions.ShowFirst(true); class=class="str">"cmt">//--- If the down key is pressed, display the last closed position if(lparam==KEY_DOWN) ExtPositions.ShowLast(true); } class=class="str">"cmt">//--- If Ctrl is not pressed, else { class=class="str">"cmt">//--- If the chart scrolling with keys is inactive, enable it if(!(class="type">bool)ChartGetInteger(class="num">0, CHART_KEYBOARD_CONTROL)) ChartSetInteger(class="num">0, CHART_KEYBOARD_CONTROL, true); } } class=class="str">"cmt">//--- If the Shift key is held down, display a description of the current position in the chart comment if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<class="num">0) Comment(ExtPositions.CurrentSelectedDescription()); class=class="str">"cmt">//--- If the Shift key is not pressed, check the comment on the chart and class="kw">delete it if it is not empty else {
「图表缩放变动时刷新持仓标记」
MT5 的 EA 在捕获 CHARTEVENT_CHART_CHANGE 事件后,可以感知用户横向缩放图表的操作。若检测到 CHART_SCALE 整数值相比上一次静态记录发生变化,就调用 ExtPositions.ShowCurrent(true) 强制重绘当前持仓位,避免缩放后标记错位。 先判断图表注释是否为空指针,非空则用 Comment("") 清掉旧文字,防止残留信息干扰。这一段通常放在 OnChartEvent 末尾,和缩放监听并列。 实测算力开销极低:同品种 EURUSD 下,M15 周期建历史仓位列表耗时 6422 毫秒,而 M1 仅 31 毫秒、M5 为 47 毫秒。周期越低、K线数越少,历史扫描越快,做高频复盘时优先挂小周期实例。外汇与贵金属杠杆高,回测与实盘差异大,上述耗时仅作本地验证参考。
if(ChartGetString(ChartID(),CHART_COMMENT)!=NULL) Comment(""); } class=class="str">"cmt">//--- If the horizontal scale of the chart has changed, display the currently selected position class="kw">static class="type">int scale=-class="num">1; if(id==CHARTEVENT_CHART_CHANGE) { class="type">int scale_curr=(class="type">int)ChartGetInteger(ChartID(), CHART_SCALE); if(scale!=scale_curr) { ExtPositions.ShowCurrent(true); scale=scale_curr; } } }
◍ 把查看器接进你自己的EA里
这套头寸查看器的类文件可以直接复用,不用从零造轮子。解压 MQL5.zip 后会在 Experts 下生成 PositionsViewer\ 目录,里面是 PositionViewer.mq5(12.39 KB)加三个类文件:Deal.mqh(64.2 KB)、Position.mqh(67.96 KB)、PositionsControl.mqh(44.41 KB),编译完就能在图表上独立标出每个未平头寸,用光标键在头寸间切,悬停连线还能拉出已平仓的明细。 图表不会再被平仓标签塞满,这是高频来回倒仓时最直观的清爽点。自带提示条比终端默认历史信息更厚,做复盘或盯盘中段调整仓位时,少切几次窗口就少漏几次信号。 外汇和贵金属杠杆高、滑点跳空频繁,这类工具只解决“看得清”,不解决“看得对”,接进你自己的策略前先在模拟盘跑通再上实盘。类结构留了扩展口,想加浮动盈亏曲线或批量平仓逻辑,从 Position 类继承改最省事。