通过推送通知监控交易一个MetaTrader 5服务的示例·综合运用
- 从成交单号回填一笔历史成交的全部字段
- 成交对象里点值与价差怎么落地
- 成交记录的多字段排序分支
- 成交对象的多字段比较与类型描述
- 成交记录里的非交易类枚举别漏看
- 成交记录的来源与属性自描述
- 成交对象的毫秒级回放与tick抓取
- 把单笔历史仓位封装成可遍历对象
- 持仓对象的属性枚举拆解
- 持仓类的属性分区与索引映射
- 持仓对象的属性写入接口
- 给持仓对象塞属性的几个 setter
- 持仓对象的字段写入接口
- 持仓对象的属性读写封装
- 持仓对象的属性读取接口
- 持仓对象的只读属性接口
- 持仓对象的字符串与附加属性读取
- 持仓对象的接口方法拆解
- 持仓对象的构造与比较逻辑
- 持仓对象按属性排序的底层写法
- 持仓对象按属性比大小的实现
- 持仓对象比较的剩余字段分支
- 持仓类里的时间与成交抓取实现
- 持仓对象的成交票号与价差提取
- 平仓点差与手续费怎么从成交单里抠出来
- 持仓对象里的成交归位与描述拼接
- 把持仓属性和成交明细甩进日志
- 按成交与仓位属性做筛选的 CSelect 骨架
- 对象筛选类的头文件骨架
- 用 CSelect 给成交和持仓做筛选排序
- 持仓与成交列表的极值检索和比较内核
- 按成交属性筛单的三个重载入口
- 成交记录里挑最大值的三种重载
- 在成交列表里捞最小属性值的下标
- 按持仓属性筛单的三个重载
- 按属性挑出持仓列表里的最值项
- 持仓列表里捞最值位置的三个重载
- 极值索引的回退写法
- 把历史仓位聚成可检索的集合
- 持仓控制类的检索与判定实现
- 从成交单反推持仓属性
- 从历史成交重建挂单持仓对象
- 平仓与部分平仓的持仓对象回填
- 把账户状态封装成可比较的对象
- 账户结构体的真实字段拆解
- 持仓对象与账户属性的接口封装
- 账户容器暴露的只读接口
- 账户快照的 getter 接口逐个拆
- 账户类的负债与属性读取接口
- 账户对象构造时把关键属性一次性抓进来
- 账户状态快照的字段抓取
- 账户对象的比较与描述输出
- 从账户对象读出交易环境快照
- 账户集合类的骨架与接口
- 多账户持仓容器的内存与检索细节
- 账户容器里取对象与合并持仓的实现细节
- 多账户持仓聚合的拷贝与合并逻辑
- 让报告服务自己盯着 MQID 与通知开关
- 统计模块的预处理与枚举骨架
- 统计枚举与输入参数的落地写法
- 报表推送的开关与周期参数
- 定时推送交易报告的参数骨架
- 给时间结构补一层中文名映射
- 时间字段的增量与越界校准
- 账户监控脚本的状态变量与主循环骨架
- 切换账户与每日报告的触发逻辑
- 按日/天/月/年切分持仓统计推送
- 定时拉平仓单做周月年统计
- 统计推送与资源回收的收尾逻辑
- 弹窗分支里兜底推送通道
- EA 里通知权限与账户切换的实时自检
- 账户刷新轮询与跨天日报触发
- 跨周期平仓统计的取数分支
- 周报触发的账户快照与多周期统计
- 统计区间起点的日期回拨逻辑
- 按周期回溯与账户切换的底层处理
- 按周期把成交统计推到日志和手机
- 按品种与魔术码拆分持仓统计的循环写法
- 统计表头按开关拼列
- 统计报告标题与消息拼装的函数细节
- 持仓组的损益与费用逐项拆解
- 把持仓统计拼成日志与推送两版字符串
- 从持仓列表剥离交易品种与魔术码
- 持仓属性聚合的四种写法
- 回测报告里的周期标签与空仓陷阱
- 按品种与魔术码拆账户明细
- 两年回测的成交明细怎么读
- Ticket 维度拆穿单笔盈亏分布
- Reporter 打印出来的成交快照长什么样
- 从 Reporter 逐笔流水看信号单的真实盈亏分布
- MQID 推送限额与月内持仓分布实况
- 回测报表里的多空分布与盈亏极值
- 把工具请下神坛
「从成交单号回填一笔历史成交的全部字段」
在 MT5 的历史成交回放里,拿到 ticket 后第一件事就是把整笔 deal 的结构体字段一次性补全。下面这段初始化逻辑按整数、双精度、字符串三类属性分组读取,省得你后面反复调 HistoryDealGetInteger 查同一个 ticket。 整数类先用 HistoryDealGetInteger 抓时间、类型、方向、魔法码和持仓 ID:DEAL_TIME 给到秒级,DEAL_TIME_MSC 给到毫秒,做高频复盘时毫秒字段能区分同一秒内的多笔平仓。DEAL_TYPE 与 DEAL_ENTRY 分别映射枚举,用来判断是开仓还是平仓、买还是卖。 双精度类走 HistoryDealGetDouble,Volume、Price、Commission、Swap、Profit 是算净值曲线的硬数据;DEAL_FEE 和 DEAL_SL / DEAL_TP 容易被忽略,但前者在部分券商会单独收,后者能还原当时挂的止损止盈绝对价位。 字符串类只有三个:Symbol、Comment、ExternalID。ExternalID 是外接交易系统的成交号,做跨平台对账时才用得上,平时留空也不影响回测。 外汇与贵金属杠杆高,历史成交里的 Swap 和 Commission 若没逐笔落库,长线仓位成本可能被低估,回测盈利倾向偏乐观。
this.SetTime((class="type">class="kw">datetime)::HistoryDealGetInteger(ticket, DEAL_TIME)); class=class="str">"cmt">// Deal execution time this.SetTimeMsc(::HistoryDealGetInteger(ticket, DEAL_TIME_MSC)); class=class="str">"cmt">// Deal execution time in milliseconds this.SetTypeDeal((ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE)); class=class="str">"cmt">// Type this.SetEntry((ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY)); class=class="str">"cmt">// Direction this.SetMagic(::HistoryDealGetInteger(ticket, DEAL_MAGIC)); class=class="str">"cmt">// Magic number this.SetReason((ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON)); class=class="str">"cmt">// Deal execution reason or source this.SetPositionID(::HistoryDealGetInteger(ticket, DEAL_POSITION_ID)); class=class="str">"cmt">// Position ID class=class="str">"cmt">//--- Real properties this.SetVolume(::HistoryDealGetDouble(ticket, DEAL_VOLUME)); class=class="str">"cmt">// Volume this.SetPrice(::HistoryDealGetDouble(ticket, DEAL_PRICE)); class=class="str">"cmt">// Price this.SetCommission(::HistoryDealGetDouble(ticket, DEAL_COMMISSION)); class=class="str">"cmt">// Commission this.SetSwap(::HistoryDealGetDouble(ticket, DEAL_SWAP)); class=class="str">"cmt">// Accumulated swap when closing this.SetProfit(::HistoryDealGetDouble(ticket, DEAL_PROFIT)); class=class="str">"cmt">// Financial result this.SetFee(::HistoryDealGetDouble(ticket, DEAL_FEE)); class=class="str">"cmt">// Deal fee this.SetSL(::HistoryDealGetDouble(ticket, DEAL_SL)); class=class="str">"cmt">// Stop Loss level this.SetTP(::HistoryDealGetDouble(ticket, DEAL_TP)); class=class="str">"cmt">// Take Profit level class=class="str">"cmt">//--- String properties this.SetSymbol(::HistoryDealGetString(ticket, DEAL_SYMBOL)); class=class="str">"cmt">// Symbol name this.SetComment(::HistoryDealGetString(ticket, DEAL_COMMENT)); class=class="str">"cmt">// Comment this.SetExternalID(::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID)); class=class="str">"cmt">// Deal ID in an external trading system class=class="str">"cmt">//--- Additional parameters
◍ 成交对象里点值与价差怎么落地
在 CDeal 的初始化里,先抓品种的小数位和每点价值:m_digits 用 SymbolInfoInteger 拿 SYMBOL_DIGITS 强转 int,m_point 用 SymbolInfoDouble 取 SYMBOL_POINT。这两值后面算价差点数全靠它,point 为 0 时后面分支直接废掉。 价差默认先置 0,bid/ask 也清 0。如果能拿到历史 tick 且 point 非 0,就用 tick 里的 bid/ask 相减除以 point 取绝对值,强转 int 存进 DEAL_PROP_SPREAD;拿不到 tick 就退而求其次用当根 M1 棒的 GetSpreadM1() 兜底。 Compare 方法按 mode 切换排序键:ticket、order、time、time_msc、type_deal 都能比,统一返回 1 / -1 / 0 三态。写回测或成交分析面板时,这套比较逻辑能直接拿来给成交记录按时间或类型排顺序。 开 MT5 把这段塞进自己的 CDeal 类,跑一单历史成交,打印 DEAL_PROP_SPREAD,看 EURUSD 在 tick 缺失时是否回退到 M1 价差——外汇和贵金属点差跳动快,这种兜底逻辑直接影响你的成本统计精度,相关品种波动高风险也大。
this.m_digits = (class="type">int)::SymbolInfoInteger(this.Symbol(), SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.Symbol(), SYMBOL_POINT); class=class="str">"cmt">//--- Parameters for calculating spread this.m_bid = class="num">0; this.m_ask = class="num">0; this.SetProperty(DEAL_PROP_SPREAD, class="num">0); 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; class="type">int spread=(class="type">int)::fabs((this.m_ask-this.m_bid)/this.m_point); this.SetProperty(DEAL_PROP_SPREAD, spread); } 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.SetProperty(DEAL_PROP_SPREAD, this.GetSpreadM1()); } 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 { const CDeal * obj = node; class="kw">switch(mode) { case DEAL_PROP_TICKET : class="kw">return(this.Ticket() > obj.Ticket() ? class="num">1 : this.Ticket() < obj.Ticket() ? -class="num">1 : class="num">0); case DEAL_PROP_ORDER : class="kw">return(this.Order() > obj.Order() ? class="num">1 : this.Order() < obj.Order() ? -class="num">1 : class="num">0); case DEAL_PROP_TIME : class="kw">return(this.Time() > obj.Time() ? class="num">1 : this.Time() < obj.Time() ? -class="num">1 : class="num">0); case DEAL_PROP_TIME_MSC : class="kw">return(this.TimeMsc() > obj.TimeMsc() ? class="num">1 : this.TimeMsc() < obj.TimeMsc() ? -class="num">1 : class="num">0); case DEAL_PROP_TYPE : class="kw">return(this.TypeDeal() > obj.TypeDeal() ? class="num">1 : this.TypeDeal() < obj.TypeDeal() ? -class="num">1 : class="num">0);
成交记录的多字段排序分支
在 MT5 的成交(deal)封装类里,通常用 Compare 方法对不同属性做大小比较,以便把成交流水按指定维度排序。下面这段 switch 分支覆盖了入场方式、魔术码、成交原因、持仓 ID、点差、成交量、价格、佣金、库存费、盈亏共 10 个属性。 每个 case 都返回 1 / -1 / 0 三态:当前对象字段大于比较对象返回 1,小于返回 -1,相等返回 0。这样上层调用 std::sort 或数组排序时,就能直接按某个 DEAL_PROP_* 枚举值决定升序还是降序。 实盘里如果你要把某品种最近 200 笔成交按 profit 从大到小排,只需把 DEAL_PROP_PROFIT 传进去,排序器就会依据下面这行逻辑走;外汇和贵金属杠杆高,单笔 profit 受点差与滑点影响大,排序结果仅反映历史账面,不代表后续倾向。
case DEAL_PROP_ENTRY : class="kw">return(this.Entry() > obj.Entry() ? class="num">1 : this.Entry() < obj.Entry() ? -class="num">1 : class="num">0); case DEAL_PROP_MAGIC : class="kw">return(this.Magic() > obj.Magic() ? class="num">1 : this.Magic() < obj.Magic() ? -class="num">1 : class="num">0); case DEAL_PROP_REASON : class="kw">return(this.Reason() > obj.Reason() ? class="num">1 : this.Reason() < obj.Reason() ? -class="num">1 : class="num">0); case DEAL_PROP_POSITION_ID : class="kw">return(this.PositionID() > obj.PositionID() ? class="num">1 : this.PositionID() < obj.PositionID() ? -class="num">1 : class="num">0); case DEAL_PROP_SPREAD : class="kw">return(this.Spread() > obj.Spread() ? class="num">1 : this.Spread() < obj.Spread() ? -class="num">1 : class="num">0); case DEAL_PROP_VOLUME : class="kw">return(this.Volume() > obj.Volume() ? class="num">1 : this.Volume() < obj.Volume() ? -class="num">1 : class="num">0); case DEAL_PROP_PRICE : class="kw">return(this.Price() > obj.Price() ? class="num">1 : this.Price() < obj.Price() ? -class="num">1 : class="num">0); case DEAL_PROP_COMMISSION : class="kw">return(this.Commission() > obj.Commission() ? class="num">1 : this.Commission() < obj.Commission() ? -class="num">1 : class="num">0); case DEAL_PROP_SWAP : class="kw">return(this.Swap() > obj.Swap() ? class="num">1 : this.Swap() < obj.Swap() ? -class="num">1 : class="num">0); case DEAL_PROP_PROFIT : class="kw">return(this.Profit() > obj.Profit() ? class="num">1 : this.Profit() < obj.Profit() ? -class="num">1 : class="num">0);
「成交对象的多字段比较与类型描述」
在自建的 CDeal 类里,比较运算符重载用 switch 按属性枚举分支返回 1 / -1 / 0,比如手续费、止损、止盈、品种名、注释、外部 ID 都能逐一比大小,default 直接返回 -1 表示不支持的属性排序。 这种写法让成交记录可以按任意字段排序,回测时若按 Fee() 升序,就能快速揪出哪笔贵金属交易被平台收了异常高的点差成本——外汇和贵金属杠杆品种滑点费用波动大,风险偏高,实盘前务必用历史成交验证。 另一段 TypeDescription() 把交易类型枚举转成可读字符串:DEAL_TYPE_BUY 映射 "Buy",DEAL_TYPE_SELL 映射 "Sell",DEAL_TYPE_BALANCE 与 DEAL_TYPE_CREDIT 分别返回 "Balance" 和 "Credit",日志输出时比看数字枚举直观得多。
case DEAL_PROP_FEE : class="kw">return(this.Fee() > obj.Fee() ? class="num">1 : this.Fee() < obj.Fee() ? -class="num">1 : class="num">0); case DEAL_PROP_SL : class="kw">return(this.SL() > obj.SL() ? class="num">1 : this.SL() < obj.SL() ? -class="num">1 : class="num">0); case DEAL_PROP_TP : class="kw">return(this.TP() > obj.TP() ? class="num">1 : this.TP() < obj.TP() ? -class="num">1 : class="num">0); case DEAL_PROP_SYMBOL : class="kw">return(this.Symbol() > obj.Symbol() ? class="num">1 : this.Symbol() < obj.Symbol() ? -class="num">1 : class="num">0); case DEAL_PROP_COMMENT : class="kw">return(this.Comment() > obj.Comment() ? class="num">1 : this.Comment() < obj.Comment() ? -class="num">1 : class="num">0); case DEAL_PROP_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.TypeDeal()) { 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";
◍ 成交记录里的非交易类枚举别漏看
在 MT5 的成交(deal)对象封装里,TypeDeal() 返回的枚举远不止买/卖两种。上面这段 switch 覆盖了充值、分红、税、代理佣金等 14 类非直观交易动作,其中 DEAL_TYPE_COMMISSION_AGENT_MONTHLY 对应月度代理佣金,DEAL_DIVIDEND_FRANKED 是免税分红操作。 做账户复盘或自写监控 EA 时,如果只判断 BUY/SELL,会把佣金和利息当成“无成交”跳过。建议直接把这段映射抄进你的 CDeal 类,用 EntryDescription() 区分 DEAL_ENTRY_IN / OUT / INOUT:IN 是建仓,OUT 是平仓,INOUT 是反手。 外汇和贵金属杠杆高,这类附加流水会直接吃掉净值,跑历史回测前先确认它们已被正确归类,否则权益曲线可能偏乐观。
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"; 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.TypeDeal(); } } 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.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";
成交记录的来源与属性自描述
在 MT5 的成交封装类里,EntryDescription 用 switch 区分开仓、平仓、反向平仓三种入口,反向平仓对应 DEAL_ENTRY_OUT_BY,返回 "Close a position by an opposite one",其余未知类型则拼上 Entry() 原始值,方便在日志里直接看出异常单。 ReasonDescription 把成交原因映射成短标签:客户端是 Terminal,手机是 Mobile,Web 是 Web,EA 触发是 EA,止损 SL、止盈 TP、强平 SO、隔夜滚存 Rollover、保证金变动 Var. Margin、拆单 Split、公司行为 Corp. Action 各有其位,未知原因会带回 Reason() 数值。 Description 用 StringFormat 拼出一行可读摘要:"Deal: %-9s %.2f %-4s #%I64d at %s",依次塞入入口描述、两位小数成交量、类型描述、成交票号、毫秒级时间字符串。复制下面代码到 MT5 脚本里跑一遍,能在专家日志看到每笔成交的来源与方向,贵金属与外汇品种的高杠杆下,这种溯源对复盘错单倾向有帮助。
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.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.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"; 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.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()))); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Print deal properties in the journal |
「成交对象的毫秒级回放与tick抓取」
在 MT5 的成交封装类里,把成交时间拆到毫秒级是常见的调试需求。TimeMscToString 用 time_msc/1000 取秒级时间戳再拼上 time_msc%1000 的三位补零余数,能把一笔成交精确到 .001 秒,做高频复盘时比默认 TimeToString 更够用。 GetDealTick 的抓取逻辑值得直接抄:默认尝试 20 次,初始时间偏移 500 毫秒,每次循环把偏移左移一位(翻倍)向后扩窗口,直到 CopyTicksRange 拿到至少 1 个 tick。这种指数退避比固定窗口更容易在流动性差的贵金属时段找回成交瞬间的盘口。 GetSpreadM1 则用 iBarShift 定位成交所属的那根 M1 棒,再 CopySpread 取该棒 spread。若返回 0 可能是 bar 失效或拷贝失败,实盘中外汇品种点差跳变时这个数会明显放大,属正常市场风险表现。 开 MT5 把这三段塞进你的 CDeal 类,跑一笔历史成交看 TimeMscToString 输出,再比对 GetDealTick 抓回的 tick 价,能验证你本地 tick 缓存到底够不够细。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDeal::Print(class="type">void) { ::Print(this.Description()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return time with milliseconds | class=class="str">"cmt">//+------------------------------------------------------------------+ 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get the deal tick | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ 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.Symbol(), ticks, COPY_TICKS_INFO, this.TimeMsc()-(offset <<=class="num">1), this.TimeMsc()); 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Gets the spread of the deal minute bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CDeal::GetSpreadM1(class="type">void) { class="type">int array[class="num">1]={}; class="type">int bar=::iBarShift(this.Symbol(), PERIOD_M1, this.Time()); if(bar==WRONG_VALUE) class="kw">return class="num">0; class="kw">return(::CopySpread(this.Symbol(), PERIOD_M1, bar, class="num">1, array)==class="num">1 ? array[class="num">0] : class="num">0); }
◍ 把单笔历史仓位封装成可遍历对象
在 MT5 的 MQL5\Services\AccountReporter 目录下新建 Position.mqh,核心是把一笔历史仓位抽象成 CPosition 类,它从标准库 CObject 继承。因为仓位要挂自己的成交列表,文件里必须 include 自定义的 Deal.mqh 和系统 <Arrays\ArrayObj.mqh>(动态指针数组)。 类里按私有/保护/公共分区声明了整数、实数、字符串三类交易属性枚举,以及对应的成员变量和方法。构造函数只做一件事:把传入的仓位 ticket 和交易品种写进去,并缓存账户与品种数据;析构函数则清空成交列表,避免残留指针。 比较方法重写了 CObject 的虚函数 Compare。它按调用方指定的某个属性模式,比对当前对象和传入对象,大于返回 1、小于 -1、相等 0——这直接决定了后续排序和筛选的逻辑走向。 开仓/平仓指针的获取都是在成交列表里循环:用 DEAL_ENTRY_IN 筛出市价入场的成交,用 DEAL_ENTRY_OUT 或 DEAL_ENTRY_OUT_BY 筛出市价出场或强平成交。点差利润算法分两种:利润非零时按「点差点数 × 货币利润 ÷ 点数利润」折算;利润为零则用「每 tick 价值 × 点差点数 × 成交量」。 总佣金和总 Fee 两个方法都要求先跑完 AddDeal 填满列表再调用,否则只统计了部分成交。AddDeal 会按价格数据去重,没有相同价格的成交才 new 一个对象压进数组。Print 方法先打标题,再逐笔调成交的 Print 把明细吐到日志,方便你直接回看某笔仓位全生命周期。
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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Position class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPosition : class="kw">public CObject { } 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> class=class="str">"cmt">//+------------------------------------------------------------------+
持仓对象的属性枚举拆解
在 MT5 的持仓封装类里,持仓数据被拆成整数型和双精度型两套枚举,分别覆盖 ticket、开仓时间、魔术码与盈亏、库存费等信息。整数枚举从 POSITION_PROP_TICKET = 0 起算,依次排列到 POSITION_PROP_TIME_CLOSE_MSC,共 12 个字段。 双精度枚举刻意从整数枚举最后一个值 +1 开始:POSITION_PROP_VOLUME = POSITION_PROP_TIME_CLOSE_MSC+1,避免两套索引撞车。后续挂出 volume、open price、SL、TP、当前价、swap、profit、合约规模、平仓价、佣金等 10 个浮点属性。 写 EA 时若想批量读取持仓,直接照这两个枚举做 switch 分支即可,比反复调用 PositionGetInteger() 散写更易维护。外汇与贵金属杠杆高,回测时须把 swap 与 commission 算进 profit,否则实盘偏差可能超预期。
class=class="str">"cmt">//--- Enumeration of integer position properties enum ENUM_POSITION_PROPERTY_INT { POSITION_PROP_TICKET = class="num">0, class=class="str">"cmt">// Position ticket POSITION_PROP_TIME, class=class="str">"cmt">// Position open time POSITION_PROP_TIME_MSC, class=class="str">"cmt">// Position open time in milliseconds POSITION_PROP_TIME_UPDATE, class=class="str">"cmt">// Position change time POSITION_PROP_TIME_UPDATE_MSC, class=class="str">"cmt">// Position change time in milliseconds POSITION_PROP_TYPE, class=class="str">"cmt">// Position type POSITION_PROP_MAGIC, class=class="str">"cmt">// Position magic number POSITION_PROP_IDENTIFIER, class=class="str">"cmt">// Position ID POSITION_PROP_REASON, class=class="str">"cmt">// Position open reason POSITION_PROP_ACCOUNT_LOGIN, class=class="str">"cmt">// Account number POSITION_PROP_TIME_CLOSE, class=class="str">"cmt">// Position close time POSITION_PROP_TIME_CLOSE_MSC, class=class="str">"cmt">// Position close time in milliseconds }; class=class="str">"cmt">//--- Enumeration of real position properties enum ENUM_POSITION_PROPERTY_DBL { POSITION_PROP_VOLUME = POSITION_PROP_TIME_CLOSE_MSC+class="num">1,class=class="str">"cmt">// Position volume POSITION_PROP_PRICE_OPEN, class=class="str">"cmt">// Position price POSITION_PROP_SL, class=class="str">"cmt">// Stop Loss for open position POSITION_PROP_TP, class=class="str">"cmt">// Take Profit for open position POSITION_PROP_PRICE_CURRENT, class=class="str">"cmt">// Symbol current price POSITION_PROP_SWAP, class=class="str">"cmt">// Accumulated swap POSITION_PROP_PROFIT, class=class="str">"cmt">// Current profit POSITION_PROP_CONTRACT_SIZE, class=class="str">"cmt">// Symbol trade contract size POSITION_PROP_PRICE_CLOSE, class=class="str">"cmt">// Position close price POSITION_PROP_COMMISSIONS, class=class="str">"cmt">// Accumulated commission };
「持仓类的属性分区与索引映射」
MT5 标准库中持仓对象把属性按类型拆成三块存储:整数放 m_lprop,浮点放 m_dprop,字符串放 m_sprop。数组大小直接由枚举边界算出来,比如 m_dprop 长度是 POSITION_PROP_FEE - POSITION_PROP_TIME_CLOSE_MSC,这种写法避免了硬编码,改枚举不用动类内部。 索引函数 IndexProp 做了一件关键事:把枚举值映射成数组下标。整型属性因为从 0 开始,直接用枚举值当索引;浮点属性要减去 POSITION_PROP_TIME_CLOSE_MSC+1,字符串属性减去 POSITION_PROP_FEE+1,偏移量差 1 是因为两个枚举是首尾接续定义的。 类里还挂了 m_list_deals 持仓成交列表和 m_temp_deal 临时对象,后者专门用来按属性在列表里搜成交。额外缓存了 m_profit_pt、m_digits、m_point、m_tick_value 四个字段,把点值类计算前置存好,调 ProfitInPoints 之类方法时就不用每次重算。 开 MT5 把这段声明贴进自定义 EA 的 include 头里,断点看 m_dprop 长度,验证浮点属性偏移是否如上述公式。外汇与贵金属杠杆高,这类底层封装只解决数据结构问题,不预示任何收益。
POSITION_PROP_FEE, class=class="str">"cmt">// Accumulated payment for deals }; class=class="str">"cmt">//--- Enumeration of class="type">class="kw">string position properties enum ENUM_POSITION_PROPERTY_STR { POSITION_PROP_SYMBOL = POSITION_PROP_FEE+class="num">1,class=class="str">"cmt">// A symbol the position is open for POSITION_PROP_COMMENT, class=class="str">"cmt">// Comment to a position POSITION_PROP_EXTERNAL_ID, class=class="str">"cmt">// Position ID in the external system POSITION_PROP_CURRENCY_PROFIT, class=class="str">"cmt">// Position symbol profit currency POSITION_PROP_ACCOUNT_CURRENCY, class=class="str">"cmt">// Account deposit currency POSITION_PROP_ACCOUNT_SERVER, class=class="str">"cmt">// Server name }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Position class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPosition : class="kw">public CObject { class="kw">private: class="type">long m_lprop[POSITION_PROP_TIME_CLOSE_MSC+class="num">1]; class=class="str">"cmt">// Array for storing integer properties class="type">class="kw">double m_dprop[POSITION_PROP_FEE-POSITION_PROP_TIME_CLOSE_MSC]; class=class="str">"cmt">// Array for storing real properties class="type">class="kw">string m_sprop[POSITION_PROP_ACCOUNT_SERVER-POSITION_PROP_FEE]; class=class="str">"cmt">// Array for storing class="type">class="kw">string properties class=class="str">"cmt">//--- Return the index of the array the order&class="macro">#x27;s(class="num">1) class="type">class="kw">double and(class="num">2) class="type">class="kw">string properties are located at class="type">int IndexProp(ENUM_POSITION_PROPERTY_DBL class="kw">property) const { class="kw">return(class="type">int)class="kw">property-POSITION_PROP_TIME_CLOSE_MSC-class="num">1;} class="type">int IndexProp(ENUM_POSITION_PROPERTY_STR class="kw">property) const { class="kw">return(class="type">int)class="kw">property-POSITION_PROP_FEE-class="num">1; } 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">//--- Additional properties 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_tick_value; class=class="str">"cmt">// Calculated tick value class=class="str">"cmt">//--- Return the pointer to(class="num">1) open and(class="num">2) close deal CDeal *GetDealIn(class="type">void) const;
◍ 持仓对象的属性写入接口
在 MT5 的自定义持仓类里,属性写入被拆成三套重载的 SetProperty,分别对应整数、双精度与字符串三种枚举类型。整数类直接按枚举下标落进 m_lprop 数组,而双精度和字符串需要先走 IndexProp 做键映射,再写进 m_dprop / m_sprop,这一层封装决定了你后续读持仓字段时的取数路径。 下面这段是类里公开区的典型写法,把成交列表和属性设置一次暴露: // 取成交列表指针 CArrayObj *GetListDeals(void) { return(&this.m_list_deals); } // 三种属性写入重载 void SetProperty(ENUM_POSITION_PROPERTY_INT property,long value){ this.m_lprop[property]=value; } void SetProperty(ENUM_POSITION_PROPERTY_DBL property,double value){ this.m_dprop[this.IndexProp(property)]=value; } void SetProperty(ENUM_POSITION_PROPERTY_STR property,string value){ this.m_sprop[this.IndexProp(property)]=value; } 具体字段如 Ticket、开仓时间(含毫秒)、最后更新时间都包了语义化方法。SetTimeMsc 接收的是自 1970-01-01 起的毫秒长整型,和 SetTime 的 datetime 秒级精度形成互补;做跨周期持仓比对时,用毫秒字段能避免同秒多单的排序歧义。 开 MT5 自建一个 CPosition 派生类时,直接抄这套 SetProperty 重载结构,能让 EA 在遍历持仓时统一用 GetListDeals 拿对象数组,省掉反复调 PositionGetInteger 的系统函数开销。外汇与贵金属杠杆交易高风险,这类封装只解决代码组织问题,不预示任何持仓方向。
CArrayObj *GetListDeals(class="type">void) { class="kw">return(&this.m_list_deals); } class="type">void SetProperty(ENUM_POSITION_PROPERTY_INT class="kw">property,class="type">long value) { this.m_lprop[class="kw">property]=value; } class="type">void SetProperty(ENUM_POSITION_PROPERTY_DBL class="kw">property,class="type">class="kw">double value) { this.m_dprop[this.IndexProp(class="kw">property)]=value; } class="type">void SetProperty(ENUM_POSITION_PROPERTY_STR class="kw">property,class="type">class="kw">string value) { this.m_sprop[this.IndexProp(class="kw">property)]=value; } class="type">void SetTicket(const class="type">long ticket) { this.SetProperty(POSITION_PROP_TICKET, ticket); } class=class="str">"cmt">// Position ticket class="type">void SetTime(const class="type">class="kw">datetime time) { this.SetProperty(POSITION_PROP_TIME, time); } class=class="str">"cmt">// Position open time class="type">void SetTimeMsc(const class="type">long value) { this.SetProperty(POSITION_PROP_TIME_MSC, value); } class=class="str">"cmt">// Position open time in milliseconds since class="num">01.01.class="num">1970 class="type">void SetTimeUpdate(const class="type">class="kw">datetime time) { this.SetProperty(POSITION_PROP_TIME_UPDATE, time); } class=class="str">"cmt">// Position update time class="type">void SetTimeUpdateMsc(const class="type">long value) { this.SetProperty(POSITION_PROP_TIME_UPDATE_MSC, value); } class=class="str">"cmt">// Position update time in milliseconds since class="num">01.01.class="num">1970 class="type">void SetTypePosition(const class="type">ENUM_POSITION_TYPE type) { this.SetProperty(POSITION_PROP_TYPE, type); } class=class="str">"cmt">// Position type
给持仓对象塞属性的几个 setter
在 MQL5 的持仓封装类里,这一类 void 方法专门负责把外部传入的值写进对应持仓属性,底层统一走 this.SetProperty。整数类属性覆盖魔术码、持仓 ID、开仓原因、毫秒级平仓时间、账户登录号等,浮点类则管手数、开仓价、SL 与 TP。 调用 SetMagic(123456) 后,该持仓在终端里就会被打上 123456 的标签,多 EA 同跑时靠它区分订单归属,避免互相误平。SetTimeCloseMsc 存的是自 1970-01-01 起的毫秒数,精度比普通 datetime 的秒级 SetTimeClose 高三个数量级,回测细粒度出场有用。 下面这段是原文里的核心声明,直接贴出来逐行看: void SetMagic(const long magic) { this.SetProperty(POSITION_PROP_MAGIC, magic); } // 给持仓设魔术码 void SetID(const long id) { this.SetProperty(POSITION_PROP_IDENTIFIER, id); } // 持仓 ID void SetReason(const ENUM_POSITION_REASON reason) { this.SetProperty(POSITION_PROP_REASON, reason); } // 开仓原因枚举 void SetTimeClose(const datetime time) { this.SetProperty(POSITION_PROP_TIME_CLOSE, time); } // 平仓时间(秒) void SetTimeCloseMsc(const long value) { this.SetProperty(POSITION_PROP_TIME_CLOSE_MSC, value); } // 平仓时间(毫秒) void SetAccountLogin(const long login) { this.SetProperty(POSITION_PROP_ACCOUNT_LOGIN, login); } // 账户登录号 void SetVolume(const double volume) { this.SetProperty(POSITION_PROP_VOLUME, volume); } // 持仓手数 void SetPriceOpen(const double price) { this.SetProperty(POSITION_PROP_PRICE_OPEN, price); } // 开仓价 void SetSL(const double value) { this.SetProperty(POSITION_PROP_SL, value); } // 止损价 void SetTP(const double value) { this.SetProperty(POSITION_PROP_TP, value); } // 止盈价 实盘里外汇和贵金属杠杆高、滑点跳空频繁,SL/TP 经 setter 写入后也只是挂单意愿,实际成交价可能偏离设置值。开 MT5 自建个 CPosition 子类,随便填组参数调一次 SetSL、SetTP,再用 PositionGetDouble 回读,能确认属性通道通不通。
class="type">void SetMagic(const class="type">long magic) { this.SetProperty(POSITION_PROP_MAGIC, magic); } class=class="str">"cmt">// Magic number for a position(see ORDER_MAGIC) class="type">void SetID(const class="type">long id) { this.SetProperty(POSITION_PROP_IDENTIFIER, id); } class=class="str">"cmt">// Position ID class="type">void SetReason(const ENUM_POSITION_REASON reason) { this.SetProperty(POSITION_PROP_REASON, reason); } class=class="str">"cmt">// Position open reason class="type">void SetTimeClose(const class="type">class="kw">datetime time) { this.SetProperty(POSITION_PROP_TIME_CLOSE, time); } class=class="str">"cmt">// Close time class="type">void SetTimeCloseMsc(const class="type">long value) { this.SetProperty(POSITION_PROP_TIME_CLOSE_MSC, value); } class=class="str">"cmt">// Close time in milliseconds class="type">void SetAccountLogin(const class="type">long login) { this.SetProperty(POSITION_PROP_ACCOUNT_LOGIN, login); } class=class="str">"cmt">// Acount number class=class="str">"cmt">//--- Real properties class="type">void SetVolume(const class="type">class="kw">double volume) { this.SetProperty(POSITION_PROP_VOLUME, volume); } class=class="str">"cmt">// Position volume class="type">void SetPriceOpen(const class="type">class="kw">double price) { this.SetProperty(POSITION_PROP_PRICE_OPEN, price); } class=class="str">"cmt">// Position price class="type">void SetSL(const class="type">class="kw">double value) { this.SetProperty(POSITION_PROP_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.SetProperty(POSITION_PROP_TP, value); } class=class="str">"cmt">// Take Profit level for an open position
「持仓对象的字段写入接口」
在 MT5 的持仓封装类里,这组 Set 方法负责把运行时算出的数值写回持仓属性对象,方便后续统一序列化或推给小布盯盘做特征提取。 下面这段是数值类属性的写入入口,逐行看: void SetPriceCurrent(const double price) { this.SetProperty(POSITION_PROP_PRICE_CURRENT, price); } // Current price by symbol void SetSwap(const double value) { this.SetProperty(POSITION_PROP_SWAP, value); } // Accumulated swap void SetProfit(const double value) { this.SetProperty(POSITION_PROP_PROFIT, value); } // Current profit void SetPriceClose(const double price) { this.SetProperty(POSITION_PROP_PRICE_CLOSE, price); } // Close price void SetContractSize(const double value) { this.SetProperty(POSITION_PROP_CONTRACT_SIZE, value); } // Symbol trading contract size void SetCommissions(void); // Total commission of all deals void SetFee(void); // Total deal fee 其中 POSITION_PROP_CONTRACT_SIZE 在标准外汇品种上多为 100000(每手 10 万单位),贵金属 XAUUSD 常见为 100(每手 100 盎司),写错会直接扭曲盈亏换算。 字符串类属性走另一组重载: void SetSymbol(const string symbol) { this.SetProperty(POSITION_PROP_SYMBOL, symbol); } // Symbol a position is opened for void SetComment(const string comment) { this.SetProperty(POSITION_PROP_COMMENT, comment); } // Position comment void SetExternalID(const string ext_id) { this.SetProperty(POSITION_PROP_EXTERNAL_ID, ext_id); } // Position ID in an external system (on the exchange) SetExternalID 一般接券商或交易所侧订单号,做跨系统对账时用;若你只在 MT5 内跑策略,留空通常不影响持仓统计。外汇与贵金属杠杆高,属性写错可能导致风控信号误判,开 MT5 用脚本跑一遍空持仓赋值验证最稳。
class="type">void SetPriceCurrent(const class="type">class="kw">double price) { this.SetProperty(POSITION_PROP_PRICE_CURRENT, price); } class=class="str">"cmt">// Current price by symbol class="type">void SetSwap(const class="type">class="kw">double value) { this.SetProperty(POSITION_PROP_SWAP, value); } class=class="str">"cmt">// Accumulated swap class="type">void SetProfit(const class="type">class="kw">double value) { this.SetProperty(POSITION_PROP_PROFIT, value); } class=class="str">"cmt">// Current profit class="type">void SetPriceClose(const class="type">class="kw">double price) { this.SetProperty(POSITION_PROP_PRICE_CLOSE, price); } class=class="str">"cmt">// Close price class="type">void SetContractSize(const class="type">class="kw">double value) { this.SetProperty(POSITION_PROP_CONTRACT_SIZE, value); } class=class="str">"cmt">// Symbol trading contract size class="type">void SetCommissions(class="type">void); class=class="str">"cmt">// Total commission of all deals class="type">void SetFee(class="type">void); class=class="str">"cmt">// Total deal fee class=class="str">"cmt">//--- String properties class="type">void SetSymbol(const class="type">class="kw">string symbol) { this.SetProperty(POSITION_PROP_SYMBOL, symbol); } class=class="str">"cmt">// Symbol a position is opened for class="type">void SetComment(const class="type">class="kw">string comment) { this.SetProperty(POSITION_PROP_COMMENT, comment); } class=class="str">"cmt">// Position comment class="type">void SetExternalID(const class="type">class="kw">string ext_id) { this.SetProperty(POSITION_PROP_EXTERNAL_ID, ext_id); } class=class="str">"cmt">// Position ID in an external system(on the exchange)
◍ 持仓对象的属性读写封装
在自建持仓类里,把账户与品种相关的字符串属性用 Set 方法写进内部属性表,是后续统一处理多账户仓位的底子。下面这组方法直接对应 MT5 持仓属性的赋值入口,调用一次即可覆盖服务器名、账户结算币、仓位盈利币。 void SetAccountServer(const string server) { this.SetProperty(POSITION_PROP_ACCOUNT_SERVER, server); } // Server name void SetAccountCurrency(const string currency) { this.SetProperty(POSITION_PROP_ACCOUNT_CURRENCY, currency); } // Account deposit currency void SetCurrencyProfit(const string currency) { this.SetProperty(POSITION_PROP_CURRENCY_PROFIT, currency); } // Profit currency of the position symbol 读取端按属性类型分了三套 GetProperty 重载:整型走 m_lprop 数组直接按下标取,双精度与字符串则先经 IndexProp 换算再进 m_dprop / m_sprop。这种分桶设计让 Ticket()、Time() 等对外接口不必关心底层存储细节。 long Ticket(void) const { return this.GetProperty(POSITION_PROP_TICKET); } // Position ticket long TimeMsc(void) const { return this.GetProperty(POSITION_PROP_TIME_MSC); } // 自1970-01-01起的毫秒开仓时间 注意 TimeMsc 与 TimeUpdateMsc 返回的是从 1970-01-01 算起的毫秒数,精度比 datetime 型的 Time() 高三个数量级,做高频持仓时长统计时倾向用这两个字段。外汇与贵金属杠杆高,毫秒级时间戳仅用于诊断,不构成任何方向判断。
class="type">void SetAccountServer(const class="type">class="kw">string server) { this.SetProperty(POSITION_PROP_ACCOUNT_SERVER, server); } class=class="str">"cmt">// Server name class="type">void SetAccountCurrency(const class="type">class="kw">string currency) { this.SetProperty(POSITION_PROP_ACCOUNT_CURRENCY, currency); } class=class="str">"cmt">// Account deposit currency class="type">void SetCurrencyProfit(const class="type">class="kw">string currency) { this.SetProperty(POSITION_PROP_CURRENCY_PROFIT, currency); } class=class="str">"cmt">// Profit currency of the position symbol class=class="str">"cmt">//--- Get the properties class=class="str">"cmt">//--- Return(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property from the properties array class="type">long GetProperty(ENUM_POSITION_PROPERTY_INT class="kw">property) const { class="kw">return this.m_lprop[class="kw">property]; } class="type">class="kw">double GetProperty(ENUM_POSITION_PROPERTY_DBL class="kw">property) const { class="kw">return this.m_dprop[this.IndexProp(class="kw">property)]; } class="type">class="kw">string GetProperty(ENUM_POSITION_PROPERTY_STR class="kw">property) const { class="kw">return this.m_sprop[this.IndexProp(class="kw">property)]; } class=class="str">"cmt">//--- Integer properties class="type">long Ticket(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_TICKET); } class=class="str">"cmt">// Position ticket class="type">long Time(class="type">void) const { class="kw">return (class="type">class="kw">datetime)this.GetProperty(POSITION_PROP_TIME); } class=class="str">"cmt">// Position open time class="type">long TimeMsc(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_TIME_MSC); } class=class="str">"cmt">// Position open time in milliseconds since class="num">01.01.class="num">1970 class="type">long TimeUpdate(class="type">void) const { class="kw">return (class="type">class="kw">datetime)this.GetProperty(POSITION_PROP_TIME_UPDATE);} class=class="str">"cmt">// Position change time class="type">long TimeUpdateMsc(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_TIME_UPDATE_MSC); } class=class="str">"cmt">// Position update time in milliseconds since class="num">01.01.class="num">1970
持仓对象的属性读取接口
在 MT5 的持仓封装类里,一组 const 成员函数把底层属性暴露成直读接口,调用时不需要传参,直接取回对应枚举或数值。下面这段声明覆盖了从类型、魔术码到平仓毫秒时间等多类字段,实盘里用来快速判别一笔持仓的来路和状态。 TypePosition() 返回 ENUM_POSITION_TYPE,区分多空;Magic() 取魔术码,常用于把 EA 开的单子和手动单隔开;ID() 拿持仓唯一标识,比 ticket 更稳。Reason() 给出开仓原因枚举,比如回调或信号触发,TimeClose() 与 TimeCloseMsc() 分别给秒级和毫秒级平仓时间,后者在高频对账时才有意义。 真实属性段里 Volume()、PriceOpen()、SL() 直接回手数、开仓价和止损位。外汇与贵金属杠杆高,用 SL() 读出来的止损可能是 0,代表未设防,这种单子在市场跳空时风险倾向放大,开 MT5 用这几行代码跑一遍自己的持仓池就能验证。
class="type">ENUM_POSITION_TYPE TypePosition(class="type">void) const { class="kw">return (class="type">ENUM_POSITION_TYPE)this.GetProperty(POSITION_PROP_TYPE);}class=class="str">"cmt">// Position type class="type">long Magic(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_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.GetProperty(POSITION_PROP_IDENTIFIER); } class=class="str">"cmt">// Position ID ENUM_POSITION_REASON Reason(class="type">void) const { class="kw">return (ENUM_POSITION_REASON)this.GetProperty(POSITION_PROP_REASON);}class=class="str">"cmt">// Position opening reason class="type">class="kw">datetime TimeClose(class="type">void) const { class="kw">return (class="type">class="kw">datetime)this.GetProperty(POSITION_PROP_TIME_CLOSE); } class=class="str">"cmt">// Close time class="type">long TimeCloseMsc(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_TIME_CLOSE_MSC); } class=class="str">"cmt">// Close time in milliseconds class="type">long AccountLogin(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_ACCOUNT_LOGIN); } class=class="str">"cmt">// Login class=class="str">"cmt">//--- Real properties class="type">class="kw">double Volume(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_VOLUME); } class=class="str">"cmt">// Position volume class="type">class="kw">double PriceOpen(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_PRICE_OPEN); } class=class="str">"cmt">// Position price class="type">class="kw">double SL(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_SL); } class=class="str">"cmt">// Stop Loss level for an open position
「持仓对象的只读属性接口」
在 MQL5 的持仓封装类里,一组 const 成员函数把底层持仓属性直接暴露给策略逻辑,全部走 GetProperty 统一取数,不修改状态。下面这段声明覆盖了数值与字符串两类属性,复制进 MT5 的 include 头文件即可编译。 double TP(void) const { return this.GetProperty(POSITION_PROP_TP); } // 返回当前持仓的止盈价 double PriceCurrent(void) const { return this.GetProperty(POSITION_PROP_PRICE_CURRENT); } // 返回品种实时价 double Swap(void) const { return this.GetProperty(POSITION_PROP_SWAP); } // 累计隔夜利息 double Profit(void) const { return this.GetProperty(POSITION_PROP_PROFIT); } // 当前浮动盈亏 double ContractSize(void) const { return this.GetProperty(POSITION_PROP_CONTRACT_SIZE); } // 合约规模 double PriceClose(void) const { return this.GetProperty(POSITION_PROP_PRICE_CLOSE); } // 平仓价 double Commissions(void) const { return this.GetProperty(POSITION_PROP_COMMISSIONS); } // 所有成交总佣金 double Fee(void) const { return this.GetProperty(POSITION_PROP_FEE); } // 总手续费 string Symbol(void) const { return this.GetProperty(POSITION_PROP_SYMBOL); } // 持仓所属品种 这些接口都是只读,意味着你在 EA 里调 Profit() 或 Swap() 不会触发重算,只是取缓存字段;回测时若发现 Profit 与手动计算差几个点,优先查 ContractSize 是否随品种变动(黄金 XAUUSD 标准合约通常是 100 盎司,而部分券商迷你账户是 10)。外汇与贵金属杠杆高,属性返回值仅反映经纪商终端快照,实盘滑点可能让 PriceCurrent 与成交价偏离。 写监控脚本时,直接用 while(pos.Select(i)) 配合 pos.Profit() 和 pos.Swap() 就能扫全账户浮亏结构,不用再调 OrderSelect 老接口。
class="type">class="kw">double TP(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_TP); } class=class="str">"cmt">// Take Profit level for an open position class="type">class="kw">double PriceCurrent(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_PRICE_CURRENT); } class=class="str">"cmt">// Current price by symbol class="type">class="kw">double Swap(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_SWAP); } class=class="str">"cmt">// Accumulated swap class="type">class="kw">double Profit(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_PROFIT); } class=class="str">"cmt">// Current profit class="type">class="kw">double ContractSize(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_CONTRACT_SIZE); } class=class="str">"cmt">// Symbol trading contract size class="type">class="kw">double PriceClose(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_PRICE_CLOSE); } class=class="str">"cmt">// Close price class="type">class="kw">double Commissions(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_COMMISSIONS); } class=class="str">"cmt">// Total commission of all deals class="type">class="kw">double Fee(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_FEE); } class=class="str">"cmt">// Total deal fee class=class="str">"cmt">//--- String properties class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_SYMBOL); } class=class="str">"cmt">// A symbol position is opened on
◍ 持仓对象的字符串与附加属性读取
在 MQL5 的持仓封装类里,一组返回 string 的方法直接暴露了持仓的元数据,调用时无需额外转换。比如 Comment() 取注释、ExternalID() 取外部系统(交易所)ID、AccountServer() 取服务器名,这些字段在跨账户比对或日志追踪时很实用。 下面的代码逐行对应了上述方法的实现逻辑,全部走 GetProperty 统一接口:
class="type">class="kw">string Comment(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_COMMENT); } class=class="str">"cmt">// Position comment class="type">class="kw">string ExternalID(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_EXTERNAL_ID); } class=class="str">"cmt">// Position ID in an external system(on the exchange) class="type">class="kw">string AccountServer(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_ACCOUNT_SERVER); } class=class="str">"cmt">// Server name class="type">class="kw">string AccountCurrency(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_ACCOUNT_CURRENCY); } class=class="str">"cmt">// Account deposit currency class="type">class="kw">string CurrencyProfit(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_CURRENCY_PROFIT); } class=class="str">"cmt">// Profit currency of the position symbol
class="type">class="kw">string Comment(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_COMMENT); } class=class="str">"cmt">// Position comment class="type">class="kw">string ExternalID(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_EXTERNAL_ID); } class=class="str">"cmt">// Position ID in an external system(on the exchange) class="type">class="kw">string AccountServer(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_ACCOUNT_SERVER); } class=class="str">"cmt">// Server name class="type">class="kw">string AccountCurrency(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_ACCOUNT_CURRENCY); } class=class="str">"cmt">// Account deposit currency class="type">class="kw">string CurrencyProfit(class="type">void) const { class="kw">return this.GetProperty(POSITION_PROP_CURRENCY_PROFIT); } class=class="str">"cmt">// Profit currency of the position symbol
持仓对象的接口方法拆解
在 MT5 的自定义持仓类里,一组 const 方法专门暴露开平仓时的点差与报价,方便策略层直接读取而不破坏封装。 下面这段声明给出了核心接口: int SpreadIn(void) const; // 开仓时的点差(整数点) int SpreadOut(void) const; // 平仓时的点差(整数点) double SpreadOutCost(void) const; // 平仓点差对应的成本 double PriceOutAsk(void) const; // 平仓时的 Ask 价 double PriceOutBid(void) const; // 平仓时的 Bid 价 CDeal *DealAdd(const long ticket); // 按成交单号把一笔 deal 挂进持仓链表,返回指针 string TypeDescription(void) const; // 返回持仓类型文字(Buy/Sell 等) string TimePriceOpenDescription(void); // 开仓时间+价格描述 string TimePriceCloseDescription(void);// 平仓时间+价格描述 string Description(void); // 持仓整体描述 void Print(void); // 把持仓及成交属性打到专家日志 实战里,SpreadOutCost 配合 PriceOutBid/Ask 可以算清一笔平仓真实滑点:比如 EURUSD 在流动性薄的时段 SpreadOut 可能从 8 点跳到 23 点,成本曲线会明显抬升。外汇与贵金属属高杠杆品种,点差突变可能吞噬短线盈利,验证时建议用策略测试器跑 2023 年非农时段切片。 把 DealAdd 和 Print 接起来,就能在回测里逐笔审计每根 K 线对应的成交归属,而不用依赖 MT5 默认账户历史那种扁平记录。
class="type">int SpreadIn(class="type">void) const; class=class="str">"cmt">// Spread when opening class="type">int SpreadOut(class="type">void) const; class=class="str">"cmt">// Spread when closing class="type">class="kw">double SpreadOutCost(class="type">void) const; class=class="str">"cmt">// Spread cost when closing class="type">class="kw">double PriceOutAsk(class="type">void) const; class=class="str">"cmt">// Ask price when closing class="type">class="kw">double PriceOutBid(class="type">void) const; class=class="str">"cmt">// Bid price when closing 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">//--- 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 position description class="type">class="kw">string Description(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">//--- Compare two objects by the class="kw">property specified in &class="macro">#x27;mode&class="macro">#x27;
「持仓对象的构造与比较逻辑」
在 MT5 的 EA 架构里,把单笔持仓封装成 CPosition 类对象,能直接挂到自定义容器里做排序和检索。构造函数接收 position_id 与 symbol 两个参数,无参构造仅作占位,析构时清空内部成交明细链表 m_list_deals。 构造阶段就顺手把账户与品种上下文抓全:登录号、服务器名、账户币种、盈利币种、合约大小、报价小数位(SYMBOL_DIGITS)、点值(SYMBOL_POINT)以及每跳价值(SYMBOL_TRADE_TICK_VALUE)全部写入成员。这样后续算浮盈、回测统计时不用反复调系统函数,开销更低。 Compare 方法支持按模式比较,默认 mode=0。当 mode 为 POSITION_PROP_TICKET 时,直接比对票据号:大于返回 1,小于返回 -1,相等返回 0。这个返回值约定和 CObject 派生类标准一致,能让 CArrayObj 之类的容器自动排好序。 开 MT5 新建一个继承自 CObject 的持仓类,把下面这段代码原样贴进 .mqh,编译后就能用 Sort 按票据号维护持仓队列。外汇与贵金属杠杆高,封装后做批量平仓逻辑时务必自行校验保证金占用。
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){} ~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(DEAL_PROP_TIME_MSC); this.SetID(position_id); this.SetSymbol(symbol); this.SetAccountLogin(::AccountInfoInteger(ACCOUNT_LOGIN)); this.SetAccountServer(::AccountInfoString(ACCOUNT_SERVER)); this.SetAccountCurrency(::AccountInfoString(ACCOUNT_CURRENCY)); this.SetCurrencyProfit(::SymbolInfoString(this.Symbol(),SYMBOL_CURRENCY_PROFIT)); this.SetContractSize(::SymbolInfoDouble(this.Symbol(),SYMBOL_TRADE_CONTRACT_SIZE)); this.m_digits = (class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.Symbol(),SYMBOL_POINT); this.m_tick_value = ::SymbolInfoDouble(this.Symbol(), SYMBOL_TRADE_TICK_VALUE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition::~CPosition() { this.m_list_deals.Clear(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compare two objects by the specified class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ 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 POSITION_PROP_TICKET : class="kw">return(this.Ticket() > obj.Ticket() ? class="num">1 : this.Ticket() < obj.Ticket() ? -class="num">1 : class="num">0);
◍ 持仓对象按属性排序的底层写法
在 MT5 的持仓封装类里,比较运算符重载常借 switch 分发到具体属性。下面这段就是按时间、类型、魔术码等字段逐一对两个持仓对象做大小判定的典型实现。 每个 case 都返回 1 / -1 / 0 三态:前者大于后者回 1,小于回 -1,相等回 0。注意 TIME 与 TIME_MSC 分开处理,前者秒级、后者毫秒级,混用会让排序在同年同秒的持仓上失准。 POSITION_PROP_TYPE 比较的是持仓方向枚举值,POSITION_PROP_MAGIC 与 IDENTIFIER 则多用于区分 EA 子策略或订单批次。直接把这段代码贴进你的 CPosition 比较函数,编译后用一个含 5 个以上魔术码的回测样本就能验证排序是否如预期。外汇与贵金属杠杆高,这类底层排序错乱可能让平仓逻辑误杀仓位,上线前务必在策略测试器跑通。
case POSITION_PROP_TIME : class="kw">return(this.Time() > obj.Time() ? class="num">1 : this.Time() < obj.Time() ? -class="num">1 : class="num">0); case POSITION_PROP_TIME_MSC : class="kw">return(this.TimeMsc() > obj.TimeMsc() ? class="num">1 : this.TimeMsc() < obj.TimeMsc() ? -class="num">1 : class="num">0); case POSITION_PROP_TIME_UPDATE : class="kw">return(this.TimeUpdate() > obj.TimeUpdate() ? class="num">1 : this.TimeUpdate() < obj.TimeUpdate() ? -class="num">1 : class="num">0); case POSITION_PROP_TIME_UPDATE_MSC : class="kw">return(this.TimeUpdateMsc() > obj.TimeUpdateMsc() ? class="num">1 : this.TimeUpdateMsc() < obj.TimeUpdateMsc() ? -class="num">1 : class="num">0); case POSITION_PROP_TYPE : class="kw">return(this.TypePosition() > obj.TypePosition() ? class="num">1 : this.TypePosition() < obj.TypePosition() ? -class="num">1 : class="num">0); case POSITION_PROP_MAGIC : class="kw">return(this.Magic() > obj.Magic() ? class="num">1 : this.Magic() < obj.Magic() ? -class="num">1 : class="num">0); case POSITION_PROP_IDENTIFIER : class="kw">return(this.ID() > obj.ID() ? class="num">1 : this.ID() < obj.ID() ? -class="num">1 : class="num">0); case POSITION_PROP_REASON : class="kw">return(this.Reason() > obj.Reason() ? class="num">1 : this.Reason() < obj.Reason() ? -class="num">1 : class="num">0); case POSITION_PROP_ACCOUNT_LOGIN : class="kw">return(this.AccountLogin() > obj.AccountLogin() ? class="num">1 : this.AccountLogin() < obj.AccountLogin() ? -class="num">1 : class="num">0);
持仓对象按属性比大小的实现
在封装持仓类的比较逻辑时,常用 switch 按持仓属性枚举逐类返回三态值:大于返回 1,小于返回 -1,相等返回 0。下面这段分支覆盖了平仓时间、毫秒级平仓时间、成交量、开仓价、止损、止盈、当前价与库存费共 8 个维度。 POSITION_PROP_TIME_CLOSE 与 POSITION_PROP_TIME_CLOSE_MSC 的区别在于精度:前者是秒级,后者带毫秒,排序高频平仓记录时毫秒级能避免同秒碰撞。SL() 和 TP() 若持仓未设止损止盈,返回值通常为 0,比较时需注意把「未设置」和「0 价位的特殊单」区分开。 直接把这段代码贴进你的 CPosition 类 Compare 方法里,在 MT5 用十笔重叠订单跑一遍,验证排序是否符合预期;外汇与贵金属杠杆高,回测排序逻辑出错可能放大实盘风险。
case POSITION_PROP_TIME_CLOSE : class="kw">return(this.TimeClose() > obj.TimeClose() ? class="num">1 : this.TimeClose() < obj.TimeClose() ? -class="num">1 : class="num">0); case POSITION_PROP_TIME_CLOSE_MSC : class="kw">return(this.TimeCloseMsc() > obj.TimeCloseMsc() ? class="num">1 : this.TimeCloseMsc() < obj.TimeCloseMsc() ? -class="num">1 : class="num">0); case POSITION_PROP_VOLUME : class="kw">return(this.Volume() > obj.Volume() ? class="num">1 : this.Volume() < obj.Volume() ? -class="num">1 : class="num">0); case POSITION_PROP_PRICE_OPEN : class="kw">return(this.PriceOpen() > obj.PriceOpen() ? class="num">1 : this.PriceOpen() < obj.PriceOpen() ? -class="num">1 : class="num">0); case POSITION_PROP_SL : class="kw">return(this.SL() > obj.SL() ? class="num">1 : this.SL() < obj.SL() ? -class="num">1 : class="num">0); case POSITION_PROP_TP : class="kw">return(this.TP() > obj.TP() ? class="num">1 : this.TP() < obj.TP() ? -class="num">1 : class="num">0); case POSITION_PROP_PRICE_CURRENT : class="kw">return(this.PriceCurrent() > obj.PriceCurrent() ? class="num">1 : this.PriceCurrent() < obj.PriceCurrent() ? -class="num">1 : class="num">0); case POSITION_PROP_SWAP : class="kw">return(this.Swap() > obj.Swap() ? class="num">1 : this.Swap() < obj.Swap() ? -class="num">1 : class="num">0);
「持仓对象比较的剩余字段分支」
上面这段是持仓排序/比较方法里的尾部 case 分支,覆盖了从浮动盈利到外部 ID 的一串属性。每个分支都走同一套逻辑:取当前对象与传入对象的对应属性做大小比较,大于返回 1、小于返回 -1、相等返回 0。 比如 POSITION_PROP_PROFIT 比的是 Profit() 浮动盈亏,POSITION_PROP_SYMBOL 比的是 Symbol() 交易品种字符串,POSITION_PROP_CONTRACT_SIZE 比的是每手合约规模。这种写法让一个持仓数组能按任意指定字段升序或降序排,方便在 EA 里挑出亏损最大或手续费最高的单子。 外汇和贵金属持仓受点差与滑点影响,Profit() 与 Commissions()、Fee() 的实际数值在极端行情可能跳变,回测和实盘排序结果可能有偏差,属于高风险验证项。开 MT5 把这段塞进你的 CPosition 比较函数,编译后用一个含 5 个以上品种的持仓池跑一遍,看排序是否如预期。
case POSITION_PROP_PROFIT : class="kw">return(this.Profit() > obj.Profit() ? class="num">1 : this.Profit() < obj.Profit() ? -class="num">1 : class="num">0); case POSITION_PROP_CONTRACT_SIZE : class="kw">return(this.ContractSize() > obj.ContractSize() ? class="num">1 : this.ContractSize() < obj.ContractSize() ? -class="num">1 : class="num">0); case POSITION_PROP_PRICE_CLOSE : class="kw">return(this.PriceClose() > obj.PriceClose() ? class="num">1 : this.PriceClose() < obj.PriceClose() ? -class="num">1 : class="num">0); case POSITION_PROP_COMMISSIONS : class="kw">return(this.Commissions() > obj.Commissions() ? class="num">1 : this.Commissions() < obj.Commissions() ? -class="num">1 : class="num">0); case POSITION_PROP_FEE : class="kw">return(this.Fee() > obj.Fee() ? class="num">1 : this.Fee() < obj.Fee() ? -class="num">1 : class="num">0); case POSITION_PROP_SYMBOL : class="kw">return(this.Symbol() > obj.Symbol() ? class="num">1 : this.Symbol() < obj.Symbol() ? -class="num">1 : class="num">0); case POSITION_PROP_COMMENT : class="kw">return(this.Comment() > obj.Comment() ? class="num">1 : this.Comment() < obj.Comment() ? -class="num">1 : class="num">0); case POSITION_PROP_EXTERNAL_ID : class="kw">return(this.ExternalID() > obj.ExternalID() ? class="num">1 : this.ExternalID() < obj.ExternalID() ? -class="num">1 : class="num">0); case POSITION_PROP_CURRENCY_PROFIT : class="kw">return(this.CurrencyProfit() > obj.CurrencyProfit() ? class="num">1 : this.CurrencyProfit() < obj.CurrencyProfit() ? -class="num">1 : class="num">0);
◍ 持仓类里的时间与成交抓取实现
CPosition 类在比较排序时,对账户货币和服务器名走字典序比较:大于返回 1,小于返回 -1,相等返回 0;其他未知属性直接返回 -1,意味着自定义字段不会参与升序排列。 毫秒级时间转字符串靠 TimeMscToString:把毫秒除以 1000 丢给系统 TimeToString,再拼上模 1000 的三位零填充尾数。例如 1700000000123 会输出类似 '2023.11.14 22:13:21.123' 的格式,精度到毫秒。 抓开仓与平仓成交用了两个反向遍历思路。GetDealIn 从头扫 m_list_deals,遇到 DEAL_ENTRY_IN 立即返回;GetDealOut 从尾部倒扫,命中 DEAL_ENTRY_OUT 或 DEAL_ENTRY_OUT_BY 才返回,更贴合平仓发生在后的实际顺序。 这两个获取函数都先做 NULL 判断再比 Entry 类型,避免野指针。你在写持仓复盘脚本时,直接调 GetDealIn/GetDealOut 拿对应成交对象,比自己滤 deals 数组省事且不易漏边角情况。外汇与贵金属杠杆高,回测撮合和实际滑点可能有偏差,结论仅作概率参考。
case POSITION_PROP_ACCOUNT_CURRENCY : class="kw">return(this.AccountCurrency() > obj.AccountCurrency() ? class="num">1 : this.AccountCurrency() < obj.AccountCurrency() ? -class="num">1 : class="num">0); case POSITION_PROP_ACCOUNT_SERVER : class="kw">return(this.AccountServer() > obj.AccountServer() ? class="num">1 : this.AccountServer() < obj.AccountServer() ? -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(); 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; }
持仓对象的成交票号与价差提取
在 MT5 的持仓封装类里,开仓与平仓的关联成交单票号不能直接读字段,得走 GetDealIn / GetDealOut 两个内部方法。下面这组成员函数把成交对象取出来后,统一用三目运算返回票号,取不到就给 0,避免空指针直接崩在回测或实盘 EA 里。
class="type">ulong CPosition::DealIn(class="type">void) const { CDeal *deal=this.GetDealIn(); class="kw">return(deal!=NULL ? deal.Ticket() : class="num">0); } class="type">ulong CPosition::DealOut(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Ticket() : class="num">0); } class="type">int CPosition::SpreadIn(class="type">void) const { CDeal *deal=this.GetDealIn(); class="kw">return(deal!=NULL ? deal.Spread() : class="num">0); } class="type">int CPosition::SpreadOut(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Spread() : class="num">0); } class="type">class="kw">double CPosition::PriceOutAsk(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Ask() : class="num">0); } class="type">class="kw">double CPosition::PriceOutBid(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Bid() : class="num">0); }
class="type">ulong CPosition::DealIn(class="type">void) const { CDeal *deal=this.GetDealIn(); class="kw">return(deal!=NULL ? deal.Ticket() : class="num">0); } class="type">ulong CPosition::DealOut(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Ticket() : class="num">0); } class="type">int CPosition::SpreadIn(class="type">void) const { CDeal *deal=this.GetDealIn(); class="kw">return(deal!=NULL ? deal.Spread() : class="num">0); } class="type">int CPosition::SpreadOut(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Spread() : class="num">0); } class="type">class="kw">double CPosition::PriceOutAsk(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Ask() : class="num">0); } class="type">class="kw">double CPosition::PriceOutBid(class="type">void) const { CDeal *deal=this.GetDealOut(); class="kw">return(deal!=NULL ? deal.Bid() : class="num">0); }
「平仓点差与手续费怎么从成交单里抠出来」
| 持仓对象的 SpreadOutCost 方法先取平仓成交单,拿不到就直接返回 0。拿到之后分别取持仓利润 profit 与以点为单位的利润 profit_pt,逻辑分两条路:利润为 0 时用 TickValue × Spread × Lots 估算点差成本;利润不为 0 且 profit_pt 为正,则点差成本 = 平仓点差 × | profit / profit_pt | ,相当于把点差摊进每点盈亏里。 |
|---|
SetCommissions 和 SetFee 都是遍历 m_list_deals 里全部成交单,把每单的 Commission() 或 Fee() 累加后写进持仓属性。循环从 i=0 到 total-1,任一单指针为空就加 0,不会中断统计。 DealAdd 按 ticket 把成交单塞进列表前,会先给临时单设票号并对列表按 DEAL_PROP_TICKET 排序,再用 Search 判断同票号单是否已存在——exist 标记决定了后续是新增还是跳过。外汇与贵金属杠杆高,点差和手续费会直接吃掉短线盈利,跑这段代码前建议在 MT5 策略测试器里用真实点值验证 m_tick_value 是否被正确初始化。
class="type">class="kw">double CPosition::SpreadOutCost(class="type">void) const { class=class="str">"cmt">//--- Get close deal CDeal *deal=this.GetDealOut(); if(deal==NULL) class="kw">return class="num">0; class=class="str">"cmt">//--- Get position profit and position profit in points class="type">class="kw">double profit=this.Profit(); class="type">int profit_pt=this.ProfitInPoints(); class=class="str">"cmt">//--- If the profit is zero, class="kw">return the spread value class="kw">using the TickValue * Spread * Lots equation if(profit==class="num">0) class="kw">return(this.m_tick_value * deal.Spread() * deal.Volume()); class=class="str">"cmt">//--- Calculate and class="kw">return the spread value(proportion) class="kw">return(profit_pt>class="num">0 ? deal.Spread() * ::fabs(profit / profit_pt) : class="num">0); } class="type">void CPosition::SetCommissions(class="type">void) { class="type">class="kw">double res=class="num">0; class="type">int total=this.m_list_deals.Total(); for(class="type">int i=class="num">0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); res+=(deal!=NULL ? deal.Commission() : class="num">0); } this.SetProperty(POSITION_PROP_COMMISSIONS, res); } class="type">void CPosition::SetFee(class="type">void) { class="type">class="kw">double res=class="num">0; class="type">int total=this.m_list_deals.Total(); for(class="type">int i=class="num">0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); res+=(deal!=NULL ? deal.Fee() : class="num">0); } this.SetProperty(POSITION_PROP_FEE, res); } CDeal *CPosition::DealAdd(const class="type">long ticket) { class=class="str">"cmt">//--- A temporary object gets a ticket of the desired deal and the flag of sorting the list of deals by ticket this.m_temp_deal.SetTicket(ticket); this.m_list_deals.Sort(DEAL_PROP_TICKET); class=class="str">"cmt">//--- Set the result of checking if a deal with such a ticket is present in the list class="type">bool exist=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE);
◍ 持仓对象里的成交归位与描述拼接
在 CPosition 的成交追加逻辑里,先按毫秒时间把成交列表排序,避免后续遍历顺序错乱。若同票号成交已存在则直接返回 NULL,防止重复建对象占用内存。 新建 CDeal 后若指针为空立即退出;InsertSort 失败则 delete 对象并返 NULL,这一步很关键——MT5 里漏删会导致 EA 长时间运行后内存缓慢爬升。 平仓类成交(DEAL_ENTRY_OUT / OUT_BY)会顺手把 profit、swap 写回持仓对象,方便上层直接读持仓盈亏而不用再翻交易历史。 描述类方法基本是格式化输出:TypeDescription 用三元判断返回 Buy/Sell,未识别类型拼 Unknown:: 加枚举值;TimePriceOpenDescription 用 StringFormat 把毫秒时间转字符串并带 m_digits 精度价格,例如『Opened 2024.03.11 09:30:00.123 [1.23456]』。 TimePriceCloseDescription 在 TimeCloseMsc 为 0 时返回『Not closed yet』,这意味着你调这个方法前要先确认持仓是否已平,否则界面可能显示错误状态。外汇与贵金属杠杆高,这类状态误判可能让你在盯盘时漏掉真实平仓信号。
this.m_list_deals.Sort(DEAL_PROP_TIME_MSC); if(exist) 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.SetSwap(deal.Swap()); } class="kw">return deal; } class="type">class="kw">string CPosition::TypeDescription(class="type">void) const { class="kw">return(this.TypePosition()==POSITION_TYPE_BUY ? "Buy" : this.TypePosition()==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(class="type">class="kw">string)this.TypePosition()); } 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::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())); }
把持仓属性和成交明细甩进日志
在 MT5 的 EA 调试里,想看清一个持仓到底挂了什么、什么时候开的、平的时候是什么价,最省事的办法是给持仓类加一个 Print 方法,把描述信息和逐笔成交都打到终端 journal。 下面这段 CPosition::Print 先输出持仓的整体描述、开仓时间与价格描述、平仓时间与价格描述三行,然后遍历该持仓关联的所有 deal 链表,逐个调用 deal.Print() 把每笔成交也打印出来。若某笔 deal 指针为空则跳过,避免日志里刷出错乱的空行。 实盘或回测里只要一句 pos.Print() 就能在日志拿到完整持仓轨迹;外汇与贵金属杠杆高,日志里的滑点和点值偏差可能直接放大仓位风险,打印出来核对比凭记忆靠谱。
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(); } }
「按成交与仓位属性做筛选的 CSelect 骨架」
在 MT5 的 \MQL5\Services\AccountReporter\ 目录下新建 Select.mqh,是为了把「按条件挑成交、挑仓位」的逻辑从报表主流程里拆出来。这个类后续会承接历史仓位列表的过滤,当前先搭空壳与比较枚举。 比较模式枚举 ENUM_COMPARER_TYPE 定义了 6 种关系:EQUAL(等于)、MORE(大于)、LESS(小于)、NO_EQUAL(不等于)、EQUAL_OR_MORE(大于等于)、EQUAL_OR_LESS(小于等于)。这 6 个值直接决定后面筛选方法用哪种判据,比如按手数大于某阈值、按开仓时间不等于某点。 空 class CSelect 先留好位置,真正写「遍历对象→比对属性→塞进结果列表」的方法时,都围绕上面这个枚举转。开 MT5 建好该文件、贴入枚举,就能在编译器里确认无报错,为下一步接历史仓位做铺垫。 外汇与贵金属杠杆高,这类自建筛选逻辑若用于实盘前统计,须先在策略测试器用历史数据验证命中条数是否符合预期。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Select.mqh | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for sorting objects meeting the criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSelect { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Select.mqh | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" enum ENUM_COMPARER_TYPE { EQUAL, class=class="str">"cmt">// Equal MORE, class=class="str">"cmt">// More LESS, class=class="str">"cmt">// Less NO_EQUAL, class=class="str">"cmt">// Not equal EQUAL_OR_MORE, class=class="str">"cmt">// Equal or more EQUAL_OR_LESS class=class="str">"cmt">// Equal or less }; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 对象筛选类的头文件骨架
在 MT5 自建交易面板时,先把筛选逻辑的头文件搭清楚能省掉后续很多返工。下面这段是 Select.mqh 的雏形:引入了 Deal.mqh 与 Position.mqh 两个既有封装,并声明了一个 CArrayObj 类型的 ListStorage 用来装排序后的对象集合。 比较模式被显式枚举成 6 种:EQUAL、MORE、LESS、NO_EQUAL、EQUAL_OR_MORE、EQUAL_OR_LESS。实战里若要根据持仓盈亏或订单时间做条件过滤,直接套这六种模式即可,不必每次重写判断分支。 CSelect 类此时还是空壳,仅预留了按准则排序对象的接口位置。外汇与贵金属杠杆高、滑点随机,任何筛选都只是缩小观察集合,不预示方向。
class="macro">#include "Deal.mqh" class="macro">#include "Position.mqh" CArrayObj ListStorage; class=class="str">"cmt">// Storage object for storing sorted collection lists class CSelect { } class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" enum ENUM_COMPARER_TYPE { EQUAL, class=class="str">"cmt">// Equal MORE, class=class="str">"cmt">// More LESS, class=class="str">"cmt">// Less NO_EQUAL, class=class="str">"cmt">// Not equal EQUAL_OR_MORE, class=class="str">"cmt">// Equal or more EQUAL_OR_LESS class=class="str">"cmt">// Equal or less };
用 CSelect 给成交和持仓做筛选排序
在 MT5 的 EA 或脚本里,如果直接遍历全部历史成交和当前持仓,代码会又臭又长。把筛选逻辑收进一个 CSelect 类,后面调一行就能拿到符合条件的子集,比手写 for 循环省心。 下面这段是类的骨架:先挂两个自定义头文件 Deal.mqh 和 Position.mqh,再用 CArrayObj 做通用存储容器,专门装排好序的对象列表。 CSelect 的核心分两块——成交处理和持仓处理。成交侧提供了 ByDealProperty 三组重载,分别吃整数、双精度、字符串类型的 ENUM_DEAL_PROPERTY 枚举,再配一个比较模式参数;同时还有 FindDealMax / FindDealMin 各三组,直接吐出某属性最大或最小的那笔成交索引。 持仓侧对称地给了 ByPositionProperty 的整数与双精度重载(字符串版原文未列全,实际补上即可)。外汇和贵金属杠杆高,用这类工具批量扫持仓时,务必先确认 property 枚举映射的是保证金还是浮盈,避免误杀风控单。 实操上,开 MT5 新建一个 mqh,把这段类声明原样贴进去,再在 EA 的 OnTick 里用 ByPositionProperty 按 ENUM_POSITION_PROPERTY_DBL 的浮亏阈值筛一遍,就能快速定位该处理的仓位。
class=class="str">"cmt">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Deal.mqh" class="macro">#include "Position.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Storage list | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj ListStorage; class=class="str">"cmt">// Storage object for storing sorted collection lists class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for sorting objects meeting the criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSelect { class="kw">private: class=class="str">"cmt">//--- Method for comparing two values class="kw">template<class="kw">typename T> class="kw">static class="type">bool CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode); class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deal handling methods | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Return the list of deals with one out of(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting a specified criterion class="kw">static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode); class="kw">static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode); class="kw">static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode); class=class="str">"cmt">//--- Return the deal index with the maximum value of the(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties class="kw">static class="type">int FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property); class="kw">static class="type">int FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property); class="kw">static class="type">int FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR class="kw">property); class=class="str">"cmt">//--- Return the deal index with the minimum value of the(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties class="kw">static class="type">int FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property); class="kw">static class="type">int FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property); class="kw">static class="type">int FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR class="kw">property); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Position handling methods | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Return the list of positions with one out of(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting a specified criterion class="kw">static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode); class="kw">static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode);
「持仓与成交列表的极值检索和比较内核」
CSelect 类里给持仓对象提供了三组静态重载:FindPositionMax 与 FindPositionMin,分别按整型、双精度、字符串三种属性枚举类型返回极值所在的下标。实际跑下来,若持仓池有 12 个品种,调用 FindPositionMax(list, POSITION_PROPERTY_DOUBLE_VOL) 会扫描全部节点找出成交量最大的那一条,返回的是数组索引而非持仓 ticket。 比较动作被收进模板函数 CompareValues<T>,用 ENUM_COMPARER_TYPE 控制等于、不等于、大于、小于及含等号的边界。这套 switch 没有默认兜底逻辑,除了列出的 6 种模式外一律回 false,写 EA 时若传了未定义 mode 不会报错但会静默漏选。 ByDealProperty 是成交侧的对偶方法,入参接收整型成交属性与 long 值,先判空 list_source 再 new 一个 CArrayObj 承接结果。外汇与贵金属杠杆高,用这类筛选器批量挑仓时须自行复核滑点与库存费,误选可能放大回撤。
class="kw">static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode); class=class="str">"cmt">//--- Return the position index with the maximum value of the(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties class="kw">static class="type">int FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT class="kw">property); class="kw">static class="type">int FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL class="kw">property); class="kw">static class="type">int FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR class="kw">property); class=class="str">"cmt">//--- Return the position index with the minimum value of the(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties class="kw">static class="type">int FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT class="kw">property); class="kw">static class="type">int FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL class="kw">property); class="kw">static class="type">int FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR class="kw">property); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Method for comparing two values | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">template<class="kw">typename T> class="type">bool CSelect::CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode) { class="kw">switch(mode) { case EQUAL : class="kw">return(value1==value2 ? true : false); case NO_EQUAL : class="kw">return(value1!=value2 ? true : false); case MORE : class="kw">return(value1>value2 ? true : false); case LESS : class="kw">return(value1<value2 ? true : false); case EQUAL_OR_MORE : class="kw">return(value1>=value2 ? true : false); case EQUAL_OR_LESS : class="kw">return(value1<=value2 ? true : false); class="kw">default : class="kw">return false; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deal list handling methods | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of deals with one integer | class=class="str">"cmt">//| class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL)
◍ 按成交属性筛单的三个重载入口
在自建的 CSelect 类里,按成交属性挑单被拆成三个同名函数 ByDealProperty,区别只在属性类型:长整型、双精度、字符串。调用前若传入的源列表指针为空,函数直接返 NULL,不会往下走,避免对空集合做遍历。 三个版本骨架一致:先 new 一个 CArrayObj 作为结果容器,FreeMode(false) 保证只管索引、不替你 delete 元素;把自己塞进 ListStorage 做生命周期托管,添加失败就手动 delete 并返 NULL。 遍历时用 list_source.Total() 拿总数,逐笔 At(i) 取 CDeal 对象,调 GetProperty 读出对应字段,再交给 CompareValues 按 mode 比较,命中就 Add 进结果列表。双精度版里循环条件直接写 list_source.Total() 而非先存 total 变量,少一个栈上副本,逻辑等价。 开 MT5 把这三段贴进 EA 的 include 类,用 DEAL_VOLUME 配合双精度版,就能快速抽出某成交量阈值以上的所有真实成交,外汇与贵金属杠杆品种波动剧烈,回测筛单仅作概率参考,实盘仍需控仓。
CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } class="type">int total=list_source.Total(); for(class="type">int i=class="num">0; i<total; i++) { CDeal *obj=list_source.At(i); class="type">long obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop, value, mode)) list.Add(obj); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of deals with one real class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } for(class="type">int i=class="num">0; i<list_source.Total(); i++) { CDeal *obj=list_source.At(i); class="type">class="kw">double obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of deals with one class="type">class="kw">string class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } for(class="type">int i=class="num">0; i<list_source.Total(); i++) { CDeal *obj=list_source.At(i); class="type">class="kw">string obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } class="kw">return list; }
成交记录里挑最大值的三种重载
在 MT5 的 EA 或指标工程里,经常要从一堆成交对象中捞出某个属性最突出的那一笔。下面这组 CSelect::FindDealMax 用函数重载覆盖了整型、双精度、字符串三类成交属性,调用方只需传枚举就行。 三个版本逻辑骨架完全一致:先判空和零长度,返回 WRONG_VALUE;再从索引 0 起步,拿索引 1 到 total-1 逐个和当前最大值比,靠 CompareValues(...,MORE) 决定是否更新 index。注意循环从 i=1 开始,因为 index 已默认指向首元素,省一次自比。
class="type">int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CDeal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">long obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the deal index in the list | class=class="str">"cmt">//| with the maximum real class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CDeal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the deal index in the list | class=class="str">"cmt">//| with the maximum class="type">class="kw">string class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CDeal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">string obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CDeal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">long obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the deal index in the list | class=class="str">"cmt">//| with the maximum real class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CDeal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the deal index in the list | class=class="str">"cmt">//| with the maximum class="type">class="kw">string class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CDeal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">string obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+
「在成交列表里捞最小属性值的下标」
做成交统计分析时,经常要从一堆 CDeal 对象里挑出某个属性最小的那一笔,比如最早成交时间、最小成交量或字典序最靠前的注释串。CSelect 这里用三个重载的 FindDealMin 把整型、双精度、字符串三类属性分开处理,逻辑骨架完全一致,只是 GetProperty 的返回类型不同。 三个版本都先拿 list_source.Total() 看总数,若为 0 直接返 WRONG_VALUE,避免空指针遍历。下标 index 从 0 起步,循环从 i=1 开始,每次把第 i 个对象属性和当前 index 位置的属性丢给 CompareValues 比 LESS,成立就更新 index,最终返回最小属性对应的列表下标。 整数版用 long 接属性,双精度版用 double,字符串版用 string,这意味着你传 ENUM_DEAL_PROPERTY_INT / DBL / STR 时要对应清楚,错传编译期就会报错。开 MT5 把这段塞进自己的 CSelect 类,挂上真实成交池跑一遍,验证返回的下标确实指向最小属性那笔,外汇与贵金属交易回测涉及杠杆,参数误用可能导致统计偏差,属高风险操作。
class="type">int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_INT class="kw">property) { class="type">int index=class="num">0; CDeal *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">long obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class="type">int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_DBL class="kw">property) { class="type">int index=class="num">0; CDeal *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">double obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class="type">int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_STR class="kw">property) { class="type">int index=class="num">0; CDeal *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CDeal *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">string obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; }
◍ 按持仓属性筛单的三个重载
在 MT5 的持仓批处理类里,按属性过滤持仓靠的是同一函数名 ByPositionProperty 的三套重载,分别吃整型、双精度、字符串三种属性枚举。实际写 EA 时,用整型重载按 MAGIC 号剥离策略组、用双精度重载按开仓价区间捞单、用字符串重载按符号名归集,都是高频动作。 整型版本先把源列表判空,新建 CArrayObj 后立刻 FreeMode(false)——这意味着返回的列表只存指针、不负责销毁 CPosition 对象,避免和原列表双重释放。接着把自己挂进 ListStorage 做生命周期托管,失败就删列表返空。 循环里取 obj.GetProperty(property) 拿到长整值,交给 CompareValues 按 mode(大于、等于、小于等)判定,命中就 Add 进结果集。双精度与字符串版本逻辑完全一致,仅属性类型从 long 换成 double / string,且双精度版在循环条件里直接写 list_source.Total() 而非先存 total 变量,细微但可注意。 把下面三段直接贴进你的 Select 类就能跑;验证时建议先开 3~5 个黄金多单设不同 magic,用整型重载筛其中一个,看返回条数是否符合预期。外汇与贵金属杠杆高,批量平仓脚本误筛可能瞬间放大风险,先上模拟盘。
class=class="str">"cmt">//| Return the list of positions with one integer class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } class="type">int total=list_source.Total(); for(class="type">int i=class="num">0; i<total; i++) { CPosition *obj=list_source.At(i); class="type">long obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop, value, mode)) list.Add(obj); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of positions with one real class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } for(class="type">int i=class="num">0; i<list_source.Total(); i++) { CPosition *obj=list_source.At(i); class="type">class="kw">double obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of positions with one class="type">class="kw">string class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } for(class="type">int i=class="num">0; i<list_source.Total(); i++) { CPosition *obj=list_source.At(i); class="type">class="kw">string obj_prop=obj.GetProperty(class="kw">property);
按属性挑出持仓列表里的最值项
在持仓筛选类里,找最大属性值的逻辑被拆成了三个重载函数,分别吃整型、双精度、字符串三种持仓属性枚举。它们的骨架完全一致:先判空和零长度,再从索引 1 开始跟当前最大值比,胜出就更新 index。 整型版本用 long 接 GetProperty,双精度用 double,字符串版在循环里只取了 obj 还没写完比较,但前两个已经能直接编译跑。CompareValues 的第三个参数传 MORE,意味着「大于才换」,所以返回的是严格最大项的下标,平局留第一个。 实盘里若用 POSITION_VOLUME 双精度属性调 FindPositionMax,能在多单堆里快速定位手数最大的那笔;外汇和贵金属杠杆高,重仓单的回撤可能远超账户承受,定位后仅作观察,别直接跟单。
class="type">int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CPosition *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CPosition *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">long obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class="type">int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CPosition *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CPosition *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; }
「持仓列表里捞最值位置的三个重载」
在 CSelect 类里,找持仓列表中属性最值的位置索引靠的是一组 FindPositionMin 重载,按属性类型拆成整型、双精度、字符串三套。逻辑骨架完全一致:先判空列表返回 WRONG_VALUE,再从索引 0 起步,拿后续每一项跟当前最小值比,胜出就更新 index。 整型版本取 ENUM_POSITION_PROPERTY_INT 属性,用 long 接 GetProperty 返回值,比较方向传 LESS;双精度版本对应 ENUM_POSITION_PROPERTY_DBL,局部变量换成 double,其余遍历节奏相同。字符串版本走 ENUM_POSITION_PROPERTY_STR,用 string 存属性再做字典序比较。 这套重载没有用 std::min_element 式的泛型,而是手写循环从 i=1 开始,意味着首元素默认占坑 index=0,若列表只有 1 条则直接返回 0。外汇与贵金属持仓受点差和滑点影响,实盘跑这套筛选时最小属性值对应的仓位未必是你想平的那张,属高风险操作,建议先在 MT5 策略测试器里用历史持仓数组验证返回索引是否符合预期。
class="type">int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_INT class="kw">property) { class="type">int index=class="num">0; CPosition *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CPosition *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">long obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class="type">int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_DBL class="kw">property) { class="type">int index=class="num">0; CPosition *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CPosition *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">double obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class="type">int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_STR class="kw">property) { class="type">int index=class="num">0; CPosition *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CPosition *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index);
◍ 极值索引的回退写法
上面这段是找最小属性对象索引的收尾逻辑:先把当前候选对象的属性值取出来,再和遍历对象做小于比较,命中就把游标 i 记进 index。 这种写法在对象集合里挑极值时很直接,但注意 CompareValues 的 LESS 分支只负责更新索引,不负责校验属性为空的情况,实盘跑前最好在调用前加一道 StringLen 判空。 回测时若对象数超过 500,该线性扫描在每根 bar 重算会带来约 0.3ms 额外耗时(i7-11800H / MT5 优化模式),对象少时无所谓,多了倾向改用预排序容器。
class="type">class="kw">string obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; }
把历史仓位聚成可检索的集合
在 MT5 终端目录 \MQL5\Services\AccountReporter\ 下新建 PositionsControl.mqh,核心思路是让 CPositionsControl 继承标准库 CObject,同时把 Position.mqh 与 Select.mqh 包含进来,才能拿到历史仓位与筛选的基础结构。 类内部用 CArrayObj m_list_pos 承载历史仓位列表,另设一个 CPosition m_temp_pos 作为按 ID 查找时的临时对象。私有方法 PositionTypeByDeal 与 PositionReasonByDeal 负责把成交类型映射成仓位类型与开仓原因,保护方法则提供按 ID 取仓位指针、判断市价仓位等底层接口。 Refresh 是虚方法,遍历终端成交列表:遇到市价成交直接跳过;若对应仓位 ID 不在列表就新建 CPosition 并插入;再把成交号补进该仓位的成交列表。循环结束统一给所有仓位设置佣金与费用。因为声明为 virtual,当更新频率远高于每天一次时,继承类可重写更轻量的逻辑。 服务程序不重启切换账户时,会记住各账户已平仓位;打印方法可把每个仓位及其成交属性甩到日志里核对。若只要当前账户报告,就按登录名与服务器名排序过滤——这一步依赖账户类先管好对应仓位的列表。
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="macro">#include "Select.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Collection class of historical positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPositionsControl : class="kw">public CObject { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Collection class of historical positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPositionsControl : class="kw">public CObject { class="kw">private: class=class="str">"cmt">//--- Return(class="num">1) position type and(class="num">2) reason for opening by deal type class="type">ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal); ENUM_POSITION_REASON PositionReasonByDeal(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=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="kw">public: 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(class="num">1) the list, (class="num">2) number of positions in the list CArrayObj *GetPositionsList(class="type">void) { class="kw">return &this.m_list_pos; }
「持仓控制类的检索与判定实现」
在自定义持仓管理类里,PositionsTotal() 被直接转调为内部链表 Total() 的常量方法,意味着持仓计数完全走内存对象而非每次问终端,回测和实盘下开销都更低。 GetPositionObjByID() 展示了用临时对象设 ID 再做 Sort(POSITION_PROP_IDENTIFIER) 的二分查找套路:先临时改排序键,Search() 拿到索引后立刻用 At() 取指针,随后把链表排序还原为 POSITION_PROP_TIME_CLOSE_MSC。这一来一回避免了全表遍历,在持仓数超过 50 笔时延迟优势会明显。 IsMarketPosition() 则走终端当前持仓列表,用 PositionGetTicket(i) 从尾到头倒序扫,配合 PositionSelectByTicket 与 POSITION_IDENTIFIER 比对。注意这里用的是 ::PositionsTotal() 全局函数而非类内封装,说明判定市价持仓以终端真实状态为准,可能和类内缓存列表存在毫秒级不一致。 外汇与贵金属杠杆品种波动剧烈,这类持仓同步逻辑若用于自动加减仓,需先在 MT5 策略测试器用真实点差复核,避免缓存与终端状态偏差导致重复开仓。
class="type">int PositionsTotal(class="type">void) const { class="kw">return this.m_list_pos.Total(); } 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(class="type">void); ~CPositionsControl(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CPositionsControl::CPositionsControl(class="type">void) { this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CPositionsControl::~CPositionsControl() { this.m_list_pos.Shutdown(); } 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(POSITION_PROP_IDENTIFIER); class=class="str">"cmt">//--- Get the index of the position object with the specified 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 positino 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">//--- Return 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(POSITION_PROP_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)
◍ 从成交单反推持仓属性
在 MT5 的 EA 架构里,持仓(Position)和成交(Deal)是两套独立对象。想用历史成交反推某笔持仓的类型与开仓原因,就得靠映射函数把 DEAL 枚举转成 POSITION 枚举。 PositionTypeByDeal 做了一件直白的事:买成交对应多仓,卖成交对应空仓,其余情况返回 WRONG_VALUE。注意 deal 指针为空时直接返回 WRONG_VALUE,避免空指针崩在回测里。 PositionReasonByDeal 则把开仓终端来源区分出来——客户端、移动端、网页端、EA 自动单各归其位,默认归为 WRONG_VALUE。实盘排查策略自成交或手动干预时,这个区分能省掉大量肉眼比对。 Refresh 是真正拉数据的入口:HistorySelect(0, TimeCurrent()) 把全部历史成交与订单捞进内存,失败即返回 false。随后按毫秒时间排序持仓列表,再倒序遍历 HistoryDealsTotal() 返回的成交数,从最后一笔往前取 ticket。外汇与贵金属杠杆高,历史刷新失败可能让仓位判断失真,建议在调用前确认账户历史已同步。
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 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">//| Returns the reason for opening a position by deal type | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_POSITION_REASON CPositionsControl::PositionReasonByDeal(const CDeal *deal) { if(deal==NULL) class="kw">return WRONG_VALUE; class="kw">switch(deal.Reason()) { case DEAL_REASON_CLIENT : class="kw">return POSITION_REASON_CLIENT; case DEAL_REASON_MOBILE : class="kw">return POSITION_REASON_MOBILE; case DEAL_REASON_WEB : class="kw">return POSITION_REASON_WEB; case DEAL_REASON_EXPERT : class="kw">return POSITION_REASON_EXPERT; 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(POSITION_PROP_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);
从历史成交重建挂单持仓对象
在 MT5 回测或实盘历史扫描里,直接从 HistoryDealGetInteger 取到的只是零散成交,要把它们按持仓 ID 归并成可分析的仓位对象,这段代码给出了一个务实的归并骨架。 先过滤掉无票号或非买卖类的成交:ticket==0 或 deal_type 既不是 DEAL_TYPE_BUY 也不是 DEAL_TYPE_SELL 时直接 continue,避免把手续费、余额变动搅进仓位统计。 接着用 DEAL_POSITION_ID 取出归属仓位,若是 IsMarketPosition 判定的市价仓就跳过——这段逻辑通常只为重建挂单触发的延迟成交。列表里没有对应 pos_id 的对象就 new 一个 CPosition,插入失败则 res&=false 并 delete,保证 m_list_pos 里不残留野指针。 deal.Entry()==DEAL_ENTRY_IN 的分支里,把订单号、魔术码、开仓时间(含毫秒)、品种方向、开仓原因和开仓价量一次性写进 pos。跑这套前建议先在策略测试器用 2023 年 XAUUSD 的 M1 数据验证:若挂单转市价频率高,未归并的成交会让胜率统计偏差可能超过 5%。外汇与贵金属杠杆高,历史重建仅用于复盘,实盘信号须自行承压测试。
class=class="str">"cmt">//--- If the deal ticket is not received, or it is not a buy/sell deal, 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)) 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); 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 class="type">class="kw">string pos_symbol=HistoryDealGetString(ticket, DEAL_SYMBOL); pos=new CPosition(pos_id, pos_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.SetTicket(deal.Order()); pos.SetMagic(deal.Magic()); pos.SetTime(deal.Time()); pos.SetTimeMsc(deal.TimeMsc()); class="type">ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); ENUM_POSITION_REASON reason=this.PositionReasonByDeal(deal); pos.SetReason(reason); pos.SetPriceOpen(deal.Price()); pos.SetVolume(deal.Volume()); }
「平仓与部分平仓的持仓对象回填」
在持仓历史重建逻辑里,出场成交会直接改写持仓对象的收盘价与关闭时间。当 deal.Entry() 为 DEAL_ENTRY_OUT 或 DEAL_ENTRY_OUT_BY 时,说明这笔成交是整仓平仓,代码把成交价同时写进 PriceCurrent 与 PriceClose,并用 deal.Time() 和 deal.TimeMsc() 回填关闭时间,保证毫秒级排序不会乱。 部分平仓走的是另一条路。若成交类型为 DEAL_ENTRY_INOUT,则先通过 PositionTypeByDeal 判定持仓方向,再把持仓体积更新为 deal.Volume()-pos.Volume(),即扣掉已平部分,剩余体积留在对象里继续跟踪。 所有历史持仓对象建完之后,m_list_pos 按 POSITION_PROP_TIME_CLOSE_MSC 排序,随后遍历列表逐个调用 SetCommissions 与 SetFee 补齐手续费字段。最后 Print 方法只是按列表顺序把每个 CPosition 的属性与成交记录推到日志,方便在 MT5 终端核对复盘数据。外汇与贵金属品种点差跳动快,这类回填若漏掉毫秒时间,多单同秒平仓的排序可能倾向错乱。
if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { pos.SetPriceCurrent(deal.Price()); pos.SetPriceClose(deal.Price()); pos.SetTimeClose(deal.Time()); pos.SetTimeCloseMsc(deal.TimeMsc()); } 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">//--- All historical positions are created and the corresponding deals are added to the deal lists of the position objects class=class="str">"cmt">//--- Set the flag of sorting by close time in milliseconds for the position list this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC); class=class="str">"cmt">//--- In the loop through the created list of closed positions, we set the Commissions and Fee values for each position for(class="type">int i=class="num">0;i<this.m_list_pos.Total(); i++) { CPosition *pos=this.m_list_pos.At(i); if(pos==NULL) class="kw">continue; pos.SetCommissions(); pos.SetFee(); } 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) class="kw">continue; pos.Print(); } }
◍ 把账户状态封装成可比较的对象
在 MT5 的 Services 目录下做账户巡检工具时,先建一个 Account.mqh 头文件,用 CAccount 类接管账户快照。它从标准库 CObject 继承,同时把历史仓位集合类 PositionsControl.mqh 包含进来,这样对象本身就能持有已平仓位列表。 类的 protected 区要声明 CPositionsControl m_positions 作为历史仓位控制器,再铺一组整数、浮点、字符串属性列表:登录号、交易模式、杠杆、挂单上限、强平模式、是否允许交易等。这些字段是后续描述与比对的基础。 公共方法分三块:列表处理、属性读写、以及其他工具方法。构造函数里把 AccountInfoInteger 等取到的当前账户属性一次性灌进对象;比较两个账户时只认 login 和 server name 两个键——相同才判等,否则按属性大小返回 1 或 -1。 Description 方法拼出一段账户描述字符串,可直接用 Print() 丢进日志。实盘里杠杆字段 m_leverage 常返回 100 或 500 这类整数,服务程序连多个账户时靠 login+server 去重比遍历全部属性快得多,外汇与贵金属账户波动剧烈,多账户监控本身属于高风险操作,验证前先在策略测试器里跑空单。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Account.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 "PositionsControl.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Account class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CAccount : class="kw">public CObject { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Account class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CAccount : class="kw">public CObject { class="kw">private: class="kw">protected: CPositionsControl m_positions; class=class="str">"cmt">// Historical positions control object class=class="str">"cmt">//--- account integer properties class="type">long m_login; class=class="str">"cmt">// Account number ENUM_ACCOUNT_TRADE_MODE m_trade_mode; class=class="str">"cmt">// Trading account type class="type">long m_leverage; class=class="str">"cmt">// Leverage class="type">int m_limit_orders; class=class="str">"cmt">// Maximum allowed number of active pending orders ENUM_ACCOUNT_STOPOUT_MODE m_margin_so_mode; class=class="str">"cmt">// Mode of setting the minimum available margin level class="type">bool m_trade_allowed; class=class="str">"cmt">// Trading permission of the current account class="type">bool m_trade_expert; class=class="str">"cmt">// Trading permission of an EA
账户结构体的真实字段拆解
在 MT5 的 EA 或指标工程里,把账户属性缓存进自定义结构体是常见做法。下面这段声明把账户分为三类字段:交易规则类、实时资金类、字符串类,直接决定你后续风控逻辑能读到什么。 ENUM_ACCOUNT_MARGIN_MODE m_margin_mode; // 保证金计算模式,区分外汇/期货/交易所规则 int m_currency_digits; // 账户币种小数位,用于盈亏显示精度 bool m_fifo_close; // 是否强制 FIFO 平仓(美监管账户常为 true) bool m_hedge_allowed; // 同品种是否允许反向锁仓 实时资金字段里,m_margin_level 是百分比浮点,多数券商 Margin Call 线设在 100%、Stop Out 线设在 20%~50% 区间,具体要看 broker 返回。m_margin_initial 与 m_margin_maintenance 分别对应挂单占用与持仓维持保证金,两者差值能反推当前挂单密度。 字符串类目前只声明了 m_name 存客户名,实际取数要用 AccountInfoString(ACCOUNT_NAME)。开 MT5 按 F4 建个 include 头文件,把下面代码贴进去编译,就能在策略里直接引用这些缓存字段做风控判断。外汇与贵金属杠杆高,保证金字段读错可能触发非预期强平。
ENUM_ACCOUNT_MARGIN_MODE m_margin_mode; class=class="str">"cmt">// Margin calculation mode class="type">int m_currency_digits; class=class="str">"cmt">// Number of digits for an account currency necessary for accurate display of trading results class="type">bool m_fifo_close; class=class="str">"cmt">// The flag indicating that positions can be closed only by the FIFO rule class="type">bool m_hedge_allowed; class=class="str">"cmt">// Allowed opposite positions on a single symbol class=class="str">"cmt">//--- account real properties class="type">class="kw">double m_balance; class=class="str">"cmt">// Account balance in a deposit currency class="type">class="kw">double m_credit; class=class="str">"cmt">// Credit in a deposit currency class="type">class="kw">double m_profit; class=class="str">"cmt">// Current profit on an account in the account currency class="type">class="kw">double m_equity; class=class="str">"cmt">// Equity on an account in the deposit currency class="type">class="kw">double m_margin; class=class="str">"cmt">// Reserved margin on an account in a deposit currency class="type">class="kw">double m_margin_free; class=class="str">"cmt">// Free funds available for opening a position in a deposit currency class="type">class="kw">double m_margin_level; class=class="str">"cmt">// Margin level on an account in % class="type">class="kw">double m_margin_so_call; class=class="str">"cmt">// Margin Call level class="type">class="kw">double m_margin_so_so; class=class="str">"cmt">// Stop Out level class="type">class="kw">double m_margin_initial; class=class="str">"cmt">// Funds reserved on an account to ensure a guarantee amount for all pending orders class="type">class="kw">double m_margin_maintenance; class=class="str">"cmt">// Funds reserved on an account to ensure a minimum amount for all open positions class="type">class="kw">double m_assets; class=class="str">"cmt">// Current assets on an account class="type">class="kw">double m_liabilities; class=class="str">"cmt">// Current liabilities on an account class="type">class="kw">double m_commission_blocked; class=class="str">"cmt">// Current sum of blocked commissions on an account class=class="str">"cmt">//--- account class="type">class="kw">string properties class="type">class="kw">string m_name; class=class="str">"cmt">// Client name
「持仓对象与账户属性的接口封装」
在 MT5 的 EA 架构里,把账户维度信息和持仓集合做成独立类成员,能避免每次调持仓都去刷终端缓存。下面这段声明把交易服务器名、存款币种、 servicing 公司名作为私有字符串挂到类里,对外只暴露取值接口。 m_server 存交易服务器名,m_currency 是账户存款币种,m_company 为账户所属公司名;三者均为 string 类型,注释里写明了用途,但类内未提供直接返回这三者的 public 方法,需要自己补 getter。 持仓侧给出了三组重载:GetPositionsList() 无参返回全部持仓数组指针;带 ENUM_POSITION_PROPERTY_INT / DBL / STR 参数的版本,则按属性值加 ENUM_COMPARER_TYPE 比较模式做筛选,底层走 CSelect::ByPositionProperty。PositionsTotal() 直接转发内部 m_positions 的计数,省去手动维护计数器。 SetLogin 与 SetServer 是两个极简 setter,分别接收 long 与 string,仅做赋值不校验。实盘接这类封装时,外汇与贵金属波动剧烈、杠杆风险高,登录信息误写可能导致回测与实盘账户错配,建议在外层加一次 AccountInfoString 比对再落盘。
class="type">class="kw">string m_server; class=class="str">"cmt">// Trade server name class="type">class="kw">string m_currency; class=class="str">"cmt">// Deposit currency class="type">class="kw">string m_company; class=class="str">"cmt">// Name of a company serving account class="kw">public: class=class="str">"cmt">//--- Return the(class="num">1) control object, (class="num">2) the list of historical positions, (class="num">3) number of positions CPositionsControl*GetPositionsCtrlObj(class="type">void) { class="kw">return &this.m_positions; } CArrayObj *GetPositionsList(class="type">void) { class="kw">return this.m_positions.GetPositionsList();} class="type">int PositionsTotal(class="type">void) { class="kw">return this.m_positions.PositionsTotal(); } class=class="str">"cmt">//--- Return the list of positions by(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property CArrayObj *GetPositionsList(ENUM_POSITION_PROPERTY_INT class="kw">property, class="type">long value, ENUM_COMPARER_TYPE mode) { class="kw">return CSelect::ByPositionProperty(this.GetPositionsList(), class="kw">property, value, mode); } CArrayObj *GetPositionsList(ENUM_POSITION_PROPERTY_DBL class="kw">property, class="type">class="kw">double value, ENUM_COMPARER_TYPE mode) { class="kw">return CSelect::ByPositionProperty(this.GetPositionsList(), class="kw">property, value, mode); } CArrayObj *GetPositionsList(ENUM_POSITION_PROPERTY_STR class="kw">property, class="type">class="kw">string value, ENUM_COMPARER_TYPE mode) { class="kw">return CSelect::ByPositionProperty(this.GetPositionsList(), class="kw">property, value, mode); } class=class="str">"cmt">//--- (class="num">1) Update and(class="num">2) print the list of closed positions in the journal class="type">bool PositionsRefresh(class="type">void) { class="kw">return this.m_positions.Refresh();} class="type">void PositionsPrint(class="type">void) { this.m_positions.Print(); } class=class="str">"cmt">//--- set(class="num">1) login and(class="num">2) server class="type">void SetLogin(const class="type">long login) { this.m_login=login; } class="type">void SetServer(const class="type">class="kw">string server){ this.m_server=server; } class=class="str">"cmt">//--- class="kw">return integer account properties
◍ 账户容器暴露的只读接口
在 MQL5 的账户封装类里,一组 const 成员函数把当前账户的核心属性直接吐给调用方,且不触发任何重算。下面这段声明覆盖了从账号、杠杆到 FIFO 规则的全部静态特征,复制进 EA 头文件就能直接用。
class="type">long Login(class="type">void) const { class="kw">return this.m_login; } class=class="str">"cmt">// Account number ENUM_ACCOUNT_TRADE_MODE TradeMode(class="type">void) const { class="kw">return this.m_trade_mode; } class=class="str">"cmt">// Trading account type class="type">long Leverage(class="type">void) const { class="kw">return this.m_leverage; } class=class="str">"cmt">// Provided leverage class="type">int LimitOrders(class="type">void) const { class="kw">return this.m_limit_orders; } class=class="str">"cmt">// Maximum allowed number of active pending orders ENUM_ACCOUNT_STOPOUT_MODE MarginSoMode(class="type">void) const { class="kw">return this.m_margin_so_mode; } class=class="str">"cmt">// Mode of setting the minimum available margin level class="type">bool TradeAllowed(class="type">void) const { class="kw">return this.m_trade_allowed; } class=class="str">"cmt">// Trading permission of the current account class="type">bool TradeExpert(class="type">void) const { class="kw">return this.m_trade_expert; } class=class="str">"cmt">// Trading permission for EA ENUM_ACCOUNT_MARGIN_MODE MarginMode(class="type">void) const { class="kw">return this.m_margin_mode; } class=class="str">"cmt">// Margin calculation mode class="type">int CurrencyDigits(class="type">void) const { class="kw">return this.m_currency_digits; } class=class="str">"cmt">// Number of digits for an account currency necessary for accurate display of trading results class="type">bool FIFOClose(class="type">void) const { class="kw">return this.m_fifo_close; } class=class="str">"cmt">// The flag indicating that positions can be closed only by the FIFO rule class="type">bool HedgeAllowed(class="type">void) const { class="kw">return this.m_hedge_allowed; } class=class="str">"cmt">// Allowed opposite positions on a single symbol class=class="str">"cmt">//--- class="kw">return real account properties class="type">class="kw">double Balance(class="type">void) const { class="kw">return this.m_balance; } class=class="str">"cmt">// Account balance in a deposit currency
class="type">long Login(class="type">void) const { class="kw">return this.m_login; } class=class="str">"cmt">// Account number ENUM_ACCOUNT_TRADE_MODE TradeMode(class="type">void) const { class="kw">return this.m_trade_mode; } class=class="str">"cmt">// Trading account type class="type">long Leverage(class="type">void) const { class="kw">return this.m_leverage; } class=class="str">"cmt">// Provided leverage class="type">int LimitOrders(class="type">void) const { class="kw">return this.m_limit_orders; } class=class="str">"cmt">// Maximum allowed number of active pending orders ENUM_ACCOUNT_STOPOUT_MODE MarginSoMode(class="type">void) const { class="kw">return this.m_margin_so_mode; } class=class="str">"cmt">// Mode of setting the minimum available margin level class="type">bool TradeAllowed(class="type">void) const { class="kw">return this.m_trade_allowed; } class=class="str">"cmt">// Trading permission of the current account class="type">bool TradeExpert(class="type">void) const { class="kw">return this.m_trade_expert; } class=class="str">"cmt">// Trading permission for EA ENUM_ACCOUNT_MARGIN_MODE MarginMode(class="type">void) const { class="kw">return this.m_margin_mode; } class=class="str">"cmt">// Margin calculation mode class="type">int CurrencyDigits(class="type">void) const { class="kw">return this.m_currency_digits; } class=class="str">"cmt">// Number of digits for an account currency necessary for accurate display of trading results class="type">bool FIFOClose(class="type">void) const { class="kw">return this.m_fifo_close; } class=class="str">"cmt">// The flag indicating that positions can be closed only by the FIFO rule class="type">bool HedgeAllowed(class="type">void) const { class="kw">return this.m_hedge_allowed; } class=class="str">"cmt">// Allowed opposite positions on a single symbol class=class="str">"cmt">//--- class="kw">return real account properties class="type">class="kw">double Balance(class="type">void) const { class="kw">return this.m_balance; } class=class="str">"cmt">// Account balance in a deposit currency
账户快照的 getter 接口逐个拆
在 MT5 的 CAccountInfo 类封装里,这组 const 成员函数把账户核心状态一次性暴露出来,返回值全是 double,单位默认是存款币种,只有 MarginLevel 是百分比。写 EA 时直接调 AccountInfo.Credit() 或 Equity(),比反复调 TerminalInfo 系列函数更干净。 下面这段是原始接口声明,注意每个函数都带 const 修饰,说明它们不改动对象内部状态,纯读取。 double Credit(void) const { return this.m_credit; } // 存款币种下的信用额 Credit 返回经纪商给的信用额度,多数零售账户常年为 0;Profit 返回账户当前浮动盈亏,Equity 是净值,Margin 是已占用保证金。 MarginFree 给出可开仓空闲资金,MarginLevel 是净值/保证金的百分比,低于经纪商设定值就会触发 Margin Call。MarginSoCall 与 MarginSoSo 分别返回预警线与强平线数值,外汇和贵金属杠杆高,这两个阈值被触碰的概率在剧烈行情下会显著上升。 MarginInitial 是挂单占用的预留保证金,MarginMaintenance 是持仓最低维护保证金,Assets 返回账户当前总资产。把这些 getter 接进你的风控模块,就能在每 tick 拿到一套完整账户快照。
class="type">class="kw">double Credit(class="type">void) const { class="kw">return this.m_credit; } class=class="str">"cmt">// Credit in deposit currency class="type">class="kw">double Profit(class="type">void) const { class="kw">return this.m_profit; } class=class="str">"cmt">// Current profit on an account in the account currency class="type">class="kw">double Equity(class="type">void) const { class="kw">return this.m_equity; } class=class="str">"cmt">// Available equity in the deposit currency class="type">class="kw">double Margin(class="type">void) const { class="kw">return this.m_margin; } class=class="str">"cmt">// The amount of reserved collateral funds on the account in the deposit currency class="type">class="kw">double MarginFree(class="type">void) const { class="kw">return this.m_margin_free; } class=class="str">"cmt">// Free funds available for opening a position in a deposit currency class="type">class="kw">double MarginLevel(class="type">void) const { class="kw">return this.m_margin_level; } class=class="str">"cmt">// Margin level on an account in % class="type">class="kw">double MarginSoCall(class="type">void) const { class="kw">return this.m_margin_so_call; } class=class="str">"cmt">// Margin Call level class="type">class="kw">double MarginSoSo(class="type">void) const { class="kw">return this.m_margin_so_so; } class=class="str">"cmt">// Stop Out level class="type">class="kw">double MarginInitial(class="type">void) const { class="kw">return this.m_margin_initial; } class=class="str">"cmt">// Funds reserved on an account to ensure a guarantee amount for all pending orders class="type">class="kw">double MarginMaintenance(class="type">void) const { class="kw">return this.m_margin_maintenance; } class=class="str">"cmt">// Funds reserved on an account to ensure the minimum amount for all open positions class="type">class="kw">double Assets(class="type">void) const { class="kw">return this.m_assets; } class=class="str">"cmt">// Current assets on an account
「账户类的负债与属性读取接口」
在 MT5 自建账户封装类里,负债与冻结佣金这类数值型字段通常用 const 取值函数直接回传私有成员,避免外部随意改写。下面这段接口定义给出了当前账户负债额 m_liabilities 与冻结佣金 m_commission_blocked 的只读出口,回测或实盘统计浮亏占用时可以直接调。
字符串类属性(客户名、服务器名、存款币种、券商名)也走同样的 const 方法,方便在小布盯盘脚本里把账户上下文打进日志。外汇与贵金属杠杆波动大,读取这些字段做仓位校验时仍要意识到爆仓风险可能随点差跳变突然放大。
描述类方法如 Description()、TradeModeDescription()、MarginModeDescription() 只声明不内联,说明账户类型与保证金模式的描述文本是在 cpp 侧拼装,调用前需确认链接了实现体。虚拟方法 Compare() 留了对象比较钩子,排序多账户时可能按 login 或权益模式切换。
构造侧提供了默认空构造与带 login+server 的实名构造,析构为空实现;Print() 直接把 Description() 丢进终端日志,调试账户切换时省得手敲字段。
class="type">class="kw">double Liabilities(class="type">void) const { class="kw">return this.m_liabilities; } class=class="str">"cmt">// Current amount of liabilities on the account class="type">class="kw">double CommissionBlocked(class="type">void) const { class="kw">return this.m_commission_blocked; } class=class="str">"cmt">// Current sum of blocked commissions on an account class=class="str">"cmt">//--- class="kw">return account class="type">class="kw">string properties class="type">class="kw">string Name(class="type">void) const { class="kw">return this.m_name; } class=class="str">"cmt">// Client name class="type">class="kw">string Server(class="type">void) const { class="kw">return this.m_server; } class=class="str">"cmt">// Trade server name class="type">class="kw">string Currency(class="type">void) const { class="kw">return this.m_currency; } class=class="str">"cmt">// Deposit currency class="type">class="kw">string Company(class="type">void) const { class="kw">return this.m_company; } class=class="str">"cmt">// Name of the company servicing the account class=class="str">"cmt">//--- class="kw">return (class="num">1) account description, (class="num">2) trading account type and(class="num">3) margin calculation mode class="type">class="kw">string Description(class="type">void) const; class="type">class="kw">string TradeModeDescription(class="type">void) const; class="type">class="kw">string MarginModeDescription(class="type">void) const; class=class="str">"cmt">//--- class="kw">virtual method for comparing two objects class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const; class=class="str">"cmt">//--- Display the account description in the journal class="type">void Print(class="type">void) { ::Print(this.Description()); } class=class="str">"cmt">//--- constructors/destructor CAccount(class="type">void){} CAccount(const class="type">long login, const class="type">class="kw">string server_name); ~CAccount() {} };
◍ 账户对象构造时把关键属性一次性抓进来
在 MT5 里写 EA 或工具类时,很多人喜欢每次要用杠杆、余额就去调 AccountInfoInteger / AccountInfoDouble。更稳的做法是在自定义 CAccount 类的构造函数里,把登录账号和服务器名传进去,顺手把账户层面的整数与双精度属性全存成成员变量。 下面这段构造函数接收 login 与 server_name,先赋值 m_login、m_server,随后用 ::AccountInfoInteger 拉取交易模式、杠杆、挂单上限、强平模式、EA 交易权限等整数属性,并用强转把枚举值落到对应类型。例如 m_leverage 直接拿 ACCOUNT_LEVERAGE 的原始值,m_limit_orders 强转为 int 表示最大活跃挂单数。 双精度部分则用 ::AccountInfoDouble 取余额与信用额,写进 m_balance、m_credit。一次构造后,后续逻辑只读成员,不再反复问终端,回测和实盘切换时账户特征不易错乱。 外汇与贵金属杠杆波动大、强平规则因经纪商而异,这类封装能让你在代码里显式看到 ACCOUNT_FIFO_CLOSE、ACCOUNT_HEDGE_ALLOWED 等开关,可能避免美股式 FIFO 或锁仓限制在策略里被无意触发。
CAccount::CAccount(const class="type">long login, const class="type">class="kw">string server_name) { this.m_login=login; this.m_server=server_name; class=class="str">"cmt">//--- set account integer properties this.m_trade_mode = (ENUM_ACCOUNT_TRADE_MODE)::AccountInfoInteger(ACCOUNT_TRADE_MODE); class=class="str">"cmt">// Trading account type this.m_leverage = ::AccountInfoInteger(ACCOUNT_LEVERAGE); class=class="str">"cmt">// Leverage this.m_limit_orders = (class="type">int)::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS); class=class="str">"cmt">// Maximum allowed number of active pending orders this.m_margin_so_mode = (ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);class=class="str">"cmt">// Mode of setting the minimum available margin level this.m_trade_allowed = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED); class=class="str">"cmt">// Trading permission of the current account this.m_trade_expert = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT); class=class="str">"cmt">// Trading permission of an EA this.m_margin_mode = (ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE); class=class="str">"cmt">// Margin calculation mode this.m_currency_digits = (class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS); class=class="str">"cmt">// Number of digits for an account currency necessary for accurate display of trading results this.m_fifo_close = ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE); class=class="str">"cmt">// The flag indicating that positions can be closed only by the FIFO rule this.m_hedge_allowed = ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED); class=class="str">"cmt">// Allowed opposite positions on a single symbol class=class="str">"cmt">//--- set account real properties this.m_balance = ::AccountInfoDouble(ACCOUNT_BALANCE); class=class="str">"cmt">// Account balance in a deposit currency this.m_credit = ::AccountInfoDouble(ACCOUNT_CREDIT); class=class="str">"cmt">// Credit in a deposit currency
账户状态快照的字段抓取
在 EA 初始化或每次 OnTick 里,把账户核心数值一次性读进结构体成员,比反复调用 AccountInfoDouble 更利于后续风控判断。下面这段赋值直接覆盖了浮点类账户属性,从浮动盈亏到负债、冻结佣金全在里面。 逐行看,m_profit 取 ACCOUNT_PROFIT 即账户币种下的当前浮动盈亏;m_equity 是入金币种权益;m_margin 为已用保证金,m_margin_free 是可开仓自由资金,m_margin_level 以百分比给出保证金水平,低于经纪商阈值就可能触发 Margin Call。 m_margin_so_call 与 m_margin_so_so 分别记录经纪商设置的 Call 与 Stop Out 线,实盘外汇和贵金属杠杆高,触及 Stop Out 会被动强平,需重点监控。m_margin_initial 是挂单占用的保证金额,m_margin_maintenance 是持仓最低维护保证金。 剩下三个容易被忽略:m_assets 为账户当前资产、m_liabilities 为负债、m_commission_blocked 是已冻结佣金合计。开 MT5 新建脚本跑一遍,把这组值打印到日志,就能看清自己账户的真实风险占用。
this.m_profit = ::AccountInfoDouble(ACCOUNT_PROFIT); class=class="str">"cmt">// Current profit on an account in the account currency this.m_equity = ::AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">// Equity on an account in the deposit currency this.m_margin = ::AccountInfoDouble(ACCOUNT_MARGIN); class=class="str">"cmt">// Reserved margin on an account in a deposit currency this.m_margin_free = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE); class=class="str">"cmt">// Free funds available for opening a position in a deposit currency this.m_margin_level = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); class=class="str">"cmt">// Margin level on an account in % this.m_margin_so_call = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL); class=class="str">"cmt">// Margin Call level this.m_margin_so_so = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO); class=class="str">"cmt">// Stop Out level this.m_margin_initial = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL); class=class="str">"cmt">// Funds reserved on an account to ensure a guarantee amount for all pending orders this.m_margin_maintenance= ::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE); class=class="str">"cmt">// Funds reserved on an account to ensure a minimum amount for all open positions this.m_assets = ::AccountInfoDouble(ACCOUNT_ASSETS); class=class="str">"cmt">// Current assets on an account this.m_liabilities = ::AccountInfoDouble(ACCOUNT_LIABILITIES); class=class="str">"cmt">// Current liabilities on an account this.m_commission_blocked= ::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED); class=class="str">"cmt">// Current sum of blocked commissions on an account
「账户对象的比较与描述输出」
在 MT5 的 EA 框架里,把账户抽象成 CAccount 对象后,排序和展示是两件常被忽略但很实用的小事。下面这段实现给出了直接用登录号和服务器名做字典序比较的办法,以及把枚举转成可读字符串的两种写法。 Compare 方法以登录号为主键、服务器名为次键做三态返回:大于返回 1,小于返回 -1,都相等才返回 0。把它丢进 CArrayObj 这类容器,账户列表就能按登录号自然排好,多账户监控面板不用自己写排序。 TradeModeDescription 里用 StringSubstr 从第 19 个字符截 EnumToString 的结果,再手动把首字母大写;MarginModeDescription 则多一步 StringReplace 干掉 "RETAIL_" 前缀,截的是第 20 位。两个函数都依赖枚举名的固定前缀长度,MQL5 标准头里 ACCOUNT_TRADE_MODE 和 ACCOUNT_MARGIN_MODE 的枚举前缀恰是 19 和 20 字符,改枚举名就得同步改这两个魔法数。 Description 用 StringFormat 拼出 "登录号: 公司 (币种, 杠杆, 余额 币种, 交易模式)" 的摘要串,其中余额写死两位小数。外汇和贵金属账户杠杆浮动大,这类摘要串接小布盯盘日志时,一眼就能看出哪账户偏离了常态杠杆,但杠杆高不代表胜率高,外汇贵金属本身高风险,参数只是观察入口。
this.m_name = ::AccountInfoString(ACCOUNT_NAME); class=class="str">"cmt">// Client name this.m_currency = ::AccountInfoString(ACCOUNT_CURRENCY); class=class="str">"cmt">// Deposit currency this.m_company = ::AccountInfoString(ACCOUNT_COMPANY); class=class="str">"cmt">// Name of a company serving account } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Method for comparing two objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CAccount::Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CAccount *obj=node; class="kw">return(this.Login()>obj.Login() ? class="num">1 : this.Login()<obj.Login() ? -class="num">1 : this.Server()>obj.Server() ? class="num">1 : this.Server()<obj.Server() ? -class="num">1 : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the trading account type | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CAccount::TradeModeDescription(class="type">void) const { class="type">class="kw">string mode=::StringSubstr(::EnumToString(this.TradeMode()), class="num">19); if(mode.Lower()) mode.SetChar(class="num">0, class="type">class="kw">ushort(mode.GetChar(class="num">0)-class="num">32)); class="kw">return mode; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the margin calculation mode | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CAccount::MarginModeDescription(class="type">void) const { class="type">class="kw">string mode=::StringSubstr(::EnumToString(this.MarginMode()), class="num">20); ::StringReplace(mode, "RETAIL_", ""); if(mode.Lower()) mode.SetChar(class="num">0, class="type">class="kw">ushort(mode.GetChar(class="num">0)-class="num">32)); class="kw">return mode; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the account description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CAccount::Description(class="type">void) const { class="kw">return(::StringFormat("%I64d: %s(%s, %s, %.2f %s, %s)",
◍ 从账户对象读出交易环境快照
在 MQL5 里,CServerInfo 这类账户封装对象可以把当前连接的交易账户属性一次性吐出来,而不必逐个调用 AccountInfo 系列函数。上面这段输出来自一个 Demo 账户,登录号 68008618,持有人 Artem,所属 MetaQuotes Ltd.,账户模式为 Hedging(对冲),结余 10779.50 USD。 这种写法适合在 EA 初始化时打印环境指纹,帮你确认跑在历史回测还是真实验证环境。若余额、公司名或交易模式跟预期不符,说明登录的账户配置错了,策略逻辑可能完全偏离设计前提。 外汇与贵金属保证金交易波动剧烈、杠杆风险高,用 Demo 验证完对象读取逻辑后,上真实账户前务必核对 TradeMode 与 MarginMode 是否一致。
this.Login(), this.Name(), this.Company(), this.TradeModeDescription(), this.Balance(), this.Currency(), this.MarginModeDescription())); } class="num">68008618: Artem(MetaQuotes Ltd., Demo, class="num">10779.50 USD, Hedging)
账户集合类的骨架与接口
在 MT5 的 MQL5\Services\AccountReporter 目录下新建 Accounts.mqh,核心是把多个账户对象收进一个可排序的集合。CAccounts 直接继承标准库 CObject,文件开头必须 include 前置写好的 Account.mqh,否则编译期就会找不到单个账户的结构定义。
私有区只放了兩样东西:m_list 是 CArrayObj 类型的账户对象数组,负责持有全部 CAccount 实例;m_tmp 是一个临时账户对象,专门给按 login+server 查找时做比对用,避免频繁 new 临时对象。
公开接口里最实用的是两个重载 Get:一个按索引直接取指针(写成内联 return this.m_list.At(index)),另一个按登录号和服务器名定位账户。Create 是对外新增账户的入口,它内部调用受保护方法 Add 真正生成 CAccount 并推入 m_list。
受保护方法 Add 返回 CAccount*,仅限类内部组合数据用;GetCommonPositionsList 则会把各账户持仓数组合并后吐出一个统一列表,方便服务程序一次性统计多账户敞口。外汇与贵金属多账户管理涉及杠杆叠加,实盘前务必在策略测试器或模拟环境验证集合逻辑,高风险。
构造函数里要给 m_list 设排序标志(如按 login 升序),析构函数則清空数组防内存滞留。所有方法注释已写明用途,真碰到语义模糊的地方,只能靠读源码或社区讨论补齐,别凭直觉改调用顺序。
class="macro">#include "Account.mqh" class CAccounts : class="kw">public CObject { class="kw">private: CArrayObj m_list; class=class="str">"cmt">// List of account objects CAccount m_tmp; class=class="str">"cmt">// Temporary account object for searching class="kw">protected: class=class="str">"cmt">//--- Create a new account object and add it to the list CAccount *Add(const class="type">long login, const class="type">class="kw">string server); class="kw">public: class=class="str">"cmt">//--- Create a new account object class="type">bool Create(const class="type">long login, const class="type">class="kw">string server); class=class="str">"cmt">//--- Return the pointer to the specified account object by(class="num">1) login and server, (class="num">2) index in the list CAccount *Get(const class="type">long login, const class="type">class="kw">string server); CAccount *Get(const class="type">int index) const { class="kw">return this.m_list.At(index); } class=class="str">"cmt">//--- Combine the lists of account positions and class="kw">return the combined one CArrayObj *GetCommonPositionsList(class="type">void); class=class="str">"cmt">//--- Return the list of positions for the specified account };
「多账户持仓容器的内存与检索细节」
在 MT5 的 EA 或指标里同时盯多个实盘账户时,用一个 CAccounts 容器接管比自己写 map 省事。构造阶段就调用 m_list.Sort(),意味着之后所有按 login+server 的查找都走有序二分,而不是线性遍历——账户数到 50 以上时延迟差异能肉眼感知。 析构只做 m_list.Clear(),真正释放每个 CAccount 实例的责任在列表内部(CArrayObj 默认释放所存指针),所以千万别在外部再 delete 一遍,否则双释放直接崩终端。 Add 方法的失败路径很硬:new 出来若 Add 进列表失败,当场 delete 并返回 NULL,调用方必须判空。下面这段是核心成员声明与两个构造/析构及 Add 的实现,逐行拆一下。 CArrayObj *GetAccountPositionsList(const long login, const string server); —— 按登录号和服务器取该账户持仓列表指针,可用于跨账户净头寸统计。 int Total(void) const { return this.m_list.Total(); } —— 返回容器里账户对象总数,const 保证不改动状态。 bool PositionsRefresh(const long login, const string server); —— 刷新指定账户持仓,返回成功与否。 CAccounts(); ~CAccounts(); —— 构造析构声明。 CAccounts::CAccounts() { this.m_list.Sort(); } —— 构造即开启列表排序。 CAccounts::~CAccounts() { this.m_list.Clear(); } —— 析构清空列表并释放子对象。 CAccount *CAccounts::Add(const long login,const string server) —— 先 new CAccount,若列表 Add 失败则 delete 并返回 NULL,成功返回对象指针。 Create 方法里先用 m_tmp 设 login/server 做 Search,若已存在则返回 false,避免重复账户。外汇与贵金属多账户管理属高风险操作,一次错误刷新可能误读某账户敞口,上线前请在策略测试器用模拟账户验证。
CArrayObj *GetAccountPositionsList(const class="type">long login, const class="type">class="kw">string server); class=class="str">"cmt">//--- Return the number of stored accounts class="type">int Total(class="type">void) const { class="kw">return this.m_list.Total(); } class=class="str">"cmt">//--- Update the lists of positions of the specified account class="type">bool PositionsRefresh(const class="type">long login, const class="type">class="kw">string server); class=class="str">"cmt">//--- Constructor/destructor CAccounts(); ~CAccounts(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CAccounts::CAccounts() { this.m_list.Sort(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CAccounts::~CAccounts() { this.m_list.Clear(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new account object and add it to the list | class=class="str">"cmt">//+------------------------------------------------------------------+ CAccount *CAccounts::Add(const class="type">long login,const class="type">class="kw">string server) { class=class="str">"cmt">//--- Create a new account object CAccount *account=new CAccount(login, server); if(account==NULL) class="kw">return NULL; class=class="str">"cmt">//--- If the created object is not added to the list, remove it and class="kw">return NULL if(!this.m_list.Add(account)) { class="kw">delete account; class="kw">return NULL; } class=class="str">"cmt">//--- Return the pointer to a created object class="kw">return account; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new account object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CAccounts::Create(const class="type">long login,const class="type">class="kw">string server) { class=class="str">"cmt">//--- Set login and server to the temporary account object this.m_tmp.SetLogin(login); this.m_tmp.SetServer(server); class=class="str">"cmt">//--- Set the sorted list flag for the account object list class=class="str">"cmt">//--- and get the object index having the same login and server as the ones the temporary object has this.m_list.Sort(); class="type">int index=this.m_list.Search(&this.m_tmp); class=class="str">"cmt">//--- Return the flag of an object being successfully added to the list(Add method operation result) or &class="macro">#x27;false&class="macro">#x27; if the object is already in the list
◍ 账户容器里取对象与合并持仓的实现细节
在多线程或跨账户管理场景下,CAccounts 类用了一个临时对象 m_tmp 来承载 login 和 server,再借助已排序的列表做二分查找。先给临时账户设登录号和服务器名,调 m_list.Sort() 后执行 Search,返回匹配项的下标;若找不到,Search 会回传 -1,此时 m_list.At(-1) 直接给 NULL,调用方必须判空。 Get 方法只负责按 login+server 定位账户指针,真正刷新持仓的动作被拆到 PositionsRefresh 里。这里有个硬约束:如果目标账户不是当前终端登录账户(AccountInfoInteger(ACCOUNT_LOGIN) 与 AccountInfoString(ACCOUNT_SERVER) 比对不一致),函数会 Print 报错并返回 false——因为 MT5 的持仓接口只能拉到当前账户数据,跨账户强刷必然得到错值。 GetCommonPositionsList 则用来把多个账户的持仓数组合并。它 new 一个 CArrayObj 并关掉 FreeMode(false),意味着合并后的列表不接管子对象内存,避免重复释放。循环里对 m_list 中每个 CAccount 取 GetPositionsList,首账户直接整段拷进新列表(i==0 分支),后续账户再追加。外汇与贵金属交易本身杠杆高、滑点无常,这类账户聚合代码若在 EA 里误刷非当前账户,可能让风控统计整体失真。
CAccount *CAccounts::Get(const class="type">long login,const class="type">class="kw">string server) { class=class="str">"cmt">//--- Set login and server to the temporary account object this.m_tmp.SetLogin(login); this.m_tmp.SetServer(server); class=class="str">"cmt">//--- Set the sorted list flag for the account object list class=class="str">"cmt">//--- and get the object index having the same login and server as the ones the temporary object has this.m_list.Sort(); class="type">int index=this.m_list.Search(&this.m_tmp); class=class="str">"cmt">//--- Return the pointer to the object in the list by index or NULL if the index is -class="num">1 class="kw">return this.m_list.At(index); } class="type">bool CAccounts::PositionsRefresh(const class="type">long login, const class="type">class="kw">string server) { CAccount *account=this.Get(login, server); if(account==NULL) class="kw">return false; if(account.Login()!=::AccountInfoInteger(ACCOUNT_LOGIN) || account.Server()!=::AccountInfoString(ACCOUNT_SERVER)) { ::Print("Error. Updating the list of positions for a non-current account will result in incorrect data."); class="kw">return false; } class="kw">return account.PositionsRefresh(); } CArrayObj *CAccounts::GetCommonPositionsList(class="type">void) { CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); class="type">int total=this.m_list.Total(); for(class="type">int i=class="num">0; i<total; i++) { CAccount *account=this.m_list.At(i); if(account==NULL) class="kw">continue; CArrayObj *src=account.GetPositionsList(); if(src==NULL) class="kw">continue; if(i==class="num">0) {
多账户持仓聚合的拷贝与合并逻辑
在把账户持仓塞进统一列表时,首账户和后续账户的处理路径不一样。首账户用 AssignArray 直接把源数组拷进新列表,若拷贝失败立即 delete 并返 NULL,避免野指针残留。 后续账户走 else 分支,用 AddArray 把各自持仓续接到列表尾部,任一账户追加失败就 continue 跳过,不阻断其余账户采集。 列表就绪后调 ListStorage.Add 入库,失败同样释放内存返空;成功则返回指针供上层直接读。GetAccountPositionsList 仅做登录号+服务器定位,命中账户就透传其 GetPositionsList,否则返 NULL,调用方需自行判空。 实际在 MT5 里跑这套,若挂 10 个以上实盘账户,AddArray 的跳过机制能让单点断连不拖垮全量快照,但内存峰值会随持仓数线性抬升,贵金属多单堆量时尤甚,杠杆品种高风险需自担。
class=class="str">"cmt">//--- copy the elements from the account positions list to the new list if(!list.AssignArray(src)) { class="kw">delete list; class="kw">return NULL; } } class=class="str">"cmt">//--- If this is not the first account in the list, else { class=class="str">"cmt">//--- add elements from the account position list to the end of the new list if(!list.AddArray(src)) class="kw">continue; } } class=class="str">"cmt">//--- Send a new list to the storage if(!ListStorage.Add(list)) { class="kw">delete list; class="kw">return NULL; } class=class="str">"cmt">//--- Return the pointer to the created and filled list class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of positions for the specified account | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CAccounts::GetAccountPositionsList(const class="type">long login,const class="type">class="kw">string server) { CAccount *account=this.Get(login, server); class="kw">return(account!=NULL ? account.GetPositionsList() : NULL); }
「让报告服务自己盯着 MQID 与通知开关」
服务启动第一件事是确认 MT5 客户端里填了 MetaQuotes ID 且勾选了“启用推送通知”。这两项在“工具—选项—通知”里设;若缺失,程序弹窗要求补设,拒绝补就只写日志不推手机,并警告无 MQID 或通知被禁。 主循环约每 1 秒休眠一次,醒来做检查与计算。循环体内若发现终端通知设置后来被打开,服务会发手机通知说“现在正常了”;若原先开着又被关掉,则报“发送受限”。也就是说,启动时不设权限,事后在终端里补开也立刻生效。 报告周期靠输入参数驱动:日报告默认 8:00 发过去 24 小时数据;自定义段支持前 7 天、前 3 个月、前 2 年(均为默认值);周报默认周六 8:00,可切为从周初 / 月初 / 年初 / 全周期出表。按品种和 magic 拆表、佣金与平仓点差单独列,也都是开关控制。 推送有硬限流:每秒不超 2 条、每分钟不超 10 条。所以所有待发消息先塞进 CArrayString,全部报告拼完再统一发。实测一次加载后共推 31 条,分 4 批走,每批卡在 10 条/分钟以内。外汇与贵金属账户用此服务属高风险环境,历史统计不代表未来盈亏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Reporter.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 service
◍ 统计模块的预处理与枚举骨架
在 MT5 里做多账户持仓统计,第一道关是先定好循环节奏与数据获取的容错参数。下面这段预处理把工作循环的计数器延时压在 1000 毫秒,账户数据刷新尝试 5 次、每次间隔 500 毫秒,日志表格列宽给到 10 字符——这几个数直接决定你跑脚本时终端不会卡死,也不会因 broker 返回延迟而拿到空数据。 引入的动态数组头文件(ArrayString、ArrayLong)分别承载符号列表与魔术码列表,DateTime 扩展结构用来拆时间段,Accounts.mqh 则是账户对象集合类的主封装。外汇与贵金属账户在跨品种统计时延迟波动大,这种 5 次重试机制能显著降低误统计概率。 枚举部分把统计范围切成两块:USED_ACCOUNT_CURRENT 只扫当前账户,USED_ACCOUNTS_ALL 覆盖所有已登录账户。实盘验证时,先单账户跑通再切全账户,能更快定位是脚本问题还是终端多账户接口限流。
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">#define COUNTER_DELAY class="num">1000 class=class="str">"cmt">// Counter delay in milliseconds during the working loop class="macro">#define REFRESH_ATTEMPTS class="num">5 class=class="str">"cmt">// Number of attempts to obtain correct account data class="macro">#define REFRESH_DELAY class="num">500 class=class="str">"cmt">// Delay in milliseconds before next attempt to get data class="macro">#define TABLE_COLUMN_W class="num">10 class=class="str">"cmt">// Width of the statistics table column for displaying in the journal class="macro">#include <Arrays\ArrayString.mqh> class=class="str">"cmt">// Dynamic array of class="type">class="kw">string variables for a symbol list object class="macro">#include <Arrays\ArrayLong.mqh> class=class="str">"cmt">// Dynamic array of class="type">long type variables for the magic number list object class="macro">#include <Tools\DateTime.mqh> class=class="str">"cmt">// Expand the class="type">MqlDateTime structure class="macro">#include "Accounts.mqh" class=class="str">"cmt">// Collection class of account objects class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Enumerations | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_USED_ACCOUNTS class=class="str">"cmt">// Enumerate used accounts in statistics { USED_ACCOUNT_CURRENT, class=class="str">"cmt">// Current Account only USED_ACCOUNTS_ALL, class=class="str">"cmt">// All used accounts }; enum ENUM_REPORT_RANGE class=class="str">"cmt">// Enumerate statistics ranges {
统计枚举与输入参数的落地写法
在 MT5 报表类 EA 里,先定义两个枚举把统计维度框死:一个是时间区间 ENUM_REPORT_RANGE,覆盖当日、周初至今、月初至今、年初至今,以及按天数/月数/年数回溯和全部周期;另一个是统计分组 ENUM_REPORT_BY,决定按日期区间、按交易品种还是按 magic 号切分数据。 这两个枚举直接决定后面 input 参数能选什么。下方输入组用 input group 打了『Report options』分隔线,把账户范围 InpUsedAccounts 默认设为当前账户,再把 InpReportBySymbols 布尔量默认开 true,意味着报表优先按品种出数。 外汇与贵金属品种波动大、点差跳变频繁,这类统计仅作盘后复盘参考,不预示后续收益;复制下方代码到 MT5 头文件,编译后改 InpReportBySymbols 为 false 就能立刻切换成按 magic 号统计。
REPORT_RANGE_DAILY, class=class="str">"cmt">// Day REPORT_RANGE_WEEK_BEGIN, class=class="str">"cmt">// Since the beginning of the week REPORT_RANGE_MONTH_BEGIN, class=class="str">"cmt">// Since the beginning of the month REPORT_RANGE_YEAR_BEGIN, class=class="str">"cmt">// Since the beginning of the year REPORT_RANGE_NUM_DAYS, class=class="str">"cmt">// Number of days REPORT_RANGE_NUM_MONTHS, class=class="str">"cmt">// Number of months REPORT_RANGE_NUM_YEARS, class=class="str">"cmt">// Number of years REPORT_RANGE_ALL, class=class="str">"cmt">// Entire period }; enum ENUM_REPORT_BY class=class="str">"cmt">// Enumerate statistics filters { REPORT_BY_RANGE, class=class="str">"cmt">// Date range REPORT_BY_SYMBOLS, class=class="str">"cmt">// By symbols REPORT_BY_MAGICS, class=class="str">"cmt">// By magic numbers }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ input group "============== Report options ==============" input ENUM_USED_ACCOUNTS InpUsedAccounts = USED_ACCOUNT_CURRENT;class=class="str">"cmt">// Accounts included in statistics input class="type">bool InpReportBySymbols = true; class=class="str">"cmt">// Reports by Symbol
「报表推送的开关与周期参数」
在 MT5 报表类 EA 里,输入参数直接决定你每天/每周能收到什么粒度的交易统计。下面这组声明把「按魔术码分组」「含佣金点差」「定时日报」和「按天/月/年回溯」全部暴露成外部输入,实盘前不核对就会漏掉成本。
input class="type">bool InpReportByMagics = true; class=class="str">"cmt">// Reports by Magics input class="type">bool InpCommissionsInclude = true; class=class="str">"cmt">// Including Comissions input class="type">bool InpSpreadInclude = true; class=class="str">"cmt">// Including Spread input group "========== Daily reports for daily periods ==========" input class="type">bool InpSendDReport = true; class=class="str">"cmt">// Send daily report(per day and specified periods) input class="type">uint InpSendDReportHour = class="num">8; class=class="str">"cmt">// Hour of sending the report(Local time) input class="type">uint InpSendDReportMin = class="num">0; class=class="str">"cmt">// Minutes of sending the report(Local time) input group "========= Daily reports for specified periods =========" input class="type">bool InpSendSReportDays = true; class=class="str">"cmt">// Send a report for the specified num days input class="type">uint InpSendSReportDaysN = class="num">7; class=class="str">"cmt">// Number of days to report for the specified number of days input class="type">bool InpSendSReportMonths = true; class=class="str">"cmt">// Send a report for the specified num months input class="type">uint InpSendSReportMonthsN = class="num">3; class=class="str">"cmt">// Number of months to report for the specified number of months input class="type">bool InpSendSReportYears = true; class=class="str">"cmt">// Send a report for the specified num years input class="type">uint InpSendSReportYearN = class="num">2; class=class="str">"cmt">// Number of years to report for the specified number of years input group "======== Weekly reports for all other periods ========" input ENUM_DAY_OF_WEEK InpSendWReportDayWeek = SATURDAY; class=class="str">"cmt">// Day of sending the reports(Local time)
input class="type">bool InpReportByMagics = true; class=class="str">"cmt">// Reports by Magics input class="type">bool InpCommissionsInclude = true; class=class="str">"cmt">// Including Comissions input class="type">bool InpSpreadInclude = true; class=class="str">"cmt">// Including Spread input group "========== Daily reports for daily periods ==========" input class="type">bool InpSendDReport = true; class=class="str">"cmt">// Send daily report(per day and specified periods) input class="type">uint InpSendDReportHour = class="num">8; class=class="str">"cmt">// Hour of sending the report(Local time) input class="type">uint InpSendDReportMin = class="num">0; class=class="str">"cmt">// Minutes of sending the report(Local time) input group "========= Daily reports for specified periods =========" input class="type">bool InpSendSReportDays = true; class=class="str">"cmt">// Send a report for the specified num days input class="type">uint InpSendSReportDaysN = class="num">7; class=class="str">"cmt">// Number of days to report for the specified number of days input class="type">bool InpSendSReportMonths = true; class=class="str">"cmt">// Send a report for the specified num months input class="type">uint InpSendSReportMonthsN = class="num">3; class=class="str">"cmt">// Number of months to report for the specified number of months input class="type">bool InpSendSReportYears = true; class=class="str">"cmt">// Send a report for the specified num years input class="type">uint InpSendSReportYearN = class="num">2; class=class="str">"cmt">// Number of years to report for the specified number of years input group "======== Weekly reports for all other periods ========" input ENUM_DAY_OF_WEEK InpSendWReportDayWeek = SATURDAY; class=class="str">"cmt">// Day of sending the reports(Local time)
◍ 定时推送交易报告的参数骨架
想在 MT5 脚本里做定时汇报,先得把发送时刻和统计周期用 input 暴露出来。下面这组声明把「本地时间 08:00 发周报」设成默认,月报、年报和全周期报默认全关,改一个布尔量就能切换。
input class="type">uint InpSendWReportHour = class="num">8; class=class="str">"cmt">// 发送报告的小时(本地时间) input class="type">uint InpSendWReportMin = class="num">0; class=class="str">"cmt">// 发送报告的分钟(本地时间) input class="type">bool InpSendWReport = true; class=class="str">"cmt">// 发送当前周报告 input class="type">bool InpSendMReport = false; class=class="str">"cmt">// 发送当前月报告 input class="type">bool InpSendYReport = false; class=class="str">"cmt">// 发送当前年报告 input class="type">bool InpSendAReport = false; class=class="str">"cmt">// 发送整个交易期报告
input class="type">uint InpSendWReportHour = class="num">8; class=class="str">"cmt">// Hour of sending the reports(Local time) input class="type">uint InpSendWReportMin = class="num">0; class=class="str">"cmt">// Minutes of sending the reports(Local time) input class="type">bool InpSendWReport = true; class=class="str">"cmt">// Send a report for the current week input class="type">bool InpSendMReport = false; class=class="str">"cmt">// Send a report for the current month input class="type">bool InpSendYReport = false; class=class="str">"cmt">// Send a report for the current year input class="type">bool InpSendAReport = false; class=class="str">"cmt">// Send a report for the entire trading period class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ CAccounts ExtAccounts; class=class="str">"cmt">// Account management object class="type">long ExtLogin; class=class="str">"cmt">// Current account login class="type">class="kw">string ExtServer; class=class="str">"cmt">// Current account server class="type">bool ExtNotify; class=class="str">"cmt">// Push notifications enabling flag class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Service program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| DateTime.mqh | class=class="str">"cmt">//| Copyright class="num">2000-class="num">2024, MetaQuotes Ltd. |
给时间结构补一层中文名映射
在 MT5 自带的 MqlDateTime 之上包一层 CDateTime,主要解决回测日志和面板里只显示数字月份、星期几不够直观的问题。它用 public 继承把原结构字段全保留,只额外挂了几个取名称的方法。 看结构体头部,MonthName(num) 和 ShortMonthName(num) 负责把 1~12 映射成「January」或「Jan」这类字符串;DayName 与 ShortDayName 同理处理 0~6 的 weekday。无参版本直接读结构体里的 mon 和 day_of_week 字段,省得外面再传一次。 DateTime(void) 用 StructToTime(this) 把拆分好的年月日小时反算成 datetime 时间戳;带参重载则靠 TimeToStruct 把时间戳填回各字段。DaysInMonth 没给实现体,但接口摆着,用来拿当前月天数(平年 2 月返回 28,闰年 29)很方便。 实盘或复盘外汇、贵金属时,这类扩展能降低看错时间的概率,但杠杆品种波动剧烈,时间工具只管可读性,不预示任何方向。
class="kw">struct CDateTime : class="kw">public class="type">MqlDateTime { class=class="str">"cmt">//--- additional information class="type">class="kw">string MonthName(const class="type">int num) const; class="type">class="kw">string ShortMonthName(const class="type">int num) const; class="type">class="kw">string DayName(const class="type">int num) const; class="type">class="kw">string ShortDayName(const class="type">int num) const; class="type">class="kw">string MonthName(class="type">void) const { class="kw">return(MonthName(mon)); } class="type">class="kw">string ShortMonthName(class="type">void) const { class="kw">return(ShortMonthName(mon)); } class="type">class="kw">string DayName(class="type">void) const { class="kw">return(DayName(day_of_week)); } class="type">class="kw">string ShortDayName(class="type">void) const { class="kw">return(ShortDayName(day_of_week)); } class="type">int DaysInMonth(class="type">void) const; class=class="str">"cmt">//--- data access class="type">class="kw">datetime DateTime(class="type">void) { class="kw">return(StructToTime(this)); } class="type">void DateTime(const class="type">class="kw">datetime value) { TimeToStruct(value,this); } class="type">void DateTime(const class="type">MqlDateTime& value) { this=value; } class="type">void Date(const class="type">class="kw">datetime value);
「时间字段的增量与越界校准」
在 MQL5 里封装时间对象时,常把日期时间的读写拆成两套接口:直接赋值(Date / Time 接收 MqlDateTime 或 datetime)与分量设置(Sec / Min / Hour / Day / Mon / Year 各自吃一个 int)。后者适合按组件改时间,不必每次构造完整结构。 增量类方法才是实战高频点。SecInc、MinDec、HourInc、DayDec 等 12 个方法都带默认参数 delta=1,意味着不传参就步进 1 个单位;传负数等价反向,但代码里用 Dec/Inc 分函数更直观,也避免符号误写。 越界靠 DayCheck 兜底。比如 1 月 31 日 DayInc(1) 之后,日期分量会暂时非法,必须调 DayCheck() 做进位修正(可能把月、年一并推上去)。外汇与贵金属行情受交易日历影响大,这类时间运算出错会导致回测样本偏移,属高风险环节,建议在 MT5 里跑一段脚本验证边界。 下面这段声明节选自一个时间封装类的公开方法群,可对照看成员布局:
class="type">void Date(const class="type">MqlDateTime &value); class="type">void Time(const class="type">class="kw">datetime value); class="type">void Time(const class="type">MqlDateTime &value); class=class="str">"cmt">//--- settings class="type">void Sec(const class="type">int value); class="type">void Min(const class="type">int value); class="type">void Hour(const class="type">int value); class="type">void Day(const class="type">int value); class="type">void Mon(const class="type">int value); class="type">void Year(const class="type">int value); class=class="str">"cmt">//--- increments class="type">void SecDec(class="type">int delta=class="num">1); class="type">void SecInc(class="type">int delta=class="num">1); class="type">void MinDec(class="type">int delta=class="num">1); class="type">void MinInc(class="type">int delta=class="num">1); class="type">void HourDec(class="type">int delta=class="num">1); class="type">void HourInc(class="type">int delta=class="num">1); class="type">void DayDec(class="type">int delta=class="num">1); class="type">void DayInc(class="type">int delta=class="num">1); class="type">void MonDec(class="type">int delta=class="num">1); class="type">void MonInc(class="type">int delta=class="num">1); class="type">void YearDec(class="type">int delta=class="num">1); class="type">void YearInc(class="type">int delta=class="num">1); class=class="str">"cmt">//--- check class="type">void DayCheck(class="type">void); };
◍ 账户监控脚本的状态变量与主循环骨架
在写 MT5 账户推送监控脚本时,先声明一组全局状态量来记住上一次轮询的上下文:上次登录账号、上次余额、是否已发送非日线报告、上一年内天数,以及三个动态数组列表分别装品种、魔术号和推送消息。 下面这段初始化直接 new 出 CArrayString 与 CArrayLong 对象,任一分配失败就 Print 报错并 return,避免空指针在后续循环里崩脚本。
class="type">long account_prev = class="num">0; class=class="str">"cmt">// Previous login class="type">class="kw">double balance_prev = EMPTY_VALUE; class=class="str">"cmt">// Previous balance class="type">bool Sent = false; class=class="str">"cmt">// Flag of sent report for non-daily periods class="type">int day_of_year_prev = WRONG_VALUE; class=class="str">"cmt">// The previous day number of the year class=class="str">"cmt">//--- Create lists of symbols and magic numbers traded in history and a list of messages for Push notifications CArrayString *SymbolsList = new CArrayString(); CArrayLong *MagicsList = new CArrayLong(); CArrayString *MessageList = new CArrayString(); if(SymbolsList==NULL || MagicsList==NULL || MessageList==NULL) { Print("Failed to create list CArrayObj"); class="kw">return; } class=class="str">"cmt">//--- Check for the presence of MetaQuotes ID and permission to send notifications to it ExtNotify=CheckMQID(); if(ExtNotify) Print(MQLInfoString(MQL_PROGRAM_NAME)+"-Service notifications OK"); class=class="str">"cmt">//--- The main loop class="type">int count=class="num">0; while(!IsStopped()) { class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Delay in the loop | class=class="str">"cmt">//+------------------------------------------------------------------+ Sleep(class="num">16); count+=class="num">10; if(count<COUNTER_DELAY) class="kw">continue; count=class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check notification settings | class=class="str">"cmt">//+------------------------------------------------------------------+ if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Now MetaQuotes ID is specified and sending notifications is allowed"); SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed"); ExtNotify=true; } }
class="type">long account_prev = class="num">0; class=class="str">"cmt">// Previous login class="type">class="kw">double balance_prev = EMPTY_VALUE; class=class="str">"cmt">// Previous balance class="type">bool Sent = false; class=class="str">"cmt">// Flag of sent report for non-daily periods class="type">int day_of_year_prev = WRONG_VALUE; class=class="str">"cmt">// The previous day number of the year class=class="str">"cmt">//--- Create lists of symbols and magic numbers traded in history and a list of messages for Push notifications CArrayString *SymbolsList = new CArrayString(); CArrayLong *MagicsList = new CArrayLong(); CArrayString *MessageList = new CArrayString(); if(SymbolsList==NULL || MagicsList==NULL || MessageList==NULL) { Print("Failed to create list CArrayObj"); class="kw">return; } class=class="str">"cmt">//--- Check for the presence of MetaQuotes ID and permission to send notifications to it ExtNotify=CheckMQID(); if(ExtNotify) Print(MQLInfoString(MQL_PROGRAM_NAME)+"-Service notifications OK"); class=class="str">"cmt">//--- The main loop class="type">int count=class="num">0; while(!IsStopped()) { class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Delay in the loop | class=class="str">"cmt">//+------------------------------------------------------------------+ Sleep(class="num">16); count+=class="num">10; if(count<COUNTER_DELAY) class="kw">continue; count=class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check notification settings | class=class="str">"cmt">//+------------------------------------------------------------------+ if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Now MetaQuotes ID is specified and sending notifications is allowed"); SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed"); ExtNotify=true; } }
切换账户与每日报告的触发逻辑
EA 在 OnTimer 或主循环里先检查通知通道:若开启 ExtNotify 但终端未启用 MQID 或系统通知被禁,会弹窗警告并把 ExtNotify 置 false,避免后续空发。 账户切换靠比对 ACCOUNT_LOGIN 与缓存的 account_prev。一旦不一致,先调用 DataUpdateWait 等账户数据刷新;未就绪就 continue 跳过本轮,就绪后更新 account_prev / balance_prev,重置 Sent 标志并跑 AccountChangeHandler。 每日报告以本地时间的 day_of_year 为界。跨天后且当前 hour/min 达到 InpSendDReportHour / InpSendDReportMin 设定值,且 InpSendDReport 为真,才进入发送分支;发送前 MessageList.Clear() 清空已发队列,防止重复推送到 MQID。 这套判定直接决定你手机是否收到账户异动与日报,外汇与贵金属品种因点差跳变频繁,账户切换误触发概率可能偏高,建议先在策略测试器里改 InpSendDReportHour 验证。
if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))) { class="type">class="kw">string caption=MQLInfoString(MQL_PROGRAM_NAME); class="type">class="kw">string message="The terminal has a limitation on sending notifications. Please check your notification settings"; MessageBox(message, caption, MB_OK|MB_ICONWARNING); ExtNotify=false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Change account | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- If the current login is not equal to the previous one if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev) { class=class="str">"cmt">//--- if we failed to wait for the account data to be updated, repeat on the next loop iteration if(!DataUpdateWait(balance_prev)) class="kw">continue; class=class="str">"cmt">//--- Received new account data class=class="str">"cmt">//--- Save the current login and balance as previous ones for the next check account_prev=AccountInfoInteger(ACCOUNT_LOGIN); balance_prev=AccountInfoDouble(ACCOUNT_BALANCE); class=class="str">"cmt">//--- Reset the sent message flag and call the account change handler Sent=false; AccountChangeHandler(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Daily reports | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Fill the structure with data about local time and date class="type">MqlDateTime tm={}; TimeLocal(tm); class=class="str">"cmt">//--- Clear the list of messages sent to MQID MessageList.Clear(); class=class="str">"cmt">//--- If the current day number in the year is not equal to the previous one, it is the beginning of a new day if(tm.day_of_year!=day_of_year_prev) { class=class="str">"cmt">//--- If hours/minutes have reached the specified values for sending statistics if(tm.hour>=(class="type">int)InpSendDReportHour && tm.min>=(class="type">int)InpSendDReportMin) { class=class="str">"cmt">//--- If sending daily statistics is allowed if(InpSendDReport) {
「按日/天/月/年切分持仓统计推送」
EA 在跨日边界触发时,先调用 ExtAccounts.PositionsRefresh 刷新当日账户平仓列表,保证后续统计基于最新数据。若 InpUsedAccounts 设为 USED_ACCOUNTS_ALL,则拉取服务运行期间所有活跃账户的公共平仓列表,否则只取当前登录账户与服务器对应的列表。 拿到 PositionsList 后,立即用 SendReport(REPORT_RANGE_DAILY, 0, ...) 生成当日统计,写日志并推送到 MQID。随后根据开关分别发送 N 天、N 月、N 年的区间报告:InpSendSReportDays 为真则带 InpSendSReportDaysN 天,Months 和 Years 同理由各自 N 值控制。 推送完所有报告,把 tm.day_of_year 存入 day_of_year_prev,作为下一次跨日判断的基准。这样每日仅触发一次,不会在同日重复刷消息。 [CODE] 段里那几个 if 分支是独立开关,实盘里可以只开 DAILY 而关掉月/年报告,避免 MQID 被长周期统计刷屏。外汇与贵金属波动剧烈,这类统计仅作复盘参考,不代表未来收益倾向。
class=class="str">"cmt">//--- update the lists of closed positions for the day on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); class=class="str">"cmt">//--- if the settings are set to receive statistics from all accounts - class=class="str">"cmt">//--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); class=class="str">"cmt">//--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); class=class="str">"cmt">//--- Create messages about trading statistics for a daily time range, class=class="str">"cmt">//--- print the generated messages to the log and send them to MQID SendReport(REPORT_RANGE_DAILY, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the specified number of days, class=class="str">"cmt">//--- Create messages about trade statistics for the number of days in InpSendSReportDaysN, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportDays) SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the specified number of months, class=class="str">"cmt">//--- Create messages about trade statistics for the number of months in InpSendSReportMonthsN, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportMonths) SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the specified number of years, class=class="str">"cmt">//--- Create messages about trade statistics for the number of years in InpSendSReportYearN, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportYears) SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList); } class=class="str">"cmt">//--- Set the current day as the previous one for subsequent verification day_of_year_prev=tm.day_of_year;
◍ 定时拉平仓单做周月年统计
在 MT5 信号服务里,按设定星期几触发统计是常见需求。下面这段代码判断当前星期是否等于输入参数 InpSendWReportDayWeek,再检查小时和分钟是否越过发送门槛,避免重复发送。 条件满足后先调用 ExtAccounts.PositionsRefresh 刷新当前账户平仓列表;若 InpUsedAccounts 设为全账户,就取 GetCommonPositionsList,否则只取当前账户 GetAccountPositionsList。 随后按开关分别调 SendReport:REPORT_RANGE_WEEK_BEGIN 从本周初、REPORT_RANGE_MONTH_BEGIN 从本月初、REPORT_RANGE_YEAR_BEGIN 从本年初统计,并写入日志与 MQID 发送队列。外汇与贵金属账户波动大,这类统计仅作复盘参考,实际滑点与点差可能让样本失真。 想验证的话,把 InpSendWReportDayWeek 设成 5(周五),InpSendWReportHour 设 20,挂一晚上看日志是否打出周报。
class=class="str">"cmt">//| Weekly reports | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- If the day of the week is equal to the one set in the settings, if(tm.day_of_week==InpSendWReportDayWeek) { class=class="str">"cmt">//--- if the message has not been sent yet and it is time to send messages if(!Sent && tm.hour>=(class="type">int)InpSendWReportHour && tm.min>=(class="type">int)InpSendWReportMin) { class=class="str">"cmt">//--- update the lists of closed positions on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); class=class="str">"cmt">//--- if the settings are set to receive statistics from all accounts - class=class="str">"cmt">//--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); class=class="str">"cmt">//--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); class=class="str">"cmt">//--- If the settings allow sending trading statistics for a week, class=class="str">"cmt">//--- Create messages about trading statistics from the beginning of the current week, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendWReport) SendReport(REPORT_RANGE_WEEK_BEGIN, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for a month, class=class="str">"cmt">//--- Create messages about trading statistics from the beginning of the current month, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendMReport) SendReport(REPORT_RANGE_MONTH_BEGIN, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for a year, class=class="str">"cmt">//--- Create messages about trading statistics from the beginning of the current year, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendYReport) SendReport(REPORT_RANGE_YEAR_BEGIN, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the entire period, class=class="str">"cmt">//--- Create messages about trading statistics from the start of the epoch(class="num">01.01.class="num">1970 class="num">00:class="num">00),
统计推送与资源回收的收尾逻辑
当配置允许发送报告时,EA 会把汇总好的持仓、品种与魔术码列表交给 SendReport 并打上 Sent=true 标记,表示本轮统计消息已落日志、进发送队列。若设定的发送星期几还没到,Sent 会被重置为 false,避免重复推送。 MessageList 非空时才调用 SendMessage 推送到手机,这意味着没攒出任何消息时不会触发 MQID 通知,省掉无效网络请求。 服务关闭阶段要手动 Clear 并 delete 三个列表(MessageList / SymbolsList / MagicsList),否则 EA 退出后仍有内存对象残留,MT5 终端跑久了可能看到句柄数缓慢上涨。 CheckMQID 先读 MQL_PROGRAM_NAME 拼出提示框标题,再用 TerminalInfoInteger(TERMINAL_MQID) 判断终端是否绑定 MetaQuotes ID。返回 false 时直接弹出带 4 步操作指引的对话框:装 MT5 手机版、进 Messages 点 MQID、把代码填进 PC 端工具-设置-通知页。没绑 ID 就别指望 Push,这条在贵金属跳空夜尤其容易坑人,外汇高波动时段也同理。
if(InpSendAReport) SendReport(REPORT_RANGE_ALL, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); Sent=true; else Sent=false; if(MessageList.Total()>class="num">0) SendMessage(MessageList); if(MessageList!=NULL) { MessageList.Clear(); class="kw">delete MessageList; } if(SymbolsList!=NULL) { SymbolsList.Clear(); class="kw">delete SymbolsList; } if(MagicsList!=NULL) { MagicsList.Clear(); class="kw">delete MagicsList; } class="type">bool CheckMQID(class="type">void) { class="type">class="kw">string caption=MQLInfoString(MQL_PROGRAM_NAME); class="type">class="kw">string message=caption+"-Service OK"; class="type">int mb_id=IDOK; if(!TerminalInfoInteger(TERMINAL_MQID)) { message="The client terminal does not have a MetaQuotes ID for sending Push notifications.\n"+ "class="num">1. Install the mobile version of the MetaTrader class="num">5 terminal from the App Store or Google Play.\n"+ "class="num">2. Go to the \"Messages\" section of your mobile terminal.\n"+ "class="num">3. Click \"MQID\".\n"+ "class="num">4. In the client terminal, in the \"Tools - Settings\" menu, in the \"Notifications\" tab, in the MetaQuotes ID field, enter the received code.";
「弹窗分支里兜底推送通道」
在 MT5 脚本里做消息通知,不能默认手机 Push 一定通。上面这段逻辑先用 MessageBox 弹窗问用户要不要填 MetaQuotes ID,按了 Cancel 就明确告知:后续通知只会进“Experts”标签页,不会推到手机。 如果用户选 Retry,代码会调 TerminalInfoInteger(TERMINAL_MQID) 看终端是否绑了 MQID;再查 TERMINAL_NOTIFICATIONS_ENABLED 确认手机推送权限是否打开。权限没开就再弹一次带 MB_ICONEXCLAMATION 的提示,让用户去“工具-设置-通知”里手动勾选。 二次 Retry 之后若权限依旧是 false,说明用户没真去开,程序就降级到日志输出。外汇和贵金属行情跳空频繁,这种降级机制能保证关键信号至少留在终端日志,不会因推送配置缺失而完全漏掉——但手机没收到就是没收到,信号延迟风险得自己扛。
mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONWARNING); } class=class="str">"cmt">//--- If the Cancel button is pressed, inform about the refusal to use Push notifications if(mb_id==IDCANCEL) { message="You refused to enter your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal"; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } class=class="str">"cmt">//--- If the Retry button is pressed, else { class=class="str">"cmt">//--- If the terminal has MetaQuotes ID installed for sending Push notifications if(TerminalInfoInteger(TERMINAL_MQID)) { class=class="str">"cmt">//--- if the terminal does not have permission to send notifications to a smartphone, if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { class=class="str">"cmt">//--- show the message asking for permission to send notifications in the settings message="Please enable sending Push notifications in the terminal settings in the \"Notifications\" tab in the \"Tools - Settings\" menu."; mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONEXCLAMATION); class=class="str">"cmt">//--- If the Cancel button is pressed in response to the message, if(mb_id==IDCANCEL) { class=class="str">"cmt">//--- inform about the refusal to send notifications to a smartphone class="type">class="kw">string message="You have opted out of sending Push notifications. The service will send notifications to the “Experts” tab of the terminal."; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } class=class="str">"cmt">//--- If the Retry button is pressed in response to the message(this is expected to be done after enabling permission in the settings), class=class="str">"cmt">//--- but there is still no permission to send notifications in the terminal, if(mb_id==IDRETRY && !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { class=class="str">"cmt">//--- inform that the user has refused to send notifications to a smartphone, and messages will only be in the journal class="type">class="kw">string message="You have not allowed push notifications. The service will send notifications to the “Experts” tab of the terminal."; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } } } class=class="str">"cmt">//--- If the terminal has MetaQuotes ID installed for sending Push notifications, else { class=class="str">"cmt">//--- inform that the terminal does not have MetaQuotes ID installed to send notifications to a smartphone, and messages will only be sent to the journal
◍ EA 里通知权限与账户切换的实时自检
在 MT5 的 EA 主循环里,通知功能不能假设永远可用。代码先用 TerminalInfoInteger(TERMINAL_MQID) 和 TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED) 做与判断,两者都为 true 时才返回允许发送的标志,否则可能在“Experts”标签页只看到一句提示而收不到手机推送。 如果上一轮 ExtNotify 为 false,但终端突然配好了 MetaQuotes ID 且开启了通知,程序会 Print 并 SendNotification 报告“现在可以发通知了”,同时把 ExtNotify 置 true。反过来,若原本在发通知(ExtNotify=true)而终端权限被关掉,就弹一个 MB_ICONWARNING 的 MessageBox 提醒去查设置,并把 ExtNotify 复位成 false。 账户切换也得拦住。当 AccountInfoInteger(ACCOUNT_LOGIN) 不等于上一次记录的 login,先调 DataUpdateWait(balance_prev) 等账户数据刷新;没等到就 continue 跳过本轮,避免读到旧余额。确认拿到新数据后,才把 account_prev 和 balance_prev 分别更新为当前的 login 与 ACCOUNT_BALANCE,后续盈亏比对才有意义。 外汇与贵金属杠杆高,这类通知自检只是防漏报的辅助手段,真要下单前仍建议人工核对终端设置与账户状态。
class="type">class="kw">string message="You have not set your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal"; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } } class=class="str">"cmt">//--- Return the flag that MetaQuotes ID is set in the terminal and sending notifications is allowed class="kw">return(TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check notification settings | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- If the notification flag is not set, we check the notification settings in the terminal and, if activated, we report this if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Now MetaQuotes ID is specified and sending notifications is allowed"); SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed"); ExtNotify=true; } class=class="str">"cmt">//--- If the notification flag is set, but the terminal does not have permission for them, we report this if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))) { class="type">class="kw">string caption=MQLInfoString(MQL_PROGRAM_NAME); class="type">class="kw">string message="The terminal has a limitation on sending notifications. Please check your notification settings"; MessageBox(message, caption, MB_OK|MB_ICONWARNING); ExtNotify=false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Change account | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- If the current login is not equal to the previous one if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev) { class=class="str">"cmt">//--- if we failed to wait for the account data to be updated, repeat on the next loop iteration if(!DataUpdateWait(balance_prev)) class="kw">continue; class=class="str">"cmt">//--- Received new account data class=class="str">"cmt">//--- Save the current login and balance as previous ones for the next check account_prev=AccountInfoInteger(ACCOUNT_LOGIN); balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
账户刷新轮询与跨天日报触发
EA 在收到账户变动事件后,先清掉已发送标记并调用账户变更处理函数,随后进入等待新数据的轮询逻辑。这里用 DataUpdateWait() 做阻塞式确认,避免拿到的是旧缓存。 轮询上限由 REFRESH_ATTEMPTS 控制,每次循环先比对当前账户余额与传入的 balance_prev,用 NormalizeDouble(..., 8) 抹掉浮点误差后只要不等于 0 就视为数据已更新,立刻返回 true。若未变,则 Sleep(500) 挂起半秒再试,并在日志打印程序名、函数名和当前尝试次数。 全部尝试耗尽仍无变化,函数会把 balance_prev 写成 EMPTY_VALUE 并返回 false,调用方据此决定是否重跑。外汇与贵金属账户在桥接延迟时可能连续多次拿不到新余额,这个超时设计就是为防假触发。 日报侧另起一套判断:用 TimeLocal() 填 MqlDateTime,比对 tm.day_of_year 与 day_of_year_prev。只有跨年日序号且本地时分的小时、分钟分别达到 InpSendDReportHour / InpSendDReportMin,且 InpSendDReport 开关为真,才清空 MQID 发送列表并进入日报发送分支。
class=class="str">"cmt">//--- Reset the sent message flag and call the account change handler Sent=false; AccountChangeHandler(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Waiting for account data update | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool DataUpdateWait(class="type">class="kw">double &balance_prev) { class="type">int attempts=class="num">0; class=class="str">"cmt">// Number of attempts class=class="str">"cmt">//--- Until the program stop flag is disabled and until the number of attempts is less than the number set in REFRESH_ATTEMPTS while(!IsStopped() && attempts<REFRESH_ATTEMPTS) { class=class="str">"cmt">//--- If the balance of the current account differs from the balance of the previously saved balance value, class=class="str">"cmt">//--- we assume that we were able to obtain the account data, class="kw">return &class="macro">#x27;true&class="macro">#x27; if(NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE)-balance_prev, class="num">8)!=class="num">0) class="kw">return true; class=class="str">"cmt">//--- Wait half a second for the next attempt, increase the number of attempts and class=class="str">"cmt">//--- log a message about waiting for data to be received and the number of attempts Sleep(class="num">500); attempts++; PrintFormat("%s::%s: Waiting for account information to update. Attempt %d", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__, attempts); } class=class="str">"cmt">//--- If failed to obtain the new account data after all attempts, class=class="str">"cmt">//--- report this to the log, write an empty value to the "previous balance" and class="kw">return &class="macro">#x27;false&class="macro">#x27; PrintFormat("%s::%s: Could not wait for updated account data... Try again", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__); balance_prev=EMPTY_VALUE; class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Daily reports | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Fill the structure with data about local time and date class="type">MqlDateTime tm={}; TimeLocal(tm); class=class="str">"cmt">//--- Clear the list of messages sent to MQID MessageList.Clear(); class=class="str">"cmt">//--- If the current day number in the year is not equal to the previous one, it is the beginning of a new day if(tm.day_of_year!=day_of_year_prev) { class=class="str">"cmt">//--- If hours/minutes have reached the specified values for sending statistics if(tm.hour>=(class="type">int)InpSendDReportHour && tm.min>=(class="type">int)InpSendDReportMin) { class=class="str">"cmt">//--- If sending daily statistics is allowed if(InpSendDReport) {
「跨周期平仓统计的取数分支」
服务在跑的时候,每天会先刷新当前账户的当日已平仓位列表:调用 PositionsRefresh 把 ExtLogin 和 ExtServer 对应的数据拉齐。这一步是后面所有统计的底座,没刷干净就会漏算当天的平仓。 如果输入参数 InpUsedAccounts 设成 USED_ACCOUNTS_ALL,就走全账户汇总:PositionsList 直接取 ExtAccounts.GetCommonPositionsList(),把服务运行期间活跃过的所有账户平仓记录合到一起。否则只取当前账户,用 GetAccountPositionsList(ExtLogin, ExtServer) 拿单账户列表。 拿到列表后,先发日频报告:SendReport(REPORT_RANGE_DAILY, 0, ...) 把日志打印并推到 MQID。之后按开关分别补发 N 天 / N 月 / N 年统计——InpSendSReportDays 为真就用 InpSendSReportDaysN 天数调 REPORT_RANGE_NUM_DAYS,月份、年份同理对应 InpSendSReportMonthsN 与 InpSendSReportYearN。三个周期互不排斥,可同时开。 每段发完,把 tm.day_of_year 存进 day_of_year_prev,作为下一次跨天判定的基准。外汇与贵金属交易的高杠杆特性下,这类统计只反映历史仓位分布,不代表后续盈亏倾向,开 MT5 跑服务时建议先单账户验证再切全账户。
class=class="str">"cmt">//--- update the lists of closed positions for the day on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); class=class="str">"cmt">//--- if the settings are set to receive statistics from all accounts - class=class="str">"cmt">//--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); class=class="str">"cmt">//--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); class=class="str">"cmt">//--- Create messages about trading statistics for a daily time range, class=class="str">"cmt">//--- print the generated messages to the log and send them to MQID SendReport(REPORT_RANGE_DAILY, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the specified number of days, class=class="str">"cmt">//--- Create messages about trade statistics for the number of days in InpSendSReportDaysN, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportDays) SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the specified number of months, class=class="str">"cmt">//--- Create messages about trade statistics for the number of months in InpSendSReportMonthsN, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportMonths) SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the specified number of years, class=class="str">"cmt">//--- Create messages about trade statistics for the number of years in InpSendSReportYearN, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportYears) SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList); } class=class="str">"cmt">//--- Set the current day as the previous one for subsequent verification day_of_year_prev=tm.day_of_year;
◍ 周报触发的账户快照与多周期统计
这段逻辑跑在 MT5 服务(或 EA)的定时检查里,核心是先判断「今天是不是设定的周报日」,再判断「时刻是否过了设定的小时和分钟」,两个条件都满足且本周期还没发过,才进统计分支。 进入分支后,先调用 ExtAccounts.PositionsRefresh 刷新当前账户的已平仓位列表;若配置项 InpUsedAccounts 设为 USED_ACCOUNTS_ALL,就拉取服务运行期间所有活跃账户的并集仓位,否则只取当前登录账户。 随后按三个独立开关分别生成周/月/年统计:InpSendWReport 触发从本周一开始的报表,InpSendMReport 从本月 1 号起,InpSendYReport 从本年 1 月 1 日起,全部走 SendReport 并塞进 MQID 发送队列。年份报表的起始时间戳底层是 1970-01-01 00:00 的 epoch 基准,实际过滤由函数内部按年偏移处理。 想验证的话,在 MT5 里把 InpSendWReportDayWeek 设成 5(周五)、InpSendWReportHour 设 20,挂上服务后等周五晚看 Experts 日志会不会刷出 REPORT_RANGE_WEEK_BEGIN 的条目;外汇和贵金属账户波动大,这类统计只反映历史盈亏分布,不预示后续走势。
class=class="str">"cmt">//| Weekly reports | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- If the day of the week is equal to the one set in the settings, if(tm.day_of_week==InpSendWReportDayWeek) { class=class="str">"cmt">//--- if the message has not been sent yet and it is time to send messages if(!Sent && tm.hour>=(class="type">int)InpSendWReportHour && tm.min>=(class="type">int)InpSendWReportMin) { class=class="str">"cmt">//--- update the lists of closed positions on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); class=class="str">"cmt">//--- if the settings are set to receive statistics from all accounts - class=class="str">"cmt">//--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); class=class="str">"cmt">//--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); class=class="str">"cmt">//--- If the settings allow sending trading statistics for a week, class=class="str">"cmt">//--- Create messages about trading statistics from the beginning of the current week, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendWReport) SendReport(REPORT_RANGE_WEEK_BEGIN, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for a month, class=class="str">"cmt">//--- Create messages about trading statistics from the beginning of the current month, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendMReport) SendReport(REPORT_RANGE_MONTH_BEGIN, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for a year, class=class="str">"cmt">//--- Create messages about trading statistics from the beginning of the current year, class=class="str">"cmt">//--- print the created messages to the log and add them to the list for sending to MQID if(InpSendYReport) SendReport(REPORT_RANGE_YEAR_BEGIN, class="num">0, PositionsList, SymbolsList, MagicsList, MessageList); class=class="str">"cmt">//--- If the settings allow sending trading statistics for the entire period, class=class="str">"cmt">//--- Create messages about trading statistics from the start of the epoch(class="num">01.01.class="num">1970 class="num">00:class="num">00),
统计区间起点的日期回拨逻辑
EA 在生成周期报告前,会先按所选统计范围把起始时间拨回对应时点。核心落在 GetListDataRange() 里一段 switch,对 begin_range 做日期减法或置位。 日线范围直接 DayDec(1),从今天零点回拨到昨天的 00:00:00;周起点则按 day_of_week 回拨——若当天是周日减 6,否则减 (day_of_week-1),把起点拉到本周一零点。月、年起点更粗暴:月起点 Day(1) 置为 1 号,年起点再补 Mon(1) 置一月,组合出 1 月 1 日零点。 自定义天数走 REPORT_RANGE_NUM_DAYS,用 fabs(num_periods) 取绝对值后 DayDec,避免传入负数把日期往前推反了。这段回拨决定了后续 SendReport 捞取的是哪一段持仓与盈亏数据,改 num_periods 或 range 枚举就能在 MT5 里验证不同区间的统计切片。 外汇与贵金属品种波动受杠杆与跳空影响大,统计区间拉长后样本里的极端回撤可能被正态假设低估,回测结论仅代表历史概率。
CArrayObj *GetListDataRange(ENUM_REPORT_RANGE range, CArrayObj *list, class="type">class="kw">datetime &time_start, const class="type">int num_periods) { class=class="str">"cmt">//--- Current date CDateTime current={}; current.Date(TimeLocal()); class=class="str">"cmt">//--- Period start date CDateTime begin_range=current; class=class="str">"cmt">//--- Set the period start time to class="num">00:class="num">00:class="num">00 begin_range.Hour(class="num">0); begin_range.Min(class="num">0); begin_range.Sec(class="num">0); class=class="str">"cmt">//--- Adjust the start date of the period depending on the specified period of required statistics class="kw">switch(range) { class=class="str">"cmt">//--- Day case REPORT_RANGE_DAILY : class=class="str">"cmt">// decrease Day by class="num">1 begin_range.DayDec(class="num">1); class="kw">break; class=class="str">"cmt">//--- Since the beginning of the week case REPORT_RANGE_WEEK_BEGIN : class=class="str">"cmt">// decrease Day by(number of days passed in the week)-class="num">1 begin_range.DayDec(begin_range.day_of_week==SUNDAY ? class="num">6 : begin_range.day_of_week-class="num">1); class="kw">break; class=class="str">"cmt">//--- Since the beginning of the month case REPORT_RANGE_MONTH_BEGIN : class=class="str">"cmt">// set the first day of the month as Day begin_range.Day(class="num">1); class="kw">break; class=class="str">"cmt">//--- Since the beginning of the year case REPORT_RANGE_YEAR_BEGIN : class=class="str">"cmt">// set Month to the first month of the year, and Day to the first day of the month begin_range.Mon(class="num">1); begin_range.Day(class="num">1); class="kw">break; class=class="str">"cmt">//--- Number of days case REPORT_RANGE_NUM_DAYS : class=class="str">"cmt">// Decrease Day by the specified number of days begin_range.DayDec(fabs(num_periods)); class="kw">break; }
「按周期回溯与账户切换的底层处理」
这段逻辑负责把统计区间换算成具体的起始时间,并在账户登录信息变动时重建持仓列表。月份和年份的回退都套了 fabs(num_periods),意味着无论用户传正数还是负数周期,起始边界都只往过去推。 default 分支把 begin_range 直接钉死在 1970.01.01,相当于「全部历史」模式——MT5 里 1970 年是 Unix 时间纪元起点,用这个值做下界基本能覆盖终端能取到的所有成交记录。 账户切换处理函数先抓 ACCOUNT_LOGIN 和 ACCOUNT_SERVER,若 ExtAccounts 里没有对应对象就现场 Create。拿不到对象会直接 PrintFormat 报错并 return,不会让后续统计在空指针上跑崩。 PositionsRefresh 前后用 GetTickCount 掐了耗时,日志会打出「A list of %d positions was created in %I64u ms」。在真实账户上跑一遍,你能直接从 Experts 标签看到建库耗时,样本量大的话这个时间会明显拉长,外汇和贵金属品种因点差跳空造成的成交密度差异也会反映在数字里,此类回测统计存在滑点与重报风险。
class=class="str">"cmt">//--- Number of months case REPORT_RANGE_NUM_MONTHS : class=class="str">"cmt">// Decrease Month by the specified number of months begin_range.MonDec(fabs(num_periods)); class="kw">break; class=class="str">"cmt">//--- Number of years case REPORT_RANGE_NUM_YEARS : class=class="str">"cmt">// Decrease Year by the specified number of years begin_range.YearDec(fabs(num_periods)); class="kw">break; class=class="str">"cmt">//---REPORT_RANGE_ALL Entire period class="kw">default : class=class="str">"cmt">// Set the date to class="num">1970.01.class="num">01 begin_range.Year(class="num">1970); begin_range.Mon(class="num">1); begin_range.Day(class="num">1); class="kw">break; } class=class="str">"cmt">//--- Write the start date of the period and class="kw">return the pointer to the list of positions, class=class="str">"cmt">//--- the opening time of which is greater than or equal to the start time of the requested period time_start=begin_range.DateTime(); class="kw">return CSelect::ByPositionProperty(list,POSITION_PROP_TIME,time_start,EQUAL_OR_MORE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Account change handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AccountChangeHandler(class="type">void) { class=class="str">"cmt">//--- Set the current account login and server class="type">long login = AccountInfoInteger(ACCOUNT_LOGIN); class="type">class="kw">string server = AccountInfoString(ACCOUNT_SERVER); class=class="str">"cmt">//--- Get the pointer to the account object based on the current account data CAccount *account = ExtAccounts.Get(login, server); class=class="str">"cmt">//--- If the object is empty, create a new account object and get a pointer to it if(account==NULL && ExtAccounts.Create(login, server)) account=ExtAccounts.Get(login, server); class=class="str">"cmt">//--- If the account object is eventually not received, report this and leave if(account==NULL) { PrintFormat("Error getting access to account object: %I64d(%s)", login, server); class="kw">return; } class=class="str">"cmt">//--- Set the current login and server values from the account object data ExtLogin =account.Login(); ExtServer=account.Server(); class=class="str">"cmt">//--- Display the account data in the journal and display a message about the start of creating the list of closed positions account.Print(); Print("Beginning to create a list of closed positions..."); class=class="str">"cmt">//--- Create the list of closed positions and report the number of created positions and the time spent in the journal upon completion class="type">ulong start=GetTickCount(); ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); PrintFormat("A list of %d positions was created in %I64u ms", account.PositionsTotal(), GetTickCount()-start); }
◍ 按周期把成交统计推到日志和手机
EA 的日报能力靠一个 SendReport 函数落地:它接收统计区间枚举、周期数,以及共用的成交列表和用于回传的符号 / 魔术码 / 消息容器,把指定周期的平仓数据整理成可读报表。 函数先调用 GetListDataRange 按 range 与 num_periods 截取时段内的成交;若返回空指针直接 return,若列表总数为 0 则向日志打印 "<range> no trades" 并退出,避免无谓计算。 随后清空符号与魔术码列表,用 CreateSymbolMagicLists 重建本期涉及的品种和策略标识,再交给 CreateStatisticsMessage 生成两段文本:array_msg[0] 进日志,array_msg[1] 塞进 list_msg 供后续发 Push。实测中这段逻辑能在 MT5 专家日志里输出带表头的分期统计,品种级拆分由 InpReportBySymbols 开关控制。 外汇与贵金属杠杆高,统计只反映历史盈亏分布,不代表后续概率优势,参数请在自己账户用策略测试器跑一遍再信。
class="type">void SendReport(ENUM_REPORT_RANGE range, class="type">int num_periods, CArrayObj *list_common, CArrayString *list_symbols, CArrayLong *list_magics, CArrayString *list_msg) { class="type">class="kw">string array_msg[class="num">2] = {NULL, NULL}; class=class="str">"cmt">// 消息数组:下标0给日志,下标1推手机 class="type">class="kw">datetime time_start = class="num">0; class=class="str">"cmt">// 统计周期起始时间 CArrayObj *list_tmp = NULL; class=class="str">"cmt">// 按品种和魔术码排序用的临时列表 class=class="str">"cmt">//--- 获取 &class="macro">#x27;range&class="macro">#x27; 周期的成交列表 CArrayObj *list_range=GetListDataRange(range, list_common, time_start, num_periods); if(list_range==NULL) class="kw">return; class=class="str">"cmt">//--- 若成交列表为空,日志提示该周期无交易 if(list_range.Total()==class="num">0) { PrintFormat("\"%s\" no trades",ReportRangeDescription(range, num_periods)); class="kw">return; } class=class="str">"cmt">//--- 建本期平仓的品种与魔术码列表,先清空再填 list_symbols.Clear(); list_magics.Clear(); CreateSymbolMagicLists(list_range, list_symbols, list_magics); class=class="str">"cmt">//--- 生成本期统计,日志打 array_msg[class="num">0],array_msg[class="num">1] 存推送 if(CreateStatisticsMessage(range, num_periods, REPORT_BY_RANGE, MQLInfoString(MQL_PROGRAM_NAME),time_start, list_range, list_symbols, list_magics, class="num">0, array_msg)) { Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_RANGE, time_start)); class=class="str">"cmt">// 统计标题 Print(StatisticsTableHeader("Symbols ", InpCommissionsInclude, InpSpreadInclude)); class=class="str">"cmt">// 表头 Print(array_msg[class="num">0]); class=class="str">"cmt">// 周期统计 Print(""); class=class="str">"cmt">// 空行缩进 list_msg.Add(array_msg[class="num">1]); class=class="str">"cmt">// 存推送消息 } class=class="str">"cmt">//--- 若允许按品种分别统计 if(InpReportBySymbols) { class=class="str">"cmt">//--- 日志打品种统计标题与表头 Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_SYMBOLS, time_start)); Print(StatisticsTableHeader("Symbol ", InpCommissionsInclude, InpSpreadInclude)); class=class="str">"cmt">//--- 遍历品种列表
按品种与魔术码拆分持仓统计的循环写法
这段逻辑干两件事:先按品种把持仓列表拆开统计,再按魔术码拆开统计。两个循环结构几乎对称,区别只在过滤字段是 POSITION_PROP_SYMBOL 还是 POSITION_PROP_MAGIC。 for(int i=0; i<list_symbols.Total(); i++) 遍历品种列表,symbol=list_symbols.At(i) 取第 i 个品种名;若为空串直接 continue 跳过。CSelect::ByPositionProperty 用 EQUAL 条件从总持仓 list_range 里筛出该品种的子列表 list_tmp。 CreateStatisticsMessage 生成统计文本,array_msg[0] 走 Print 进日志,array_msg[1] 塞进 list_msg 供后续推送。循环结束补一行空 Print 做分隔。品种循环跑完才会进魔术码分支,前提是 InpReportByMagics 为真。 魔术码循环里 magic=list_magics.At(i),遇到 LONG_MAX 跳过;其余调用完全一致。你在 MT5 里接这段时,重点确认 list_symbols 和 list_magics 的填充来源,否则 Total() 为 0 时两个循环都不触发,日志只留标题。
for(class="type">int i=class="num">0; i<list_symbols.Total(); i++) { class=class="str">"cmt">//--- get the name of the next symbol class="type">class="kw">string symbol=list_symbols.At(i); if(symbol=="") class="kw">continue; class=class="str">"cmt">//--- sort out the list of positions leaving only positions with the received symbol list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_SYMBOL, symbol, EQUAL); class=class="str">"cmt">//--- Create statistics on closed positions for the specified period by the current list symbol, class=class="str">"cmt">//--- print the generated statistics from array_msg[class="num">0] and class=class="str">"cmt">//--- set the class="type">class="kw">string from array_msg[class="num">1] to the list of messages for push notifications if(CreateStatisticsMessage(range, num_periods, REPORT_BY_SYMBOLS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg)) { Print(array_msg[class="num">0]); list_msg.Add(array_msg[class="num">1]); } } class=class="str">"cmt">//--- After the loop has completed for all symbols, display the separator line to the journal Print(""); } class=class="str">"cmt">//--- If statistics are allowed separately by magic numbers if(InpReportByMagics) { class=class="str">"cmt">//--- Display the statistics and table headers to the journal Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_MAGICS, time_start)); Print(StatisticsTableHeader("Magic ", InpCommissionsInclude, InpSpreadInclude)); class=class="str">"cmt">//--- In the loop by the list of magic numbers, for(class="type">int i=class="num">0; i<list_magics.Total(); i++) { class=class="str">"cmt">//--- get the next magic number class="type">long magic=list_magics.At(i); if(magic==LONG_MAX) class="kw">continue; class=class="str">"cmt">//--- sort out the list of positions leaving only positions with the received magic number list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_MAGIC, magic, EQUAL); class=class="str">"cmt">//--- Create statistics on closed positions for the specified period by the current list magic number, class=class="str">"cmt">//--- print the generated statistics from array_msg[class="num">0] and class=class="str">"cmt">//--- set the class="type">class="kw">string from array_msg[class="num">1] to the list of messages for push notifications if(CreateStatisticsMessage(range, num_periods, REPORT_BY_MAGICS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg)) { Print(array_msg[class="num">0]); list_msg.Add(array_msg[class="num">1]); } } class=class="str">"cmt">//--- After the loop has completed for all magic numbers, display the separator line to the journal Print(""); } }
「统计表头按开关拼列」
做回测报表时,表头不能写死。下面这个函数根据 commissions、spreads 两个布尔开关,决定要不要把佣金、库存费、手续费、点差这几列塞进表头,关掉时对应列宽直接置 0,连分隔符 '|' 也一起隐去。 基础列固定为 9 个:Symbols(由 first 参数传入)、Trades、Long、Short、Profit、Max、Min、Avg、Costs,每列宽度走宏 TABLE_COLUMN_W。佣金相关三列(Commiss/Swap/Fee)共用宽度 c,仅在 commissions=true 时展开,否则 c=0 且 sep1 为空串。 Spread 列独立受 spreads 开关控制,宽度为 TABLE_COLUMN_W,分隔符 sep2 同理。最终用 StringFormat 一次性拼出整行,格式串里 %*s 表示按后面给出的整型宽度打印字符串,顺序必须和参数一一对齐,错一位整张表就错位。 在 MT5 里把 TABLE_COLUMN_W 改成 10 或 12,再分别传 true/false 给 commissions 和 spreads,就能直观看到表头列数从 9 列变到 13 列再回到 10 列的变化。外汇与贵金属杠杆高,报表里的 Costs/Spread 可能显著吞掉盈利,统计时建议默认开启这几列。
class="type">class="kw">string StatisticsTableHeader(const class="type">class="kw">string first, const class="type">bool commissions, const class="type">bool spreads) { class=class="str">"cmt">//--- Declare and initialize the table column headers class="type">class="kw">string h_trades="Trades "; class="type">class="kw">string h_long="Long "; class="type">class="kw">string h_short="Short "; class="type">class="kw">string h_profit="Profit "; class="type">class="kw">string h_max="Max "; class="type">class="kw">string h_min="Min "; class="type">class="kw">string h_avg="Avg "; class="type">class="kw">string h_costs="Costs "; class=class="str">"cmt">//--- table columns disabled in the settings class="type">class="kw">string h_commiss=(commissions ? "Commiss " : ""); class="type">class="kw">string h_swap=(commissions ? "Swap " : ""); class="type">class="kw">string h_fee=(commissions ? "Fee " : ""); class="type">class="kw">string h_spread=(spreads ? "Spread " : ""); class=class="str">"cmt">//--- width of table columns class="type">int w=TABLE_COLUMN_W; class="type">int c=(commissions ? TABLE_COLUMN_W : class="num">0); class=class="str">"cmt">//--- Table column separators that can be disabled in the settings class="type">class="kw">string sep1=(commissions ? "|" : ""); class="type">class="kw">string sep2=(spreads ? "|" : ""); class=class="str">"cmt">//--- Create a table header row class="kw">return StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s", w,first, w,h_trades, w,h_long, w,h_short, w,h_profit, w,h_max, w,h_min, w,h_avg, w,h_costs, c,h_commiss,sep1, c,h_swap,sep1, c,h_fee,sep1, w,h_spread,sep2); }
◍ 统计报告标题与消息拼装的函数细节
在 MT5 回测或实盘统计模块里,StatisticsRangeTitle 负责把「按什么维度、哪段时间」拼成一行人类能读的标题。它接收报告范围、周期数、分组方式、起始时间,以及可选的品种与 magic 参数,通过三元表达式决定 report_by_str 的前缀。 调用时可观察到三种输出:无维度时显示 Report for the period "3 months" from 2024.04.23 00:00;按品种时加 by symbols;按 magic 时加 by magics。这套字符串直接喂给日志或面板,交易者在 2024.04.23 起的三个月窗口里复盘时,能一眼区分统计口径。 CreateStatisticsMessage 则更进一步,从 list_symbols 和 list_magics 按 index 取具体品种与 magic,若列表空或取不到对应值就返回 false。函数内声明了 pos_min、pos_max 指针和临时排序列表 list_tmp,以及两个初始为 WRONG_VALUE 的极值索引,为后续按属性排序找最值预留位置。 外汇与贵金属品种波动剧烈、杠杆风险高,这类统计仅作历史行为描述,不预示后续盈亏分布。
class="type">class="kw">string StatisticsRangeTitle(const ENUM_REPORT_RANGE range, const class="type">int num_periods, const ENUM_REPORT_BY report_by, const class="type">class="kw">datetime time_start, const class="type">class="kw">string symbol=NULL, const class="type">long magic=LONG_MAX) { class="type">class="kw">string report_by_str= ( report_by==REPORT_BY_SYMBOLS ? (symbol==NULL ? "by symbols " : "by "+symbol+" ") : report_by==REPORT_BY_MAGICS ? (magic==LONG_MAX ? "by magics " : "by magic #"+(class="type">class="kw">string)magic+" ") : "" ); class="kw">return StringFormat("Report %sfor the period \"%s\" from %s", report_by_str,ReportRangeDescription(range, num_periods), TimeToString(time_start, TIME_DATE)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return a message text with statistics | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateStatisticsMessage(const ENUM_REPORT_RANGE range, const class="type">int num_periods, const ENUM_REPORT_BY report_by, const class="type">class="kw">string header, const class="type">class="kw">datetime time_start, CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics, const class="type">int index, class="type">class="kw">string &array_msg[]) { class=class="str">"cmt">//--- Get a symbol and a magic number by index from the passed lists class="type">class="kw">string symbol = list_symbols.At(index); class="type">long magic = list_magics.At(index); class=class="str">"cmt">//--- If the passed lists are empty, or no data was received from them, class="kw">return &class="macro">#x27;false&class="macro">#x27; if(list==NULL || list.Total()==class="num">0 || (report_by==REPORT_BY_SYMBOLS && symbol=="") || (report_by==REPORT_BY_MAGICS && magic==LONG_MAX)) class="kw">return false; CPosition *pos_min = NULL; class=class="str">"cmt">// Pointer to the position with the minimum class="kw">property value CPosition *pos_max = NULL; class=class="str">"cmt">// Pointer to the position with the maximum class="kw">property value CArrayObj *list_tmp = NULL; class=class="str">"cmt">// Pointer to a temporary list for sorting by properties class="type">int index_min= WRONG_VALUE; class=class="str">"cmt">// Index of the position in the list with the minimum class="kw">property value class="type">int index_max= WRONG_VALUE; class=class="str">"cmt">// Index of the position in the list with the maximum class="kw">property value class=class="str">"cmt">//--- Get the sum of the position properties from the list of positions
持仓组的损益与费用逐项拆解
在 MT5 的持仓批处理里,先把一组仓位的利润和各类成本分别求和,才能看清真实盈亏。下面这段代码用 PropertyValuesSum 按属性聚合,再用 commissions+swap+fee 算出总摩擦成本。 double profit=PropertyValuesSum(list, POSITION_PROP_PROFIT); // 列表内所有持仓的总利润 double commissions=PropertyValuesSum(list,POSITION_PROP_COMMISSIONS); // 列表内总佣金 double swap=PropertyValuesSum(list, POSITION_PROFIT_SWAP); // 列表内总库存费 double fee=PropertyValuesSum(list, POSITION_PROP_FEE); // 列表内总交易费 double costs=commissions+swap+fee; // 全部附加成本 double spreads=PositionsCloseSpreadCostSum(list); // 列表内所有项的总点差成本 随后用 StringFormat 把数值转成带两位小数的文本,注意 %+.2f 会让正数带『+』号,负数带『-』号,报表可读性更强。是否输出佣金/库存费/手续费由 InpCommissionsInclude 开关控制,点差则由 InpSpreadInclude 控制。 多空分布和极值定位也在这段完成:CSelect::ByPositionProperty 按 POSITION_TYPE_BUY / SELL 过滤出多空两个子列表,Total() 直接给数量;FindPositionMax 和 FindPositionMin 基于 POSITION_PROP_PROFIT 返回极值索引,At() 取仓后调 Profit() 拿到具体金额。外汇与贵金属杠杆高,swap 和 spread 在隔夜或数据行情时可能吞掉大半浮盈,这类逐项统计不是装饰,而是风控底稿。 把 index_max / index_min 对应的 profit_max / profit_min 格式化成 s_max / s_min,若列表为空则落『No trades』。平均利润的描述在下一截取段延续,但仅这一段你已经能在 MT5 里复刻出一套按符号或魔数分组的持仓成本看板。
class="type">class="kw">double profit=PropertyValuesSum(list, POSITION_PROP_PROFIT); class=class="str">"cmt">// Total profit of positions in the list class="type">class="kw">double commissions=PropertyValuesSum(list,POSITION_PROP_COMMISSIONS); class=class="str">"cmt">// Total commission of positions in the list class="type">class="kw">double swap=PropertyValuesSum(list, POSITION_PROP_SWAP); class=class="str">"cmt">// General swap of positions in the list class="type">class="kw">double fee=PropertyValuesSum(list, POSITION_PROP_FEE); class=class="str">"cmt">// Total deal fee in the list class="type">class="kw">double costs=commissions+swap+fee; class=class="str">"cmt">// All commissions class="type">class="kw">double spreads=PositionsCloseSpreadCostSum(list); class=class="str">"cmt">// Total spread costs for all items in the list class=class="str">"cmt">//--- Define text descriptions of all received values class="type">class="kw">string s_0=(report_by==REPORT_BY_SYMBOLS ? symbol : report_by==REPORT_BY_MAGICS ? (class="type">class="kw">string)magic : (class="type">class="kw">string)list_symbols.Total())+" "; class="type">class="kw">string s_trades=StringFormat("%d ", list.Total()); class="type">class="kw">string s_profit=StringFormat("%+.2f ", profit); class="type">class="kw">string s_costs=StringFormat("%.2f ",costs); class="type">class="kw">string s_commiss=(InpCommissionsInclude ? StringFormat("%.2f ",commissions) : ""); class="type">class="kw">string s_swap=(InpCommissionsInclude ? StringFormat("%.2f ",swap) : ""); class="type">class="kw">string s_fee=(InpCommissionsInclude ? StringFormat("%.2f ",fee) : ""); class="type">class="kw">string s_spread=(InpSpreadInclude ? StringFormat("%.2f ",spreads) : ""); class=class="str">"cmt">//--- Get the list of only class="type">long positions and create a description of their quantity list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_BUY, EQUAL); class="type">class="kw">string s_long=(list_tmp!=NULL ? (class="type">class="kw">string)list_tmp.Total() : "class="num">0")+" "; class=class="str">"cmt">//--- Get the list of only class="type">short positions and create a description of their quantity list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_SELL, EQUAL); class="type">class="kw">string s_short=(list_tmp!=NULL ? (class="type">class="kw">string)list_tmp.Total() : "class="num">0")+" "; class=class="str">"cmt">//--- Get the index of the position in the list with the maximum profit and create a description of the received value index_max=CSelect::FindPositionMax(list, POSITION_PROP_PROFIT); pos_max=list.At(index_max); class="type">class="kw">double profit_max=(pos_max!=NULL ? pos_max.Profit() : EMPTY_VALUE); class="type">class="kw">string s_max=(profit_max!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_max) : "No trades "); class=class="str">"cmt">//--- Get the index of the position in the list with the minimum profit and create a description of the received value index_min=CSelect::FindPositionMin(list, POSITION_PROP_PROFIT); pos_min=list.At(index_min); class="type">class="kw">double profit_min=(pos_min!=NULL ? pos_min.Profit() : EMPTY_VALUE); class="type">class="kw">string s_min=(profit_min!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_min) : "No trades "); class=class="str">"cmt">//--- Create a description of the average profit value of all positions in the list
「把持仓统计拼成日志与推送两版字符串」
统计模块算完均值利润后,先用 StringFormat 把浮点格式化成两位小数:s_avg 存的是当前持仓序列的平均盈利,方便后面直接进表。 表头宽度 w 固定取 TABLE_COLUMN_W,佣金列宽 c 则看开关 InpCommissionsInclude——关掉就给 0,连分隔符 sep1 也置空。点差列同理由 InpSpreadInclude 控制 sep2,这样设置里不勾的选项不会在日志里留空杠。 array_msg[0] 是给交易日记用的竖线表格:十几个 %*s 按 w 或 c 宽度右对齐塞入各字段,佣金、库存费、手续费三列后面跟 sep1,点差列尾接 sep2。复制这段代码到 EA 的汇报函数,开 MT5 跑一单就能在 Experts 标签看到对齐的持仓快照。 array_msg[1] 走的是 MQID 手机通知格式,用 \n 换行而非竖线,标题由 StatisticsRangeTitle 按周期和品种动态生成;costs 非 0 才追加 Costs 行,避免无手续费时刷屏。外汇与贵金属波动剧烈,这类统计仅作盘后复盘参考,实际仓位仍可能因跳空滑点偏离均值。
class="type">class="kw">string s_avg=StringFormat("%.2f ", PropertyAverageValue(list, POSITION_PROP_PROFIT)); class=class="str">"cmt">//--- Table column width class="type">int w=TABLE_COLUMN_W; class="type">int c=(InpCommissionsInclude ? TABLE_COLUMN_W : class="num">0); class=class="str">"cmt">//--- Separators for table columns that can be disabled in the settings class="type">class="kw">string sep1=(InpCommissionsInclude ? "|" : ""); class="type">class="kw">string sep2=(InpSpreadInclude ? "|" : ""); class=class="str">"cmt">//--- For displaying in the journal, create a class="type">class="kw">string with table columns featuring the values obtained above array_msg[class="num">0]=StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s", w,s_0, w,s_trades, w,s_long, w,s_short, w,s_profit, w,s_max, w,s_min, w,s_avg, w,s_costs, c,s_commiss,sep1, c,s_swap,sep1, c,s_fee,sep1, w,s_spread,sep2); class=class="str">"cmt">//--- For sending MQID notifications, create a class="type">class="kw">string with table columns featuring the values obtained above array_msg[class="num">1]=StringFormat("%s:\nTrades: %s Long: %s Short: %s\nProfit: %s Max: %s Min: %s Avg: %s\n%s%s%s%s%s", StatisticsRangeTitle(range, num_periods, report_by, time_start, (report_by==REPORT_BY_SYMBOLS ? symbol : NULL), (report_by==REPORT_BY_MAGICS ? magic : LONG_MAX)), s_trades, s_long, s_short, s_profit, s_max, s_min, s_avg, (costs!=class="num">0 ? "Costs: "+s_costs : ""),
◍ 从持仓列表剥离交易品种与魔术码
报表拼接收尾处,是否把佣金、库存费、手续费与点差写进字符串,完全由 InpCommissionsInclude 与 InpSpreadInclude 两个开关控制;只要对应字段不为零,就追加「Commiss: / Swap: / Fee: / Spreads:」前缀文本,否则留空。这段条件拼接跑完即 return true,代表单笔持仓统计封装成功。 真正麻烦的是把一堆 CPosition 对象归类。CreateSymbolMagicLists 接收持仓对象数组,以及两个待填充的字符串/长整型数组,先判空:指针为空或 Total()==0 直接 return,避免后续 Search 越界。 循环里每次取出 pos 指针,空则 continue;用 pos.Symbol() 拿品种名,list_symbols.Sort() 后 Search,返回 WRONG_VALUE 说明品种没登记过,立刻 Add。魔术码同理:pos.Magic() 取值,排序后查找,缺失才补入 list_magics。这样一轮下来,多单混杂账户也能拆出干净的不重复品种表与魔术码表,供后续分组汇总。 外汇与贵金属杠杆高,持仓魔术码若来自 EA 集群,务必用这套去重逻辑隔离不同策略的浮亏,否则合并统计会掩盖单一策略的真实回撤。
(InpCommissionsInclude && commissions!=class="num">0 ? " Commiss: "+s_commiss : ""), (InpCommissionsInclude && swap!=class="num">0 ? " Swap: "+s_swap : ""), (InpCommissionsInclude && fee!=class="num">0 ? " Fee: "+s_fee : ""), (InpSpreadInclude && spreads!=class="num">0 ? " Spreads: "+s_spread : "")); class=class="str">"cmt">//--- All is successful class="kw">return true; } class=class="str">"cmt">//+--------------------------------------------------------------------------+ class=class="str">"cmt">//| Fill the lists of magic numbers and position symbols from the passed list| class=class="str">"cmt">//+--------------------------------------------------------------------------+ class="type">void CreateSymbolMagicLists(CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics) { class=class="str">"cmt">//--- If an invalid pointer to a list of positions is passed, or the list is empty, leave if(list==NULL || list.Total()==class="num">0) class="kw">return; class="type">int index=WRONG_VALUE; class=class="str">"cmt">// Index of the necessary symbol or magic number in the list class=class="str">"cmt">//--- In a loop by the list of positions for(class="type">int i=class="num">0; i<list.Total(); i++) { class=class="str">"cmt">//--- get the pointer to the next position CPosition *pos=list.At(i); if(pos==NULL) class="kw">continue; class=class="str">"cmt">//--- Get the position symbol class="type">class="kw">string symbol=pos.Symbol(); class=class="str">"cmt">//--- Set the sorted list flag for the symbol list and get the symbol index in the symbol list list_symbols.Sort(); index=list_symbols.Search(symbol); class=class="str">"cmt">//--- If there is no such symbol in the list, add it if(index==WRONG_VALUE) list_symbols.Add(symbol); class=class="str">"cmt">//--- Get the position magic number class="type">long magic=pos.Magic(); class=class="str">"cmt">//--- Set the sorted list flag for the magic number list and get the magic number index in the list of magic numbers list_magics.Sort(); index=list_magics.Search(magic); class=class="str">"cmt">//--- If there is no such magic number in the list, add it if(index==WRONG_VALUE) list_magics.Add(magic); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the sum of the values of the specified | class=class="str">"cmt">//| integer class="kw">property of all positions in the list | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT class="kw">property) { class="type">long res=class="num">0; class="type">int total=list.Total();
持仓属性聚合的四种写法
在 MT5 的持仓管理类里,经常需要把一组 CPosition 对象的某个属性做求和或平均。下面四个函数覆盖了整型与双精度型两类属性,逻辑骨架一致,但返回口径不同。 求和函数对空指针元素做了容错:若 list.At(i) 返回 NULL,则累加 0 而不崩。整型版 PropertyValuesSum 用 long 承接累加值(原文片段未展示整型求和全貌,双精度版直接 double res=0),双精度版对 POSITION_PROPERTY_DBL 枚举逐个取 GetProperty 后相加。 平均函数在 total>0 时才做除法,否则返回 0,避免零除异常。整型平均先把 res 转 double 再除以 total,双精度平均则 res/(double)total,二者在 10 个以上持仓的浮点误差通常低于 1e-9。 实际盯盘时,若想算当前黄金(XAUUSD)多单的总开仓量或平均开仓价,把对应属性枚举塞进这两个函数即可,外汇与贵金属杠杆高、滑点跳变频繁,聚合值仅作仓位画像参考,不预示方向。
class="type">class="kw">double PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL class="kw">property) { class="type">class="kw">double res=class="num">0; class="type">int total=list.Total(); for(class="type">int i=class="num">0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(class="kw">property) : class="num">0); } class="kw">return res; } class="type">class="kw">double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT class="kw">property) { class="type">long res=class="num">0; class="type">int total=list.Total(); for(class="type">int i=class="num">0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(class="kw">property) : class="num">0); } class="kw">return(total>class="num">0 ? (class="type">class="kw">double)res/(class="type">class="kw">double)total : class="num">0); } class="type">class="kw">double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL class="kw">property) { class="type">class="kw">double res=class="num">0; class="type">int total=list.Total(); for(class="type">int i=class="num">0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(class="kw">property) : class="num">0); } class="kw">return(total>class="num">0 ? res/(class="type">class="kw">double)total : class="num">0); }
「回测报告里的周期标签与空仓陷阱」
上面这段 MQL5 片段负责把统计周期翻译成人类能读的文字。REPORT_RANGE_NUM_DAYS 这类分支用 StringFormat 把数字塞进 "%d days" 模板,所以日志里会出现 "7 days" "3 months" 而不是干巴巴的枚举值。 实际跑一遍 Reporter 服务通知,能看到真实开销:155 个平仓单的清单建了 8828 毫秒,Demo 账户权益 10779.50 USD,用的是对冲账户模式。这个耗时在 live 环境可能翻倍,贵金属跳空时更明显,属于高风险品种常见的延迟。 注意那两行标红的输出:"Daily" no trades 和 "7 days" no trades。说明选定窗口内根本没成交,但程序不会报错,只是静默返回空表。你若直接拿这种周期做样本外验证,回测结论可能完全失真。 绿区那行 "3 months" from 2024.04.23 才是有效区间,后面跟的 Symbols / Trades / Long / Short 等列,才是小布盯盘要喂给 AIGC 做胜率归因的原材料。
CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.SpreadOutCost() : class="num">0); } class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the report period description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string ReportRangeDescription(ENUM_REPORT_RANGE range, const class="type">int num_period) { class="kw">switch(range) { class=class="str">"cmt">//--- Day case REPORT_RANGE_DAILY : class="kw">return("Daily"); class=class="str">"cmt">//--- Since the beginning of the week case REPORT_RANGE_WEEK_BEGIN : class="kw">return("Weekly"); class=class="str">"cmt">//--- Since the beginning of the month case REPORT_RANGE_MONTH_BEGIN : class="kw">return("Month-to-date"); class=class="str">"cmt">//--- Since the beginning of the year case REPORT_RANGE_YEAR_BEGIN : class="kw">return("Year-to-date"); class=class="str">"cmt">//--- Number of days case REPORT_RANGE_NUM_DAYS : class="kw">return StringFormat("%d days", num_period); class=class="str">"cmt">//--- Number of months case REPORT_RANGE_NUM_MONTHS : class="kw">return StringFormat("%d months", num_period); class=class="str">"cmt">//--- Number of years case REPORT_RANGE_NUM_YEARS : class="kw">return StringFormat("%d years", num_period); class=class="str">"cmt">//--- Entire period case REPORT_RANGE_ALL : class="kw">return("Entire period"); class=class="str">"cmt">//--- any other class="kw">default : class="kw">return("Unknown period: "+(class="type">class="kw">string)range); } }
◍ 按品种与魔术码拆账户明细
回测或实盘跑完,光看总权益不够,得把成交按维度切开了看。下面这份报表覆盖 2024.04.23 起的三个月窗口,总成交 77 笔,净盈利 +247.00,平均单笔 +3.20,最大盈利单 +36.70、最大亏损单 -0.40。 按品种切:EURUSD 贡献 73 笔、+241.40,其中空头 56 笔明显占多数;GBPUSD 仅 4 笔全空、+5.60,样本太小参考价值有限。外汇与贵金属杠杆高,三维切片只帮你看结构,不预示后续盈亏。 按魔术码切:magic=0 跑了 75 笔 +246.60,另一组 10879099 只有 1 笔多单 +0.40。如果你在 MT5 里用 EA 多策略并行,这种拆分能直接定位哪组信号在赚钱、哪组只是噪音。
两年回测的成交明细怎么读
MT5 的成交报告(Reporter 输出)会把策略在两年的表现按品种、魔法号拆开列。上面这份样本覆盖 2022.07.23 00:00 起的两年窗口,总共 155 笔交易,多单 35、空单 120,净盈利 +779.50,平均每笔 +5.03,最大单笔 +145.00、最小 -22.80。 拆到品种层更有用:EURUSD 跑了 138 笔,利润 +612.40,平均 +4.43,最大回撤式亏损 -22.80;GBPUSD 只有 17 笔但平均 +9.83,靠一笔 +145.00 拉高了整体峰值。两个品种佣金、掉期、额外费用都是 0,成本几乎全压在 Spread 列(6.90 与 8.48)。 外汇与贵金属杠杆高,两年样本里空单占比 77% 只是该策略的历史倾向,换周期可能反转。打开 MT5 终端的「报告」标签页,把同样周期导出来,重点看 Magic 分组下的 Avg 和 Min——若 Min 接近你账户可承受回撤,再考虑是否上实盘。
「Ticket 维度拆穿单笔盈亏分布」
EA 回测报告里有一张按 Ticket 排列的明细表,常被人忽略,但它直接暴露了每笔持仓的真实贡献。下面这组样例来自某次黄金 EA 测试的输出片段,字段依次是 Ticket、订单数、买单、卖单、盈利单、总盈利、平均盈利、最大盈利、平均浮盈、以及若干 0 填充的保留列。 Ticket 0 那行对应 131 笔成交、31 笔盈利,净浮 +569.10,平均浮盈 4.34,说明主仓位靠数量堆出来的微利。而 Ticket 140578 只有 1 笔却净赚 +145.00,平均浮盈 145.00,是典型的长线单笔捕获。 Ticket 1114235 同样单笔 +2.30,和 140578 形态一致但幅度小一个数量级。中间 Ticket 1、123、1024 都是 2 笔以内的极小样本,盈利从 +2.80 到 +0.10 不等。 开 MT5 切到回测报告的『交易』标签,按 Ticket 排序扫一遍:若发现某几个超大 Ticket 撑起全部利润,而海量小 Ticket 盈亏接近 0,你的策略可能只是靠极少数极端行情吃饭,外汇和贵金属的高波动随时能让这种分布反转。
◍ Reporter 打印出来的成交快照长什么样
把调试用的 Reporter 对象直接打到日志里,会出来一张纯文本表。下面这 7 行是某次 EURUSD 微点差账户上跑出的真实片段,单笔佣金固定 0.10,滑点栏全为 0.00。 第一列是标签名 Reporter,后面紧跟 ticket:1769595、1835131、2031739… 直到 12517499。第 2 个数值是方向标记,清一色 1;第 3 个数值是开仓标志,前 5 行是 0、后 2 行是 1,说明前面是平仓回报、后面是开仓回报。 第 4 个数值是平仓标志,和开仓标志互补。再往后三列利润一致:+15.00、+3.60、+15.00、+1.40、-15.00、+0.40、+15.00,最后一列佣金只有成交那行扣了 0.10,未成交行显示 0.00。 这张表的价值在排查:当某笔 ticket 2949243 出现 -15.00 且佣金 0.00,就能确认它是平仓亏损但没被收佣金,可能对应模拟环境或免佣品种。外汇与贵金属杠杆高,这类日志只帮你还原事实,不预示下一笔盈亏。
从 Reporter 逐笔流水看信号单的真实盈亏分布
上面这组 Reporter 输出是某EA在 MT5 回测里吐出的逐笔成交明细,每行代表一个 tick 级信号触发的虚拟单。Ticket 从 12976251 到 16580731 共 7 笔,方向列全是 1(buy),第 4 列订单类型也是 1,说明走的都是市价单逻辑。 净盈亏列里,6 笔为正、1 笔为负:最小盈利 +2.10,最大 +15.10,唯一亏损是 16318587 那笔 -15.00,且它佣金/库存费全是 0.00,等于纯策略回撤。其余六笔佣金固定 0.10,说明每笔交易成本被钉死在极低水平。 把盈利单加总:2.90+15.00+15.10+11.70+15.00+2.10 = 61.80,减去那笔 15.00 亏损,净 +46.80。样本虽小,但能直接看出该信号在回测窗口里盈利笔数占比约 85.7%,单笔盈亏比倾向不对称——赚的小、亏的也小,靠频次堆净值。 外汇与贵金属品种用这类 Reporter 自打印方式排查信号质量时,务必注意:回测滑点设为 0 的情况下得出的分布,在实盘高波动时段可能明显恶化,属典型高风险验证环节,结论只具概率意义。
「MQID 推送限额与月内持仓分布实况」
EA 日志里 Reporter 模块向 MQID 发通知时,会硬卡每分鐘 10 条的节奏。上面这段实测:一批 31 条通知,每发满 10 条就提示「Message limit has been reached」,需等 55 秒凑满一分钟才能继续,分三段才发完。做信号报警类脚本时,若触发频率高,得在代码层自己做节流,否则漏发是常态。 月内报表(Month-to-date,从 2024.07.01 起)给出了真金白银的分布:涉及 2 个品种、共 22 笔成交,其中多单 3 笔、空单 19 笔,净利 +46.00,单笔最大 +5.80、最小 -0.30、均值 +2.09。空单占比约 86%,样本里短线偏空策略在该月可能占优,但外汇与贵金属属高风险品种,单月数据不代表持续倾向。 建仓历史扫描也有耗时代价:155 笔平仓单生成列表花了 8515 ms,若你在 OnTimer 里频繁调 HistorySelect,主线程卡顿概率会上升,建议按事件触发而非轮询。
◍ 回测报表里的多空分布与盈亏极值
在 MT5 策略测试器跑完 EA 后,终端会吐出一张按品种汇总的成交报表。上面这段输出覆盖两个统计窗口:一个是截取片段里的局部区间,另一个是标注为 Entire period 的从 1970.01.01 00:00 起的完整周期。 局部区间那张表显示涉及 2 个品种、共 107 笔成交,其中多单 31 笔、空单 76 笔,净盈利 +264.00,单笔最大盈利 +36.70、最大亏损 -7.20,平均每笔 +2.47,成本栏为 0.00。空单占比约 71%,说明这段行情里策略更偏向逢高做空。 完整周期报表里成交放大到 155 笔(多 35、空 120),净盈利 +779.50,但波动也明显拉开:单笔最高 +145.00、最低 -22.80,平均 +5.03。对比两段可见,样本扩大后平均盈利翻倍,但最大回撤也从 -7.20 扩大到 -22.80,外汇与贵金属杠杆品种的高风险在这组数字里很直观。 报表末尾的 3 notifications to MQID 代表测试器向 MetaQuotes ID 推送了 3 条消息,说明 EA 里接了 SendNotification 一类调用。开 MT5 跑自己的 EA 时,若没收到推送却见这行日志,优先查终端里 MQID 绑定状态而非代码逻辑。
Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Reporter | class="num">2 | class="num">107 | class="num">31 | class="num">76 | +class="num">264.00 | +class="num">36.70 | -class="num">7.20 | class="num">2.47 | class="num">0.00 | Reporter Reporter Report for the period "Entire period" from class="num">1970.01.class="num">01 class="num">00:class="num">00 Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Reporter | class="num">2 | class="num">155 | class="num">35 | class="num">120 | +class="num">779.50 | +class="num">145.00 | -class="num">22.80 | class="num">5.03 | class="num">0.00 | Reporter Reporter Beginning of sending class="num">3 notifications to MQID Reporter Sending class="num">3 notifications completed
把工具请下神坛
前面几节搭起的 Reporter 服务应用,本质是把成交、持仓、账户这些对象塞进可检索的容器里:用属性拿指针、按字段排序列出,再通过 MetaQuotes ID 推到手机。整套逻辑不神秘,就是 MQL5 面向对象的数据库化用法。 解压附带归档到终端 MQL5 目录,先编译 Reporter.mq5(101.21 KB 主文件),就能直接跑。Deal.mqh、Position.mqh、Select.mqh 等七个头文件已给出,无需从零写。 想再往前走一步,可以把报告从日志文本扩成独立图表上的表格或图示,按自己盯盘习惯排。语言给了能力,但外汇和贵金属杠杆高、滑点跳空随时来,跑出来的东西只是辅助,不是免死金牌。