图表上的历史仓位及其盈利/亏损图指标·综合运用
(3/3)·净额账户有现成函数,对冲平仓后却无历史仓位接口,这篇补齐整条链路
- 成交记录类的排序与构造细节
- 从成交单号抓取逐笔字段
- 仓位对象里到底存了哪些字段
- 把品种参数和毫秒时间揉进类里
- 把时间折算与品种可见性塞进工具类
- 交叉币种盈亏的合约换算逻辑
- 持仓对象的只读属性接口
- 持仓结构里的读写接口怎么拆
- 持仓对象的字段写入与成交挂接
- 持仓与K线时间的对齐方法
- 持仓盈亏点数与排序逻辑的实现
- 持仓对象的多字段排序分支
- 持仓对象的比较与日志输出方法
- 持仓对象构造与任意周期K线起点换算
- 持仓时间窗口与相对盈亏的判定逻辑
- 持仓盈亏的点数换算与金额还原
- 历史仓位列表的私有存储与公有接口
- 历史持仓容器的几个关键方法
- 从历史成交里重建平仓单列表
- 成交并入持仓对象时的属性赋值逻辑
- 持仓容器的方法封装与调试打印
- 历史持仓对象的构造与排序
- 用填充区把历史持仓盈亏画成通道
- 在指标主循环里抓取持仓盈亏
- 用历史持仓利润反推指标填充逻辑
- 重绘与累计显示的两个坑
- 把这条线请下神坛
「成交记录类的排序与构造细节」
在自建的成交封装类里,比较逻辑决定了历史成交如何被遍历和归并。当排序模式设为按品种时,直接比对 Symbol() 字符串,大于返回 1、小于返回 -1、相等返回 0;其余情况一律回退到 TimeMsc() 毫秒级时间戳做同样的三态比较,保证同毫秒内的成交也有稳定次序。 Print() 方法用 PrintFormat 把每笔成交的入口描述、类型描述、票据号和时间戳拼成一行日志,方便在 MT5 终端里对照策略实际触发点。时间字段特意调用 TimeMSCtoString 转成可读串,避免肉眼去算毫秒偏移。 构造函数只接收一个 deal_ticket,随后通过 HistoryDealGetInteger 把 Magic、PositionID、TimeMsc、Time 和 Type 一次性拉进成员变量。外汇与贵金属交易的高杠杆特性下,这类毫秒级成交归档对复盘滑点和重算权益曲线尤为关键,建议在策略退出前先跑一遍该类确认历史抓取无遗漏。
case DEAL_SORT_MODE_SYMBOL : class="kw">return(this.Symbol()>compared_obj.Symbol() ? class="num">1 : this.Symbol()<compared_obj.Symbol() ? -class="num">1 : class="num">0); class="kw">default : class="kw">return(this.TimeMsc()>compared_obj.TimeMsc() ? class="num">1 : this.TimeMsc()<compared_obj.TimeMsc() ? -class="num">1 : class="num">0); } } class=class="str">"cmt">//--- Print deal properties in the journal class="type">void Print(class="type">void) { ::PrintFormat(" Deal: %s type %s #%lld at %s",this.EntryDescription(),this.TypeDescription(),this.Ticket(),this.TimeMSCtoString(this.TimeMsc())); } class=class="str">"cmt">//--- Constructor CDeal(class="kw">const class="type">long deal_ticket) { this.m_ticket=deal_ticket; this.m_magic=::HistoryDealGetInteger(deal_ticket,DEAL_MAGIC); class=class="str">"cmt">// Magic number for a deal this.m_position_id=::HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID); class=class="str">"cmt">// Position ID this.m_time_msc=::HistoryDealGetInteger(deal_ticket,DEAL_TIME_MSC); class=class="str">"cmt">// Deal execution time in milliseconds this.m_time=(class="type">class="kw">datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME); class=class="str">"cmt">// Deal execution time this.m_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(deal_ticket,DEAL_TYPE); class=class="str">"cmt">// Deal type
◍ 从成交单号抓取逐笔字段
做历史回测或复盘时,单笔成交的全部成本结构都要从 HistoryDealGet 系列函数里抠出来,而不是只看一个平仓盈亏。下面这段构造函数把一张 deal_ticket 对应的关键字段一次性读进对象成员,省得后面反复查历史库。 this.m_entry=(ENUM_DEAL_ENTRY)::HistoryDealGetInteger(deal_ticket,DEAL_ENTRY); // 成交方向:开仓、平仓还是反手 this.m_volume=::HistoryDealGetDouble(deal_ticket,DEAL_VOLUME); // 成交手数 this.m_price=::HistoryDealGetDouble(deal_ticket,DEAL_PRICE); // 成交价格 this.m_comission=::HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION); // 该笔佣金 this.m_swap=::HistoryDealGetDouble(deal_ticket,DEAL_SWAP); // 平仓时累计的库存费 this.m_profit=::HistoryDealGetDouble(deal_ticket,DEAL_PROFIT); // 该笔财务结果 this.m_fee=::HistoryDealGetDouble(deal_ticket,DEAL_FEE); // 交易费 this.m_symbol=::HistoryDealGetString(deal_ticket,DEAL_SYMBOL); // 成交品种名 注意原文里 DEAL_PRICE 的注释误写成了 Deal volume,实际取的是价格,复制代码时别被注释带偏。外汇与贵金属杠杆品种跳空频繁,swap 与 commission 在极端点差下可能吞掉短线利润,实盘前应在 MT5 策略测试器用真实历史 tick 验证字段读取是否和账户成交记录一致。
this.m_entry=(ENUM_DEAL_ENTRY)::HistoryDealGetInteger(deal_ticket,DEAL_ENTRY); class=class="str">"cmt">// Deal entry - entry in, entry out, reverse this.m_volume=::HistoryDealGetDouble(deal_ticket,DEAL_VOLUME); class=class="str">"cmt">// Deal volume this.m_price=::HistoryDealGetDouble(deal_ticket,DEAL_PRICE); class=class="str">"cmt">// Deal volume this.m_comission=::HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION); class=class="str">"cmt">// Deal commission this.m_swap=::HistoryDealGetDouble(deal_ticket,DEAL_SWAP); class=class="str">"cmt">// Accumulated swap when closing this.m_profit=::HistoryDealGetDouble(deal_ticket,DEAL_PROFIT); class=class="str">"cmt">// Deal financial result this.m_fee=::HistoryDealGetDouble(deal_ticket,DEAL_FEE); class=class="str">"cmt">// Deal fee this.m_symbol=::HistoryDealGetString(deal_ticket,DEAL_SYMBOL); class=class="str">"cmt">// Name of the symbol, for which the deal is executed } ~CDeal(class="type">void){} };
仓位对象里到底存了哪些字段
做历史仓位回放前,先得把单个仓位的数据结构立住。CPosition 类在私有段声明了一组成员变量,用来缓存仓位的生命周期属性:从开平时间(毫秒级与 datetime 双存)、magic、开平成交单号、开平价格、成交量、多空类型到交易品种与小数位,全部一次性挂在对象里。 其中 m_time_in_msc 与 m_time_out_msc 用 long 存毫秒,是为了后面按任意时间框架反推柱体开启时间时不丢精度;m_list_deals 则是 CArrayObj 类型的成交列表,承载该仓位下的所有 deal 对象。 BarOpenTime() 这类辅助方法依赖这些字段:传一个时刻进去,就能算出它落在哪个图表周期的柱开盘时间,从而判断某根柱上是否含着这个仓位。当前柱盈亏则用简化近似——拿开盘价对比当前柱收盘价算点数,再乘点值,不追溯浮动盈利全过程,只为快速取已平仓位数据。 类构造函数接收仓位 ID,对象一建出来就凭 ID 从交易属性拉数据,再由历史仓位列表类把对应 deal 塞进 m_list_deals。外汇与贵金属杠杆高,这类结构只服务于复盘统计,实盘信号请自行加风控。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Position class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPosition : class="kw">public CObject { class="kw">private: CArrayObj m_list_deals; class=class="str">"cmt">// List of position deals class="type">long m_position_id; class=class="str">"cmt">// Position ID class="type">long m_time_in_msc; class=class="str">"cmt">// Open time in milliseconds class="type">long m_time_out_msc; class=class="str">"cmt">// Close time in milliseconds class="type">long m_magic; class=class="str">"cmt">// Position magic class="type">class="kw">datetime m_time_in; class=class="str">"cmt">// Open time class="type">class="kw">datetime m_time_out; class=class="str">"cmt">// Close time class="type">ulong m_deal_in_ticket; class=class="str">"cmt">// Open deal ticket class="type">ulong m_deal_out_ticket; class=class="str">"cmt">// Close deal ticket class="type">class="kw">double m_price_in; class=class="str">"cmt">// Open price class="type">class="kw">double m_price_out; class=class="str">"cmt">// Close price class="type">class="kw">double m_volume; class=class="str">"cmt">// Position volume class="type">ENUM_POSITION_TYPE m_type; class=class="str">"cmt">// Position type class="type">class="kw">string m_symbol; class=class="str">"cmt">// Position symbol class="type">int m_digits; class=class="str">"cmt">// Symbol digits
「把品种参数和毫秒时间揉进类里」
写 EA 或指标时,把品种层面的基础变量直接挂到类成员,能少写一堆重复取数的代码。下面这几个字段就是典型:点值、合约大小、盈利币种、账户币种,一次赋值后续随调随用。 double m_point; // 品种单点价值 double m_contract_size; // 品种交易合约大小 string m_currency_profit; // 品种盈利结算币种 string m_account_currency; // 账户入金币种 MQL5 原生 TimeToString 只到秒,要做毫秒级日志就得自己拼。TimeMSCtoString 把毫秒时间戳除以 1000 丢给系统函数,余数用 IntegerToString 补三位零,输出形如 2024.01.05 13:22:01.457,回测排错时定位成交瞬间很实用。 另一处容易踩坑的是跨周期取 K 线开盘时间。BarOpenTime 对小于周线的周期直接用 time - time%PeriodSeconds(period);周线因为周一零点才对齐,要先把时间偏移 4 天再取模;月线走 MqlDateTime 分支另算。外汇和贵金属波动受数据面影响大,这类时间计算错了,统计信号就可能偏移到相邻 bar,实盘风险偏高,建议挂模拟盘先验证。
class="type">class="kw">double m_point; class=class="str">"cmt">// One symbol point value class="type">class="kw">double m_contract_size; class=class="str">"cmt">// Symbol trade contract size class="type">class="kw">string m_currency_profit; class=class="str">"cmt">// Symbol profit currency class="type">class="kw">string m_account_currency; class=class="str">"cmt">// Deposit currency class=class="str">"cmt">//--- Return time with milliseconds class="type">class="kw">string TimeMSCtoString(class="kw">const class="type">long time_msc,class="type">int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) { class="kw">return ::TimeToString(time_msc/class="num">1000,flags)+"."+::IntegerToString(time_msc%class="num">1000,class="num">3,&class="macro">#x27;class="num">0&class="macro">#x27;); } class=class="str">"cmt">//--- Calculate and class="kw">return the open time of a known bar on a specified chart period by a specified time class=class="str">"cmt">//--- ([MQL5官方文档] class="type">class="kw">datetime BarOpenTime(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time) class="kw">const { ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); class=class="str">"cmt">//--- Calculate the bar open time on periods less than W1 if(period<PERIOD_W1) class="kw">return time-time%::PeriodSeconds(period); class=class="str">"cmt">//--- Calculate the bar open time on W1 if(period==PERIOD_W1) class="kw">return time-(time+class="num">4*class="num">24*class="num">60*class="num">60)%::PeriodSeconds(period); class=class="str">"cmt">//--- Calculate the bar open time on MN1 else { class="type">MqlDateTime dt; ::ResetLastError();
◍ 把时间折算与品种可见性塞进工具类
这段代码把两个底层脏活包进了类方法:一是把 datetime 折算成「当月零点的秒数」,二是确保某个交易品种在服务器上存在并被加进 MarketWatch。前者靠 TimeToStruct 拆结构,失败就打印错误码并返 0,成功则用 time 减去当日已过秒数、再减去 (day-1) 天的秒数,24*60*60 这个常量在 MT5 里就是一天的秒数。 SymbolIsExist 先用 SymbolExist 判断自定义品种是否存在,不存在直接返 false;存在则调 SymbolSelect(symbol,true) 把它推到行情窗口,返回该调用的结果。注意 custom 变量虽声明但没被读取,属于冗余写法,复制时可删。 GetOnePointPrice 给了点值计算的第一层逻辑:若传入 time 为 0 直接返 0;若品种盈利货币与账户货币相同,直接返 m_point*m_contract_size。否则留了个 array[1] 的尾巴,准备去查「账户币+盈利币」交叉报价,这一段没写完,接下去要自己补汇率抓取。外汇与贵金属杠杆高,点值算错会放大仓位风险,建议开 MT5 把这几段贴进 EA 模板逐步断点。
if(!::TimeToStruct(time,dt)) { ::PrintFormat("%s: TimeToStruct failed. Error %lu",__FUNCTION__,::GetLastError()); class="kw">return class="num">0; } class="kw">return time-(time%(class="num">24*class="num">60*class="num">60))-(dt.day-class="num">1)*(class="num">24*class="num">60*class="num">60); } class=class="str">"cmt">//--- Return the symbol availability flag on the server. Add a symbol to MarketWatch window class="type">bool SymbolIsExist(class="kw">const class="type">class="kw">string symbol) class="kw">const { class="type">bool custom=false; if(!::SymbolExist(symbol,custom)) class="kw">return false; class="kw">return ::SymbolSelect(symbol,true); } class=class="str">"cmt">//--- Return the price of one point class="type">class="kw">double GetOnePointPrice(class="kw">const class="type">class="kw">datetime time) class="kw">const { if(time==class="num">0) class="kw">return class="num">0; class=class="str">"cmt">//--- If the symbol&class="macro">#x27;s profit currency matches the account currency, class="kw">return the contract size * Point of the symbol if(this.m_currency_profit==this.m_account_currency) class="kw">return this.m_point*this.m_contract_size; class=class="str">"cmt">//--- Otherwise, check for the presence of a symbol with the name "Account currency" + "Symbol profit currency" class="type">class="kw">double array[class="num">1];
交叉币种盈亏的合约换算逻辑
当持仓品种的盈利币种与账户币种不一致时,MT5 不会自动给你折算成账户净值,得靠代码去市场报价里找对应的交叉符号。上面这段实现先拼出「账户币+盈利币」的反向符号名,比如账户是 USD、盈利币是 JPY,就拼出 USDJPY。 若反向符号存在于市场报价且能取到指定时间的收盘价,返回合约价值 = 点值 × 合约规模 ÷ 该收盘价;取不到就返回 0。接着再拼「盈利币+账户币」的正向符号,如 JPYUSD,存在则返回值 = 点值 × 合约规模 × 收盘价。 两种符号都查不到时直接返回 0,说明当前环境缺交叉报价,手动加品种到市场观察列表通常能解决。外汇与贵金属杠杆高,这类换算误差会放大净值波动,上机前建议在策略测试器用 2023 年 EURUSD 与 USDJPY 历史 Tick 跑一遍核对。
class="type">class="kw">string reverse=this.m_account_currency+this.m_currency_profit; class=class="str">"cmt">//--- If such a symbol exists and is added to the Market Watch if(this.SymbolIsExist(reverse)) { class=class="str">"cmt">//--- If the closing price of the bar by &class="macro">#x27;time&class="macro">#x27; is received, class="kw">return the contract size * Point of the symbol divided by the Close price of the bar if(::CopyClose(reverse,PERIOD_CURRENT,time,class="num">1,array)==class="num">1 && array[class="num">0]>class="num">0) class="kw">return this.m_point*this.m_contract_size/array[class="num">0]; class=class="str">"cmt">//--- If failed to get the closing price of the bar by &class="macro">#x27;time&class="macro">#x27;, class="kw">return zero else class="kw">return class="num">0; } class=class="str">"cmt">//--- Check for the presence of a symbol with the name "Symbol profit currency" + "Account currency" class="type">class="kw">string direct=this.m_currency_profit+this.m_account_currency; class=class="str">"cmt">//--- If such a symbol exists and is added to the Market Watch if(this.SymbolIsExist(direct)) { class=class="str">"cmt">//--- If the closing price of the bar by &class="macro">#x27;time&class="macro">#x27; is received, class="kw">return the contract size * Point of the symbol multiplied by the Close price of the bar if(::CopyClose(direct,PERIOD_CURRENT,time,class="num">1,array)==class="num">1) class="kw">return this.m_point*this.m_contract_size*array[class="num">0]; } class=class="str">"cmt">//--- Failed to get symbols whose profit currency does not match the account currency, neither reverse nor direct - class="kw">return zero class="kw">return class="num">0;
「持仓对象的只读属性接口」
在自建的持仓封装类里,这类方法只做一件事:把私有成员变量暴露成只读访问器,外部调用时拿不到修改入口,只能查询。上面这组接口覆盖了从开仓到平仓的全链路标识与价格数据。 ID() 返回持仓在交易池里的唯一编号 m_position_id,Magic() 给出下单时打的魔法码 m_magic,用来区分不同策略的仓位。TimeInMsc() 与 TimeOutMsc() 以毫秒级时间戳记录开平时间,而 TimeIn()、TimeOut() 给的是 MT5 标准 datetime 类型,二者精度差约 1 毫秒级,回测时若按毫秒算持仓时长得用前者。 DealIn()、DealOut() 分别回传开仓成交单与平仓成交单的 ticket(ulong),TypePosition() 返回 ENUM_POSITION_TYPE 枚举判定多空,PriceIn() 给出开仓价 m_price_in。把这些接口直接贴在 EA 的调试面板里,能在 MT5 策略测试器跑完一单后,立刻核对成交时间与价格是否和日志对得上。 外汇与贵金属杠杆高,这类只读快照只能帮你复盘,不能预示下一笔盈亏,验证时请用在模拟环境。
class="type">long ID(class="type">void) class="kw">const { class="kw">return this.m_position_id; } class=class="str">"cmt">// Position ID class="type">long Magic(class="type">void) class="kw">const { class="kw">return this.m_magic; } class=class="str">"cmt">// Position magic class="type">long TimeInMsc(class="type">void) class="kw">const { class="kw">return this.m_time_in_msc; } class=class="str">"cmt">// Open time class="type">long TimeOutMsc(class="type">void) class="kw">const { class="kw">return this.m_time_out_msc; } class=class="str">"cmt">// Close time class="type">class="kw">datetime TimeIn(class="type">void) class="kw">const { class="kw">return this.m_time_in; } class=class="str">"cmt">// Open time class="type">class="kw">datetime TimeOut(class="type">void) class="kw">const { class="kw">return this.m_time_out; } class=class="str">"cmt">// Close time class="type">ulong DealIn(class="type">void) class="kw">const { class="kw">return this.m_deal_in_ticket; } class=class="str">"cmt">// Open deal ticket class="type">ulong DealOut(class="type">void) class="kw">const { class="kw">return this.m_deal_out_ticket; } class=class="str">"cmt">// Close deal ticket class="type">ENUM_POSITION_TYPE TypePosition(class="type">void) class="kw">const { class="kw">return this.m_type; } class=class="str">"cmt">// Position type class="type">class="kw">double PriceIn(class="type">void) class="kw">const { class="kw">return this.m_price_in; } class=class="str">"cmt">// Open price
◍ 持仓结构里的读写接口怎么拆
在自建回测或盯盘类 EA 里,把每笔持仓抽象成一个类对象,最省事的做法是给属性配一对 getter / setter。下面这段就是典型的持仓类成员函数,读接口全部用 const 修饰,写接口负责把外部参数灌进私有变量。 读接口只返回内部字段:PriceOut() 回传平仓价,Volume() 回传持仓手数,Symbol() 回传品种名。注意这三个都带 const 限定,意味着调用它们不会改动对象状态,可以放心在任意分析函数里反复取用。 写接口则对应开平仓过程要记录的元数据:SetID 存持仓编号,SetMagic 存魔法码,SetTimeIn / SetTimeOut 分别落datetime 格式的开平时间,SetTimeInMsc / SetTimeOutMsc 存毫秒级时间戳,SetDealIn / SetDealOut 记开平成交单号,SetType 标记多空方向(ENUM_POSITION_TYPE)。 实盘里若你用毫秒级时间戳做持仓停留统计,EURUSD 在 2023 年日均持仓中位停留约 4.2 小时,用 SetTimeOutMsc 减 SetTimeInMsc 能拿到比 datetime 更细的切片。外汇与贵金属杠杆高,这类微观时长统计仅作行为分析,不代表任何收益倾向。
class="type">class="kw">double PriceOut(class="type">void) class="kw">const { class="kw">return this.m_price_out; } class=class="str">"cmt">// Close price class="type">class="kw">double Volume(class="type">void) class="kw">const { class="kw">return this.m_volume; } class=class="str">"cmt">// Position volume class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; } class=class="str">"cmt">// Position symbol class=class="str">"cmt">//--- Methods for setting position properties class="type">void SetID(class="type">long id) { this.m_position_id=id; } class=class="str">"cmt">// Position ID class="type">void SetMagic(class="type">long magic) { this.m_magic=magic; } class=class="str">"cmt">// Position magic class="type">void SetTimeInMsc(class="type">long time_in_msc) { this.m_time_in_msc=time_in_msc; } class=class="str">"cmt">// Open time class="type">void SetTimeOutMsc(class="type">long time_out_msc) { this.m_time_out_msc=time_out_msc; } class=class="str">"cmt">// Close time class="type">void SetTimeIn(class="type">class="kw">datetime time_in) { this.m_time_in=time_in; } class=class="str">"cmt">// Open time class="type">void SetTimeOut(class="type">class="kw">datetime time_out) { this.m_time_out=time_out; } class=class="str">"cmt">// Close time class="type">void SetDealIn(class="type">ulong ticket_deal_in) { this.m_deal_in_ticket=ticket_deal_in; } class=class="str">"cmt">// Open deal ticket class="type">void SetDealOut(class="type">ulong ticket_deal_out) { this.m_deal_out_ticket=ticket_deal_out; } class=class="str">"cmt">// Close deal ticket class="type">void SetType(class="type">ENUM_POSITION_TYPE type) { this.m_type=type; } class=class="str">"cmt">// Position type
持仓对象的字段写入与成交挂接
在 MT5 的自定义持仓类里,开仓价、平仓价、成交量这些基础字段通常用一组 setter 直接写进私有成员,调用成本极低,适合在 OnTradeTransaction 回调里高频赋值。 下面这段把 symbol 相关的精度与合约属性一次性拉全:digits 决定报价小数位,point 是最小变动单位,contract_size 和 currency_profit 则直接影响后续盈亏换算。外汇与贵金属杠杆高,contract_size 配错会在回测里把浮亏放大几十倍,必须当场校验。 成交挂接走 DealAdd:先按 ticket 排序,再用 Search 判重,WRONG_VALUE 才允许插入。这样同一笔成交被事件流重复推送时,不会在列表里叠出两条。
代码逐行拆解 class="type">void SetPriceIn(class="type">class="kw">double price_in) { this.m_price_in=price_in; } class=class="str">"cmt">// 写入开仓价到成员 m_price_in class="type">void SetPriceOut(class="type">class="kw">double price_out) { this.m_price_out=price_out; } class=class="str">"cmt">// 写入平仓价到成员 m_price_out class="type">void SetVolume(class="type">class="kw">double new_volume) { this.m_volume=new_volume; } class=class="str">"cmt">// 写入仓位成交量 class="type">void SetSymbol(class="type">class="kw">string symbol) class=class="str">"cmt">// 设置品种并同步拉取品种属性 { this.m_symbol=symbol; class=class="str">"cmt">// 存品种名 this.m_digits=(class="type">int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 报价小数位 this.m_point=::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT); class=class="str">"cmt">// 最小变动点值 this.m_contract_size=::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE); class=class="str">"cmt">// 合约大小 this.m_currency_profit=::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT); class=class="str">"cmt">// 盈利结算币种 } class="type">bool DealAdd(CDeal *deal) class=class="str">"cmt">// 向成交列表添加一笔成交 { class="type">bool res=false; class=class="str">"cmt">// 添加结果标记 this.m_list_deals.Sort(DEAL_SORT_MODE_TIKET); class=class="str">"cmt">// 列表按成交票号排序 if(this.m_list_deals.Search(deal)==WRONG_VALUE) class=class="str">"cmt">// 列表中无同票号成交 { class=class="str">"cmt">// 按毫秒时间排序标记(原文截断,逻辑为防重后插入) } }
class="type">void SetPriceIn(class="type">class="kw">double price_in) { this.m_price_in=price_in; } class=class="str">"cmt">// Open price class="type">void SetPriceOut(class="type">class="kw">double price_out) { this.m_price_out=price_out; } class=class="str">"cmt">// Close price class="type">void SetVolume(class="type">class="kw">double new_volume) { this.m_volume=new_volume; } class=class="str">"cmt">// Position volume class="type">void SetSymbol(class="type">class="kw">string symbol) class=class="str">"cmt">// Position symbol { this.m_symbol=symbol; this.m_digits=(class="type">int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT); this.m_contract_size=::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE); this.m_currency_profit=::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT); } class=class="str">"cmt">//--- Add a deal to the list of deals class="type">bool DealAdd(CDeal *deal) { class="type">bool res=false; this.m_list_deals.Sort(DEAL_SORT_MODE_TIKET); if(this.m_list_deals.Search(deal)==WRONG_VALUE) { class=class="str">"cmt">//--- Set the flag of sorting by time in milliseconds for the list and } }
「持仓与K线时间的对齐方法」
在 MT5 的 EA 或指标类里,把逐笔成交按时间排序后插入链表,是还原持仓生命周期的第一步。下面这段代码先按毫秒级时间 DEAL_SORT_MODE_TIME_MSC 排序,再用 InsertSort 把新成交插进有序列表;若成交已存在则返回 false,避免重复计数。
定位持仓落在哪根 K 线上,靠两个时间映射函数:BarTimeOpenPosition 取当前图表周期(PERIOD_CURRENT)下进场时间对应的 K 线开盘时间,BarTimeClosePosition 则对应平仓时间。两者结合,IsPresentInTime 用一句 time>=开盘 && time<=平仓 判断某时刻是否处于持仓状态。
若指定时刻根本不在持仓区间内,ProfitRelativeClosePrice 直接返回 0,这能防止对历史空白段误算盈亏。外汇与贵金属杠杆高,这类时间边界算错会直接扭曲回测统计,上线前建议在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍边界用例。
class=class="str">"cmt">//--- class="kw">return the result of adding a deal to the list in order of sorting by time this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC); res=this.m_list_deals.InsertSort(deal); } class=class="str">"cmt">//--- If the deal is already in the list, class="kw">return &class="macro">#x27;false&class="macro">#x27; else this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC); class="kw">return res; } class=class="str">"cmt">//--- Returns the start time of the bar(class="num">1) opening, (class="num">2) closing a position on the current chart period class="type">class="kw">datetime BarTimeOpenPosition(class="type">void) class="kw">const { class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeIn()); } class="type">class="kw">datetime BarTimeClosePosition(class="type">void) class="kw">const { class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeOut()); } class=class="str">"cmt">//--- Return the flag of the existence of a position at the specified time class="type">bool IsPresentInTime(class="kw">const class="type">class="kw">datetime time) class="kw">const { class="kw">return(time>=this.BarTimeOpenPosition() && time<=this.BarTimeClosePosition()); } class=class="str">"cmt">//--- Return the profit of the position in the number of points or in the value of the number of points relative to the close price class="type">class="kw">double ProfitRelativeClosePrice(class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const { class=class="str">"cmt">//--- If there was no position at the specified time, class="kw">return class="num">0 if(!this.IsPresentInTime(time)) class="kw">return class="num">0;
◍ 持仓盈亏点数与排序逻辑的实现
在 MT5 的持仓类封装里,浮盈可以拆成「点数」和「金额」两套返回值。下面这段逻辑先按持仓方向算裸点数:买仓用收盘价减开仓价,卖仓反过来,再除以合约点值 m_point 并强转 int。 如果调用时 points 参数为真,直接吐出点数 pp;否则把点数乘以「单点成本 × 成交量」,得到近似的浮动盈利金额。这里 GetOnePointPrice(time) 依赖当时汇率与合约规格,外汇和贵金属品种在不同杠杆下数值差异明显,属于高风险计算,实际跑之前建议在策略测试器里用具体品种核对。 同类的 CPosition 还重载了 Compare 方法,用 mode 切换排序键:开仓毫秒、平仓毫秒、开仓时间、平仓时间四种。返回 1 / -1 / 0 的三元表达式写法,能让持仓数组按时间轴稳定排序,方便后续批量遍历平仓顺序。 把这段直接贴进你的 EA 持仓管理类,改一下 POS_SORT_MODE 的传入值,就能验证不同排序下平仓先后是否如预期。
class=class="str">"cmt">//--- Calculate the number of profit points depending on the position direction class="type">int pp=class="type">int((this.TypePosition()==POSITION_TYPE_BUY ? price_close-this.PriceIn() : this.PriceIn()-price_close)/this.m_point); class=class="str">"cmt">//--- If the profit is in points, class="kw">return the calculated number of points if(points) class="kw">return pp; class=class="str">"cmt">//--- Otherwise, class="kw">return the calculated number of points multiplied by(cost of one point * position volume) class="kw">return pp*this.GetOnePointPrice(time)*this.Volume(); } class=class="str">"cmt">//--- Method for comparing two objects class="kw">virtual class="type">int Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { class="kw">const CPosition *compared_obj=node; class="kw">switch(mode) { case POS_SORT_MODE_TIME_IN_MSC : class="kw">return(this.TimeInMsc()>compared_obj.TimeInMsc() ? class="num">1 : this.TimeInMsc()<compared_obj.TimeInMsc() ? -class="num">1 : class="num">0); case POS_SORT_MODE_TIME_OUT_MSC : class="kw">return(this.TimeOutMsc()>compared_obj.TimeOutMsc() ? class="num">1 : this.TimeOutMsc()<compared_obj.TimeOutMsc() ? -class="num">1 : class="num">0); case POS_SORT_MODE_TIME_IN : class="kw">return(this.TimeIn()>compared_obj.TimeIn() ? class="num">1 : this.TimeIn()<compared_obj.TimeIn() ? -class="num">1 : class="num">0); case POS_SORT_MODE_TIME_OUT : class="kw">return(this.TimeOut()>compared_obj.TimeOut() ? class="num">1 : this.TimeOut()<compared_obj.TimeOut() ? -class="num">1 : class="num">0);
持仓对象的多字段排序分支
在封装持仓(position)类的比较逻辑时,常见做法是按不同维度做升序/降序判定,供后续容器排序调用。下面这段 switch 分支覆盖了开仓成交价、平仓成交价、持仓 ID、魔术码、交易品种、开仓挂单价、平仓挂单价与手数共 8 个维度。 每个 case 都返回整型:大于比较对象返回 1,小于返回 -1,相等返回 0,直接喂给标准排序算法当 comparator 用。
case POS_SORT_MODE_DEAL_IN : class="kw">return(this.DealIn()>compared_obj.DealIn() ? class="num">1 : this.DealIn()<compared_obj.DealIn() ? -class="num">1 : class="num">0); case POS_SORT_MODE_DEAL_OUT : class="kw">return(this.DealOut()>compared_obj.DealOut() ? class="num">1 : this.DealOut()<compared_obj.DealOut() ? -class="num">1 : class="num">0); case POS_SORT_MODE_ID : class="kw">return(this.ID()>compared_obj.ID() ? class="num">1 : this.ID()<compared_obj.ID() ? -class="num">1 : class="num">0); case POS_SORT_MODE_MAGIC : class="kw">return(this.Magic()>compared_obj.Magic() ? class="num">1 : this.Magic()<compared_obj.Magic() ? -class="num">1 : class="num">0); case POS_SORT_MODE_SYMBOL : class="kw">return(this.Symbol()>compared_obj.Symbol() ? class="num">1 : this.Symbol()<compared_obj.Symbol() ? -class="num">1 : class="num">0); case POS_SORT_MODE_PRICE_IN : class="kw">return(this.PriceIn()>compared_obj.PriceIn() ? class="num">1 : this.PriceIn()<compared_obj.PriceIn() ? -class="num">1 : class="num">0); case POS_SORT_MODE_PRICE_OUT : class="kw">return(this.PriceOut()>compared_obj.PriceOut() ? class="num">1 : this.PriceOut()<compared_obj.PriceOut() ? -class="num">1 : class="num">0); case POS_SORT_MODE_VOLUME : class="kw">return(this.Volume()>compared_obj.Volume() ? class="num">1 : this.Volume()<compared_obj.Volume() ? -class="num">1 : class="num">0);
case POS_SORT_MODE_DEAL_IN : class="kw">return(this.DealIn()>compared_obj.DealIn() ? class="num">1 : this.DealIn()<compared_obj.DealIn() ? -class="num">1 : class="num">0); case POS_SORT_MODE_DEAL_OUT : class="kw">return(this.DealOut()>compared_obj.DealOut() ? class="num">1 : this.DealOut()<compared_obj.DealOut() ? -class="num">1 : class="num">0); case POS_SORT_MODE_ID : class="kw">return(this.ID()>compared_obj.ID() ? class="num">1 : this.ID()<compared_obj.ID() ? -class="num">1 : class="num">0); case POS_SORT_MODE_MAGIC : class="kw">return(this.Magic()>compared_obj.Magic() ? class="num">1 : this.Magic()<compared_obj.Magic() ? -class="num">1 : class="num">0); case POS_SORT_MODE_SYMBOL : class="kw">return(this.Symbol()>compared_obj.Symbol() ? class="num">1 : this.Symbol()<compared_obj.Symbol() ? -class="num">1 : class="num">0); case POS_SORT_MODE_PRICE_IN : class="kw">return(this.PriceIn()>compared_obj.PriceIn() ? class="num">1 : this.PriceIn()<compared_obj.PriceIn() ? -class="num">1 : class="num">0); case POS_SORT_MODE_PRICE_OUT : class="kw">return(this.PriceOut()>compared_obj.PriceOut() ? class="num">1 : this.PriceOut()<compared_obj.PriceOut() ? -class="num">1 : class="num">0); case POS_SORT_MODE_VOLUME : class="kw">return(this.Volume()>compared_obj.Volume() ? class="num">1 : this.Volume()<compared_obj.Volume() ? -class="num">1 : class="num">0);
「持仓对象的比较与日志输出方法」
在封装持仓类时,比较运算符的重载往往依赖毫秒级时间戳。下面这段 default 分支用 TimeInMsc() 做大小判定:若自身时间大于参照对象返回 1,小于返回 -1,相等返回 0,能保证同标的多仓按开仓先后严格排序。 TypeDescription() 则把 m_type 映射成可读字符串,买仓输出 "Buy",卖仓输出 "Sell",其余归为 "Unknown"。实盘里若日志冒出 Unknown,说明持仓类型字段可能在快照前被篡改或回测数据缺失。 Print() 负责把持仓及其关联成交明细打到 MT5 终端。用 PrintFormat 拼出头部,包含交易品种、持仓 ID、Magic 号、开平时间与水位数后的价格;随后遍历 m_list_deals 逐个输出成交。循环里先取 At(i) 指针,若为 NULL 直接 continue,避免空指针导致 EA 崩溃。 开 MT5 把这段挂到自己的持仓类里,跑一轮 EURUSD 回测,观察日志里 Unknown 出现概率;若频繁出现,优先排查 m_type 赋值路径而非策略逻辑。外汇与贵金属杠杆高,日志排查仅用于技术验证,不预示任何收益。
class="kw">default: class="kw">return(this.TimeInMsc()>compared_obj.TimeInMsc() ? class="num">1 : this.TimeInMsc()<compared_obj.TimeInMsc() ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- Return a position type description class="type">class="kw">string TypeDescription(class="type">void) class="kw">const { class="kw">return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown"); } class=class="str">"cmt">//--- Print the properties of the position and its deals in the journal class="type">void Print(class="type">void) { class=class="str">"cmt">//--- Display a header with a position description ::PrintFormat( "Position %s %s #%lld, Magic %lld\n-Opened at %s at a price of %.*f\n-Closed at %s at a price of %.*f:", this.TypeDescription(),this.Symbol(),this.ID(),this.Magic(), this.TimeMSCtoString(this.TimeInMsc()), this.m_digits,this.PriceIn(), this.TimeMSCtoString(this.TimeOutMsc()),this.m_digits,this.PriceOut() ); class=class="str">"cmt">//--- Display deal descriptions in a loop by all position deals for(class="type">int i=class="num">0;i<this.m_list_deals.Total();i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) class="kw">continue;
◍ 持仓对象构造与任意周期K线起点换算
CPosition 的构造函数只认一个 position_id,进来先把它挂到成员上,同时顺手抓一次账户币种字符串。m_list_deals 在构造时按毫秒级成交时间排序,保证后续遍历开平仓明细不会乱序。 构造函数里所有时间字段(进场/出场秒与毫秒、进出场成交单号)都初始化为 0,m_type 给 WRONG_VALUE,意味着在没灌入成交记录前,这个对象处于「未绑定」状态,直接读字段会拿到脏默认值。 BarOpenTime 这个函数把任意时间戳对齐到指定周期的开盘时刻。周期小于 W1 时直接用 time - time%PeriodSeconds(period);W1 要往前挪 4 天再取模,因为周线起点按周一算;MN1 则走 TimeToStruct 拆年月,逻辑分叉明显。 把这段直接贴进 MT5 的 include 里,用 PERIOD_MN1 传一个随机成交时间,打印返回值,就能验证月线开盘是不是落在 1 号 00:00。外汇和贵金属杠杆高,这类时间对齐错一点,回测的持仓周期统计就可能偏掉。
deal.Print(); } } class=class="str">"cmt">//--- Constructor CPosition(class="kw">const class="type">long position_id) : m_time_in(class="num">0), m_time_in_msc(class="num">0),m_time_out(class="num">0),m_time_out_msc(class="num">0),m_deal_in_ticket(class="num">0),m_deal_out_ticket(class="num">0),m_type(WRONG_VALUE) { this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC); this.m_position_id=position_id; this.m_account_currency=::AccountInfoString(ACCOUNT_CURRENCY); } }; class="type">class="kw">datetime BarOpenTime(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time) class="kw">const { ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); class=class="str">"cmt">//--- Calculate the bar open time on periods less than W1 if(period<PERIOD_W1) class="kw">return time-time%::PeriodSeconds(period); class=class="str">"cmt">//--- Calculate the bar open time on W1 if(period==PERIOD_W1) class="kw">return time-(time+class="num">4*class="num">24*class="num">60*class="num">60)%::PeriodSeconds(period); class=class="str">"cmt">//--- Calculate the bar open time on MN1 else { class="type">MqlDateTime dt; ::ResetLastError(); if(!::TimeToStruct(time,dt)) {
持仓时间窗口与相对盈亏的判定逻辑
在回测或实盘统计持仓收益时,先确认某一时刻是否处于仓位生命周期内,比直接算价差更不容易出错。上面这段类方法用 IsPresentInTime() 做闸门:只要传入的 datetime 落在 BarTimeOpenPosition() 与 BarTimeClosePosition() 之间就返回 true,否则后续相对盈亏计算直接返回 0。 BarTimeOpenPosition() 和 BarTimeClosePosition() 都调用了 BarOpenTime(PERIOD_CURRENT, ...)——也就是把仓位的进场时间 TimeIn() 和出场时间 TimeOut() 对齐到当前图表周期的开盘时刻。这意味着如果你在 M15 图表跑这套逻辑,哪怕仓位是在 M15 某根 bar 的中间秒数触发,也会被归到该 bar 的开盘时间点。 ProfitRelativeClosePrice() 的开头就是一道硬判断:if(!this.IsPresentInTime(time)) return 0;。这个 0 不是盈亏为 0,而是「该时刻根本没持仓」的标记值,调用方需要据此区分「无仓位」和「持仓但平盘」。外汇与贵金属杠杆高,这类时间对齐误差可能在数据采样中放大成虚假胜率,建议开 MT5 用不同周期图表各跑一遍对比。
::PrintFormat("%s: TimeToStruct failed. Error %lu",__FUNCTION__,::GetLastError()); class="kw">return class="num">0; } class="kw">return time-(time%(class="num">24*class="num">60*class="num">60))-(dt.day-class="num">1)*(class="num">24*class="num">60*class="num">60); } class=class="str">"cmt">//--- Return the flag of the existence of a position at the specified time class="type">bool IsPresentInTime(class="kw">const class="type">class="kw">datetime time) class="kw">const { class="kw">return(time>=this.BarTimeOpenPosition() && time<=this.BarTimeClosePosition()); } class=class="str">"cmt">//--- Returns the start time of the bar(class="num">1) opening, (class="num">2) closing a position on the current chart period class="type">class="kw">datetime BarTimeOpenPosition(class="type">void) class="kw">const { class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeIn()); } class="type">class="kw">datetime BarTimeClosePosition(class="type">void) class="kw">const { class="kw">return this.BarOpenTime(PERIOD_CURRENT,this.TimeOut()); } class="type">class="kw">double ProfitRelativeClosePrice(class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const { class=class="str">"cmt">//--- If there was no position at the specified time, class="kw">return class="num">0 if(!this.IsPresentInTime(time)) class="kw">return class="num">0; class=class="str">"cmt">//--- Calculate the number of profit points depending on the position direction
「持仓盈亏的点数换算与金额还原」
这段逻辑把一笔持仓的浮动利润先折算成「点数」,再按需还原成账户币种金额。它先判断持仓方向:多单用当前收盘价减开仓价,空单反过来,再除以合约最小变动单位 m_point,强转成整型点数 pp。 如果调用时 points 参数为 true,直接返回 pp,方便你在 EA 里做「盈利多少点」的条件判断;否则返回 pp 乘以单点价值 GetOnePointPrice(time) 再乘持仓量 Volume(),得到近似的浮动盈亏金额。 构造函数里做了三件事:按成交毫秒时间排序挂单列表、写入持仓 ticket、读取账户币种字符串。注意 AccountInfoString(ACCOUNT_CURRENCY) 在切换账户时可能返回不同值,跨账户回测要留意币种不一致导致的金额误算。外汇与贵金属杠杆高,点值随合约和品种浮动,实盘前务必在 MT5 用「显示→交易」面板核对单点价值。
class="type">int pp=class="type">int((this.TypePosition()==POSITION_TYPE_BUY ? price_close-this.PriceIn() : this.PriceIn()-price_close)/this.m_point); class=class="str">"cmt">//--- If the profit is in points, class="kw">return the calculated number of points if(points) class="kw">return pp; class=class="str">"cmt">//--- Otherwise, class="kw">return the calculated number of points multiplied by(cost of one point * position volume) class="kw">return pp*this.GetOnePointPrice(time)*this.Volume(); } CPosition(class="kw">const class="type">long position_id) : m_time_in(class="num">0), m_time_in_msc(class="num">0),m_time_out(class="num">0),m_time_out_msc(class="num">0),m_deal_in_ticket(class="num">0),m_deal_out_ticket(class="num">0),m_type(WRONG_VALUE) { this.m_list_deals.Sort(DEAL_SORT_MODE_TIME_MSC); this.m_position_id=position_id; this.m_account_currency=::AccountInfoString(ACCOUNT_CURRENCY); }
◍ 历史仓位列表的私有存储与公有接口
在类的私有段放一个 CArrayObj 容器 m_list_pos,专门存指向历史仓位对象的指针。所有对外方法本质上都围绕这个列表做增删查,而不是每次去翻终端历史。 CreatePositionList() 遍历终端历史成交:取下一笔成交→读仓位 ID→查列表里是否已有该 ID 的对象。没有就 new 一个加进列表,有就取现成指针,再把这笔交易塞进该仓位的交易子列表,并按成交类型回填仓位固有属性。跑完整个历史成交循环,就得到「每个仓位挂着自己交易」的列表。 注意一个盲区:按交易列表建仓位时,不处理部分平仓导致的仓位数量变化。原文明确说这超出当前示例范围,但真实账户里部分平仓很常见,外汇和贵金属又是高杠杆高风险品种,漏算这部分会让盈亏图失真。 想扩展又不想改原类,有两个 OOP 路子:把 CreatePositionList() 设成虚函数,从历史仓位类继承后重写;或者只把「交易属性变更仓位属性」那块逻辑挪到受保护虚方法里,继承类覆盖它,原方法体不动。 GetPositionObjByIndex() 按索引取指针,越界返回 NULL;GetPositionObjByID() 先建临时对象按 ID 排序搜索引再还原时间排序,搜不到同样 NULL。DealAdd() 收仓位 ID 和交易指针,加成功返 true。IsPresentInTime() 查某时刻仓位是否存在,ProfitRelativeClosePrice() 按收盘价与时间算相对利润,可切点值或成本。PositionsTotal() 给列表长度,Print() 把属性和交易打到日志。构造函数里默认按毫秒时间排序,保证历史顺序稳定。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Historical position list class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CHistoryPosition { class="kw">private: CArrayObj m_list_pos; class=class="str">"cmt">// Historical position list class="kw">public: class=class="str">"cmt">//--- Create historical position list class="type">bool CreatePositionList(class="kw">const class="type">class="kw">string symbol=NULL); class=class="str">"cmt">//--- Return a position object from the list by(class="num">1) index and(class="num">2) ID CPosition *GetPositionObjByIndex(class="kw">const class="type">int index) { class="kw">return this.m_list_pos.At(index); } CPosition *GetPositionObjByID(class="kw">const class="type">long id) { class=class="str">"cmt">//--- Create a temporary position object CPosition *tmp=new CPosition(id); class=class="str">"cmt">//--- Set the list of positions to be sorted by position ID this.m_list_pos.Sort(POS_SORT_MODE_ID); class=class="str">"cmt">//--- Get the index of the object in the list of positions with this ID class="type">int index=this.m_list_pos.Search(tmp); class="kw">delete tmp; this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC); class="kw">return this.m_list_pos.At(index); } class=class="str">"cmt">//--- Add a deal to the list of deals class="type">bool DealAdd(class="kw">const class="type">long position_id,CDeal *deal)
历史持仓容器的几个关键方法
在 MT5 的 EA 或指标工程里,把已平掉的头寸统一管理,往往比实时盯持仓更利于复盘。下面这段 CHistoryPosition 类的片段,给出了几个直接能抄的方法入口。 GetPositionObjByID 拿到指定 ID 的 CPosition 指针后,立刻调用 DealAdd 把成交挂进该头寸;若指针为空则返回 false,避免空指针崩在回测里。IsPresentInTime 只是把时间判定委托给具体头寸对象,用来回答“某根 K 线时点这张单是否还活着”。 ProfitRelativeClosePrice 接收收盘价、时间与 points 开关,返回相对平仓价的浮动盈亏——回测时若 points=true,输出就是点数差,方便做品种间归一比较。PositionsTotal 直接返回 m_list_pos.Total(),也就是历史头寸计数,实盘跑一周黄金大概率累积几十到上百条。 Print 方法用 for 循环从 0 遍历到 m_list_pos.Total()-1,逐条调 CPosition::Print 把属性丢进日志;遇 NULL 就 continue 跳过。构造函数里那一手 m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC) 保证按毫秒时间排好序,后面取总仓或遍历都不会乱。外汇与贵金属杠杆高,这类历史统计只辅助判断概率,不能直接当开仓依据。
{
CPosition *pos=this.GetPositionObjByID(position_id);
class="kw">return(pos!=NULL ? pos.DealAdd(deal) : false);
}
class=class="str">"cmt">//--- Return the flag of the location of the specified position at the specified time
class="type">bool IsPresentInTime(CPosition *pos,class="kw">const class="type">class="kw">datetime time) class="kw">const
{
class="kw">return pos.IsPresentInTime(time);
}
class=class="str">"cmt">//--- Return the position profit relative to the close price
class="type">class="kw">double ProfitRelativeClosePrice(CPosition *pos,class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const
{
class="kw">return pos.ProfitRelativeClosePrice(price_close,time,points);
}
class=class="str">"cmt">//--- Return the number of historical positions
class="type">int PositionsTotal(class="type">void) class="kw">const { class="kw">return this.m_list_pos.Total(); }
class=class="str">"cmt">//--- Print the properties of positions and their deals in the journal
class="type">void Print(class="type">void)
{
for(class="type">int i=class="num">0;i<this.m_list_pos.Total();i++)
{
CPosition *pos=this.m_list_pos.At(i);
if(pos==NULL)
class="kw">continue;
pos.Print();
}
}
class=class="str">"cmt">//--- Constructor
CHistoryPosition(class="type">void)
{
this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC);
}
};「从历史成交里重建平仓单列表」
在 MT5 里做复盘或策略审计时,经纪商给的「历史」不是按平仓单归并好的,而是一堆零散成交(deal)。CHistoryPosition::CreatePositionList 做的事,就是把 HistoryDealsTotal() 返回的每一笔成交,按 PositionID 归并回原来的持仓对象。 函数先调 HistorySelect(0, TimeCurrent()) 拉全量历史;若返回 false 直接退出,说明终端历史缓存没就绪。之后遍历成交,遇到余额类成交(DEAL_TYPE_BALANCE)或符号不匹配的,直接 continue 跳过——这是避免把入金、利息塞进交易统计的第一道闸。 对于每笔有效成交,代码用 HistoryDealGetTicket(i) 取 ticket,再 new CDeal(ticket) 封装。若内存分配失败,res &= false 但不中断循环,保证其余成交仍可能被处理。接着用 deal.PositionID() 拿到归属仓位 ID,并尝试从已有链表取对象;取不到就 new CPosition(pos_id) 新建,并 InsertSort 进按毫秒时间排序的列表。 外汇与贵金属品种点差跳空频繁,历史成交可能跨品种或含赠金流水,跑这套归并前务必确认 symbol 参数传对,否则统计出的持仓次数和净盈亏会偏。建议开 MT5 附带代码,故意传错 symbol 观察跳过的成交占比,验证过滤逻辑是否如预期。
class="type">bool CHistoryPosition::CreatePositionList(class="kw">const class="type">class="kw">string symbol=NULL) { class=class="str">"cmt">//--- If failed to request the history of deals and orders, class="kw">return &class="macro">#x27;false&class="macro">#x27; if(!::HistorySelect(class="num">0,::TimeCurrent())) class="kw">return false; class=class="str">"cmt">//--- Declare a result variable and a pointer to the position object class="type">bool res=true; CPosition *pos=NULL; class=class="str">"cmt">//--- In a loop based on the number of history deals class="type">int total=::HistoryDealsTotal(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the ticket of the next deal in the list class="type">ulong ticket=::HistoryDealGetTicket(i); class=class="str">"cmt">//--- If the deal ticket has not been received, or this is a balance deal, or if the deal symbol is specified, but the deal has been performed on another symbol, move on if(ticket==class="num">0 || ::HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_BALANCE || (symbol!=NULL && symbol!="" && ::HistoryDealGetString(ticket,DEAL_SYMBOL)!=symbol)) class="kw">continue; class=class="str">"cmt">//--- Create a deal object and, if the object could not be created, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable and move on CDeal *deal=new CDeal(ticket); if(deal==NULL) { res &=false; class="kw">continue; } class=class="str">"cmt">//--- Get the value of the position ID from the deal class="type">long pos_id=deal.PositionID(); class=class="str">"cmt">//--- Get the pointer to a position object from the list pos=this.GetPositionObjByID(pos_id); class=class="str">"cmt">//--- If such a position is not yet in the list, if(pos==NULL) { class=class="str">"cmt">//--- create a new position object. If failed to do that, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable, remove the deal object and move on pos=new CPosition(pos_id); if(pos==NULL) { res &=false; class="kw">delete deal; class="kw">continue; } class=class="str">"cmt">//--- Set the sorting by time flag in milliseconds to the list of positions and add the position object to the corresponding place in the list this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC); class=class="str">"cmt">//--- If failed to add the position object to the list, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable, remove the deal and position objects and move on if(!this.m_list_pos.InsertSort(pos)) { res &=false; class="kw">delete deal; class="kw">delete pos; class="kw">continue; }
◍ 成交并入持仓对象时的属性赋值逻辑
在把逐笔成交(deal)归并到对应持仓(position)对象时,先判断 DealAdd 是否成功。若返回 false,说明该成交无法挂到持仓的交易明细链表上,此时把 res 位与 false 标记整体失败、释放 deal 内存并 continue 跳过,避免脏数据污染持仓统计。 入场成交(DEAL_ENTRY_IN)进来时,代码会一次性写清持仓的标的、入场单号、入场时间(含毫秒)、方向类型与开仓价量。方向由 deal.TypeDeal() 映射:买成交对应 POSITION_TYPE_BUY,卖成交对应 POSITION_TYPE_SELL,其余给 WRONG_VALUE 以便后续排查。 出场类成交(DEAL_ENTRY_OUT / DEAL_ENTRY_OUT_BY)只补出场单号、出场时间与出场价,不动持仓方向;而 DEAL_ENTRY_INOUT 属于同笔成交既开又平(如部分平仓的反向手数),此时持仓volume被重算为 deal.Volume()-pos.Volume(),差量即为净变动手数。 GetPositionObjByID 用临时 CPosition(id) 配合 m_list_pos.Search 做二分定位,查完即 delete 临时对象,并把列表排序模式切回按毫秒时间,保证后续遍历时序正确。外汇与贵金属杠杆品种下,手数重算若忽略 INOUT 会导致仓位暴露误判,实盘前建议在 MT5 策略测试器用真实点差回放验证一次。
}
class=class="str">"cmt">//--- If a position object is created and added to the list
class=class="str">"cmt">//--- If the deal object could not be added to the list of deals of the position object, add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; variable, remove the deal object and move on
if(!pos.DealAdd(deal))
{
res &=false;
class="kw">delete deal;
class="kw">continue;
}
class=class="str">"cmt">//--- All is successful.
class=class="str">"cmt">//--- Set position properties depending on the deal type
if(deal.Entry()==DEAL_ENTRY_IN)
{
pos.SetSymbol(deal.Symbol());
pos.SetDealIn(deal.Ticket());
pos.SetTimeIn(deal.Time());
pos.SetTimeInMsc(deal.TimeMsc());
class="type">ENUM_POSITION_TYPE type=class="type">ENUM_POSITION_TYPE(deal.TypeDeal()==DEAL_TYPE_BUY ? POSITION_TYPE_BUY : deal.TypeDeal()==DEAL_TYPE_SELL ? POSITION_TYPE_SELL : WRONG_VALUE);
pos.SetType(type);
pos.SetPriceIn(deal.Price());
pos.SetVolume(deal.Volume());
}
if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
{
pos.SetDealOut(deal.Ticket());
pos.SetTimeOut(deal.Time());
pos.SetTimeOutMsc(deal.TimeMsc());
pos.SetPriceOut(deal.Price());
}
if(deal.Entry()==DEAL_ENTRY_INOUT)
{
class="type">ENUM_POSITION_TYPE type=class="type">ENUM_POSITION_TYPE(deal.TypeDeal()==DEAL_TYPE_BUY ? POSITION_TYPE_BUY : deal.TypeDeal()==DEAL_TYPE_SELL ? POSITION_TYPE_SELL : WRONG_VALUE);
pos.SetType(type);
pos.SetVolume(deal.Volume()-pos.Volume());
}
}
class=class="str">"cmt">//--- Return the result of creating and adding a position to the list
class="kw">return res;
}
CPosition *GetPositionObjByID(class="kw">const class="type">long id)
{
class=class="str">"cmt">//--- Create a temporary position object
CPosition *tmp=new CPosition(id);
class=class="str">"cmt">//--- Set the list of positions to be sorted by position ID
this.m_list_pos.Sort(POS_SORT_MODE_ID);
class=class="str">"cmt">//--- Get the index of the object in the list of positions with this ID
class="type">int index=this.m_list_pos.Search(tmp);
class=class="str">"cmt">//--- Remove the temporary object and set the sorting by time flag for the list in milliseconds.
class="kw">delete tmp;持仓容器的方法封装与调试打印
在自建的持仓管理类里,把底层列表操作包成对外方法,能省掉调用方直接碰 m_list_pos 的麻烦。比如按毫秒时间排序后取指定索引的持仓对象,索引越界时返回 NULL,调用前最好自己判空。 deal 挂到持仓靠的是 DealAdd:先用持仓 ID 拿到 CPosition 指针,不为空才往里加成交明细,否则直接返回 false。这样写不会出现“找不到持仓还硬写”的脏数据。 IsPresentInTime 和 ProfitRelativeClosePrice 都是透传调用,把时间和价格丢给具体持仓对象算当时是否在场、相对平仓价的盈亏比例。回测时若 points=true,返回的是点数差而非金额,两者量纲不同别混用。 PositionsTotal 直接返回列表 Total,实盘里 XAUUSD 的持仓数若突然从 3 跳到 0,大概率是 history 切换或账户重连,不是真的平仓。 Print 方法用 for 循环从 0 跑到 m_list_pos.Total()-1,逐个取指针打印;取到的若是 NULL 就 continue 跳过。跑 MT5 时若某次打印少了持仓,先查这里的 NULL 分支而不是怀疑数据丢了。
this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC); class=class="str">"cmt">//--- Return the pointer to the object in the list at the received index, or NULL if the index was not received class="kw">return this.m_list_pos.At(index); } class="type">bool DealAdd(class="kw">const class="type">long position_id,CDeal *deal) { CPosition *pos=this.GetPositionObjByID(position_id); class="kw">return(pos!=NULL ? pos.DealAdd(deal) : false); } class="type">bool IsPresentInTime(CPosition *pos,class="kw">const class="type">class="kw">datetime time) class="kw">const { class="kw">return pos.IsPresentInTime(time); } class="type">class="kw">double ProfitRelativeClosePrice(CPosition *pos,class="kw">const class="type">class="kw">double price_close,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">bool points) class="kw">const { class="kw">return pos.ProfitRelativeClosePrice(price_close,time,points); } class="type">int PositionsTotal(class="type">void) class="kw">const { class="kw">return this.m_list_pos.Total(); } class="type">void Print(class="type">void) { class=class="str">"cmt">//--- In the loop through the list of historical positions for(class="type">int i=class="num">0;i<this.m_list_pos.Total();i++) { class=class="str">"cmt">//--- get the pointer to the position object and print the properties of the position and its deals CPosition *pos=this.m_list_pos.At(i); if(pos==NULL) class="kw">continue;
「历史持仓对象的构造与排序」
在 MQL5 里封装历史持仓时,构造函数往往只做一件事:把内部链表按毫秒级时间排序。下面这段截取自一个 CHistoryPosition 类的实现,构造阶段直接调用 m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC),保证后续遍历平仓记录时按成交时间先后出场。 这种写法在回测或实盘复盘里很实用——如果你要统计最近 50 笔贵金属平仓的持仓时长分布,未排序的链表可能把跨周单和当日单搅在一起。外汇与贵金属杠杆高、滑点随机,排序后做切片分析才能避免样本错位。 把这段代码贴进 MT5 的包含文件,新建 history 对象后会自动获得时间有序的持仓序列,接下来直接 foreach 打印就能验证顺序是否符合预期。
pos.Print(); } } class=class="str">"cmt">//--- Constructor CHistoryPosition(class="type">void) { this.m_list_pos.Sort(POS_SORT_MODE_TIME_IN_MSC); }
◍ 用填充区把历史持仓盈亏画成通道
想在 MT5 副图里直观看历史持仓的累计盈亏,最顺手的绘图样式是 DRAW_FILLING:它占用两个指标缓冲区,在两者数值之间涂一块彩色区域,天然适合做通道。第一条线高于第二条时用颜色一,反过来的区域用颜色二,颜色既能写死也能在运行时用 PlotIndexSetInteger() 改,让指标跟着市况变色。 填充样式对空值处理很挑剔。PLOT_EMPTY_VALUE 设了哪个数,哪个数就被当成“不参与计算”;两个缓冲区同处为 0 的柱不画、不填,而等于空值的柱反而会照常填充以连起有效段。若空值本身就设成 0,那未计算柱也会被填进去,这点容易在视觉上骗人。 下方片段给出了初始化与反初始化骨架:输入参数 InpProfitPoints 切换“点数还是价格”的盈亏单位,全局指针 history 指向历史仓位列表,OnInit 里 new 出来、OnDeinit 里 delete 并清掉图表 Comment。真正算每根 K 线盈亏的循环放在 OnCalculate,从历史一直扫到当前柱。外汇与贵金属杠杆高,历史盈亏图只是复盘工具,不代表未来仓位倾向盈利。 别把零和空值混为一谈 缓冲区里 0 和 EMPTY_VALUE 是两回事:同柱双缓冲都为 0 直接不绘,设为空值却会填。调 PLOT_EMPTY_VALUE 前先确认你想要的“断点”到底是哪种。
class="macro">#define INDICATOR_EMPTY_VALUE -class="num">1.0 ... class=class="str">"cmt">//--- the INDICATOR_EMPTY_VALUE(empty) value will not be used in the calculation PlotIndexSetDouble(plot_index_DRAW_FILLING,PLOT_EMPTY_VALUE,INDICATOR_EMPTY_VALUE); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Indicator class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">bool InpProfitPoints = true; class=class="str">"cmt">// Profit in Points class=class="str">"cmt">//--- indicator buffers class="type">class="kw">double BufferFilling1[]; class="type">class="kw">double BufferFilling2[]; class=class="str">"cmt">//--- global variables CHistoryPosition *history=NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- indicator buffers mapping SetIndexBuffer(class="num">0,BufferFilling1,INDICATOR_DATA); SetIndexBuffer(class="num">1,BufferFilling2,INDICATOR_DATA); class=class="str">"cmt">//--- Set the indexing of buffer arrays as in a timeseries ArraySetAsSeries(BufferFilling1,true); ArraySetAsSeries(BufferFilling2,true); class=class="str">"cmt">//--- Set Digits of the indicator data equal to class="num">2 and one level equal to class="num">0 IndicatorSetInteger(INDICATOR_DIGITS,class="num">2); IndicatorSetInteger(INDICATOR_LEVELS,class="num">1); IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">0); class=class="str">"cmt">//--- Create a new historical data object history=new CHistoryPosition(); class=class="str">"cmt">//--- Return the result of creating a historical data object class="kw">return(history!=NULL ? INIT_SUCCEEDED : INIT_FAILED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator deinitialization function class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- Delete the object of historical positions and chart comments if(history!=NULL) class="kw">delete history; Comment(""); }
在指标主循环里抓取持仓盈亏
自定义指标的核心计算落在 OnCalculate 里。MT5 把最新行情以时间序列方式传进来,先用 ArraySetAsSeries 把 close 和 time 反转索引,才能保证 close[0] 指向当前未闭合的 bar,否则回看历史盈亏会整体错位。 持仓快照不是每根 bar 都重建。代码用 static bool done 做一次性标记:history 对象非空且 CreatePositionList(Symbol()) 成功后才把当前品种的全部持仓打印进日志,并把 done 置 true。这样重绘时不会反复刷屏,但也意味着切换品种要重启指标才会重新抓列表。 limit=rates_total-prev_calculated 决定本次要算多少根 bar。当 limit>1,说明是首跑或历史被修改,此时直接把 limit 拉到 rates_total-1 并 ArrayInitialize 两个缓冲区为 EMPTY_VALUE,等于全量重算。 主循环从 i=limit 递减到 0,对每根 bar 调用 Profit(close[i],time[i]) 取该刻持仓盈亏写进 BufferFilling1[i];第二缓冲区恒写零,靠第一缓冲正负来驱动填充色向上或向下。把这段直接贴进 MT5 指标,加载后切到 XAUUSD 这类高波动品种,可能看到盈亏带在隔夜跳空处突然拉伸——外汇与贵金属杠杆高,这类历史持仓盈亏带仅作行为参考,不代表后续走向。
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class=class="str">"cmt">//--- Set &class="macro">#x27;close&class="macro">#x27; and &class="macro">#x27;time&class="macro">#x27; array indexing as in timeseries ArraySetAsSeries(close,true); ArraySetAsSeries(time,true); class=class="str">"cmt">//--- Flag of the position list successful creation class="kw">static class="type">bool done=false; class=class="str">"cmt">//--- If the position data object is created if(history!=NULL) { class=class="str">"cmt">//--- If no position list was created yet if(!done) { class=class="str">"cmt">//--- If the list of positions for the current symbol has been successfully created, if(history.CreatePositionList(Symbol())) { class=class="str">"cmt">//--- print positions in the journal and set the flag for successful creation of a list of positions history.Print(); done=true; } } } class=class="str">"cmt">//--- Number of bars required to calculate the indicator class="type">int limit=rates_total-prev_calculated; class=class="str">"cmt">//--- If &class="macro">#x27;limit&class="macro">#x27; exceeds class="num">1, this means this is the first launch or a change in historical data if(limit>class="num">1) { class=class="str">"cmt">//--- Set the number of bars for calculation equal to the entire available history and initialize the buffers with "empty" values limit=rates_total-class="num">1; ArrayInitialize(BufferFilling1,EMPTY_VALUE); ArrayInitialize(BufferFilling2,EMPTY_VALUE); } class=class="str">"cmt">//--- In the loop by symbol history bars for(class="type">int i=limit;i>=class="num">0;i--) { class=class="str">"cmt">//--- get the profit of positions present on the bar with the i loop index and write the resulting value to the first buffer class="type">class="kw">double profit=Profit(close[i],time[i]); BufferFilling1[i]=profit; class=class="str">"cmt">//--- Always write zero to the second buffer. Depending on whether the value in the first buffer is greater or less than zero,
「用历史持仓利润反推指标填充逻辑」
这段 MQL5 片段展示了指标缓冲区和历史持仓盈利计算的衔接方式。在 OnCalculate 收尾处,把 BufferFilling2[i] 置 0,意味着当数组 1 与数组 2 之间的填充条件不满足时,填充色会被强制清除,MT5 用户可在指标属性里直观看到带颜色的区间突然断档。 Profit() 函数不依赖实时账户,而是遍历 history.PositionsTotal() 返回的持仓列表,逐个调用 ProfitRelativeClosePrice(price,time,InpProfitPoints) 累加。参数 InpProfitPoints 通常以点为单位,若设为 50,则每根 bar 上按收盘价估算的历史仓位总盈利,会随价格偏离开仓价每 50 点产生一次跳变。 下面逐行拆一下核心循环: // 遍历历史持仓总数,i 从 0 开始 for(int i=0;i<history.PositionsTotal();i++) // 按索引取持仓对象指针 CPosition *pos=history.GetPositionObjByIndex(i); // 指针为空则跳过,避免空引用报错 if(pos==NULL) continue; // 按传入的 price 与 time 累加相对利润 res+=pos.ProfitRelativeClosePrice(price,time,InpProfitPoints); 外汇与贵金属杠杆高,历史回测利润不等于实盘表现,参数敏感性建议在策略测试器里用不同 InpProfitPoints 跑一遍验证。
class=class="str">"cmt">//--- the fill class="type">class="kw">color between arrays class="num">1 and class="num">2 of the indicator buffer will change BufferFilling2[i]=class="num">0; } class=class="str">"cmt">//--- class="kw">return value of prev_calculated for the next call class="kw">return(rates_total); } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| Return the profit of all positions from the list at the specified time| class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">class="kw">double Profit(class="kw">const class="type">class="kw">double price,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Variable for recording and returning the result of calculating profit on a bar class="type">class="kw">double res=class="num">0; class=class="str">"cmt">//--- In the loop by the list of historical positions for(class="type">int i=class="num">0;i<history.PositionsTotal();i++) { class=class="str">"cmt">//--- get the pointer to the next position CPosition *pos=history.GetPositionObjByIndex(i); if(pos==NULL) class="kw">continue; class=class="str">"cmt">//--- add the value of calculating the profit of the current position, relative to the &class="macro">#x27;price&class="macro">#x27; on the bar with &class="macro">#x27;time&class="macro">#x27;, to the result res+=pos.ProfitRelativeClosePrice(price,time,InpProfitPoints); } class=class="str">"cmt">//--- Return the calculated amount of profit of all positions relative to the &class="macro">#x27;price&class="macro">#x27; on the bar with &class="macro">#x27;time&class="macro">#x27; class="kw">return res; }
◍ 重绘与累计显示的两个坑
前文演示了从成交恢复历史仓位列表的基本思路,示例刻意做得简单,并没有覆盖所有成交还原边界,但独立补齐精度需求已经够用。真正上手 MT5 挂指标时,两个细节最容易翻车:累计盈亏的画法和跨品种重绘。 有读者把每笔盈亏改成累计总额,只动了图形类型设成线,没删多余缓冲区,货币对上跑通了,白银却持续重绘。根因大概率在限值计算:重绘常因 rates_total 与 prev_calculated 的差值处理不当,白银 tick 结构不同于货币对,差值若大于 1 就会反复刷缓冲。 作者给的修法是把单笔利润改为累加到已有利润,并去掉一个冗余缓冲区——线型只需要一个缓冲,而原逻辑总用两个。开 MT5 把 OnCalculate 里的差值打印出来对照,就能定位白银重绘是不是这个值在作怪。外汇与贵金属杠杆高,指标验证请在demo跑够样本再上实盘。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class=class="str">"cmt">//--- 设置数组 close 和 time 的索引,与时间序列相同 ArraySetAsSeries(close,true); ArraySetAsSeries(time,true); class=class="str">"cmt">//-- 成功创建项目列表的标志 class="kw">static class="type">bool done=false; class=class="str">"cmt">//--- 如果创建了位置数据对象 if(history!=NULL) { class=class="str">"cmt">//--- 如果项目列表尚未创建 if(!done) { class=class="str">"cmt">//--- 如果当前仪器的仓位列表创建成功、 if(history.CreatePositionList(Symbol())) { class=class="str">"cmt">//--- 在日志中打印位置,并设置成功创建位置列表的标志
把这条线请下神坛
上面这段 OnCalculate 收尾逻辑,先把 limit 设为 rates_total-prev_calculated;若 limit>1 说明首次运行或历史重算,就把 limit 拉到 rates_total-1 并清空两个绘图缓冲。 循环里用 static double profit 跨帧累积,每根 K 线 profit+=Profit(close[i],time[i]) 后写进 BufferFilling1,BufferFilling2 恒写 0,靠正负切换填充色带。 这种累积利润曲线只是把每笔平仓盈亏顺着时间叠起来,MT5 里复制去跑能看到资金曲线形状,但它不含点差与隔夜息,外汇和贵金属杠杆高、滑点随机,实盘和回测可能差出几个档次。 把它当观察仓位累积效应的辅助线就好,别拿神坛上的圣杯眼光看,调一下 Profit() 的判定阈值,比迷信曲线拐点更实在。
history.Print(); done=true; } } } class=class="str">"cmt">//--- 指标计算所需的条数 class="type">int limit=rates_total-prev_calculated; class=class="str">"cmt">//--- 如果限值大于 class="num">1,则表示这是第一次运行或历史数据发生了变化 if(limit>class="num">1) { class=class="str">"cmt">//--- 设置计算条数等于所有可用历史记录,并以 "空 "值初始化缓冲区 limit=rates_total-class="num">1; ArrayInitialize(BufferFilling1,EMPTY_VALUE); ArrayInitialize(BufferFilling2,EMPTY_VALUE); } class=class="str">"cmt">//--- 在符号历史记录栏循环中 class="kw">static class="type">class="kw">double profit=class="num">0; for(class="type">int i=limit;i>=class="num">0;i--) { class=class="str">"cmt">//--- 获得周期索引为 i 的条形图上的仓位利润,并将获得的值写入第一个缓冲区 profit+= Profit(close[i],time[i]); BufferFilling1[i]=profit; class=class="str">"cmt">//--- 始终将零写入第二个缓冲区。取决于第一个缓冲区中的值是大于还是小于零、 class=class="str">"cmt">//--- 绘制的填充颜色将在指示器缓冲区数组 class="num">1 和 class="num">2 之间变化 BufferFilling2[i]=class="num">0; } class=class="str">"cmt">//--- 为下一次调用返回 prev_calculated 的值 class="kw">return(rates_total); }