自定义交易历史表述并创建报告图表·进阶篇
(2/3)· 面对 MT5 里成百上千行错序成交与宣称记录,多数交易者连采样区间都切不准
从持仓 ID 拉全量成交明细
做历史成交复盘时,最麻烦的是把同一笔持仓下的多次开平成交归拢到一起。下面这段类方法先按时间区间取出独有持仓 ID,再逐个用 HistorySelectByPosition 把该 ID 下的所有 deal 抓出来,逻辑上能把「一仓多单」和「一仓一单」都覆盖。 注释里写得很直白:若持仓类型含 InOut,inputParam 会塞进多笔子仓位;若没有 InPut 类型,则只含一笔。这意味着你写 EA 回测或手工统计时,不能假设每笔 keeper 只对应一次成交,否则加权均价和边界点判断会算错。 核心入口是 getHistory,它先 ArrayFree 清空承接数组,再 getIDArr 拿区间内的 ID 列表,随后双层循环:外层走 ID,内层走该 ID 的 HistoryDealsTotal。在 MT5 里实测,HistorySelectByPosition 对净仓账户和对冲账户都返回对应 position 的 deal,但返回条数取决于 broker 成交记录粒度。 外汇与贵金属杠杆高,历史成交统计仅用于策略验证,实盘仍可能因滑点使成交序列偏离回测。
class="type">bool CDealHistoryGetter::getHistory(DealKeeper &deals[],class="type">class="kw">datetime from,class="type">class="kw">datetime till) { ArrayFree(deals); class=class="str">"cmt">// 清除结果数组 ID_struct ID_arr[]; if(getIDArr(ID_arr,from,till)) class=class="str">"cmt">// 获取独有 ID { class="type">int total=ArraySize(ID_arr); for(class="type">int i=class="num">0;i<total;i++) class=class="str">"cmt">// 遍历 ID { DealKeeper keeper; class=class="str">"cmt">// 存储一笔仓位的成交 keeper.ID=ID_arr[i].ID; keeper.symbol=ID_arr[i].Symb; keeper.DT_max = LONG_MIN; keeper.DT_min = LONG_MAX; if(HistorySelectByPosition(ID_arr[i].ID)) class=class="str">"cmt">// 选择指定 ID 仓位的所有成交 { class="type">int total_2=HistoryDealsTotal(); for(class="type">int n=class="num">0;n<total_2;n++) class=class="str">"cmt">// 循环遍历所选成交
◍ 逐笔成交的字段抓取与仓位归集
把历史成交逐条读进自定义结构,是做持仓复盘的第一步。下面这段逻辑在循环里用 HistoryDealGetTicket 按索引拿到成交票号,再借助 HistoryDealGetInteger / Double / String 把时间、订单号、买卖方向、魔术码、手续费、库存费、净利等 19 个字段取出来填进 DealData。 其中 DEAL_TIME 与 DEAL_TIME_MSC 分别给到秒级和毫秒级时间戳,用 MathMax / MathMin 顺手维护了该持仓的首末成交时间 DT_min / DT_max,后面按时间窗过滤会很方便。 全部成交塞进 keeper.deals 后,只要数组长度大于 0,就把整个 keeper 作为一条仓位追加到上层 deals 里;若外层连唯一 ID 都没有,直接返回 false。外汇与贵金属杠杆高,这类历史扫描只反映过去账户行为,不代表后续盈亏倾向。 调用时建议先在 MT5 策略测试器跑一小段历史,确认 DEAL_VOLUME 与 DEAL_PROFIT 的数值和你账户真实成交一致,再接去写统计逻辑。
class="type">long ticket=(class="type">long)HistoryDealGetTicket(n); DealData data; data.ID=keeper.ID; data.symbol=keeper.symbol; data.ticket= ticket; data.DT=(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME); keeper.DT_max=MathMax(keeper.DT_max,data.DT); keeper.DT_min=MathMin(keeper.DT_min,data.DT); data.order= HistoryDealGetInteger(ticket,DEAL_ORDER); data.type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE); data.DT_msc=HistoryDealGetInteger(ticket,DEAL_TIME_MSC); data.entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY); data.magic = HistoryDealGetInteger(ticket,DEAL_MAGIC); data.reason= (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON); data.volume= HistoryDealGetDouble(ticket,DEAL_VOLUME); data.price = HistoryDealGetDouble(ticket,DEAL_PRICE); data.comission=HistoryDealGetDouble(ticket,DEAL_COMMISSION); data.swap=HistoryDealGetDouble(ticket,DEAL_SWAP); data.profit=HistoryDealGetDouble(ticket,DEAL_PROFIT); data.comment=HistoryDealGetString(ticket,DEAL_COMMENT); data.ID_external=HistoryDealGetString(ticket,DEAL_EXTERNAL_ID); addArray(keeper.deals,data); class=class="str">"cmt">// 添加成交 } if(ArraySize(keeper.deals)>class="num">0) addArray(deals,keeper); class=class="str">"cmt">// 添加仓位 } } class="kw">return ArraySize(deals) > class="num">0; } else class="kw">return class="kw">false; class=class="str">"cmt">// 如果没有独有 ID } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 存储结算结果的结构,以及 | class=class="str">"cmt">//| 有关所选仓位的主要信息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct DealDetales { class="type">class="kw">string symbol; class=class="str">"cmt">// 品名
「成交记录结构体与加权均价算法」
做历史成交统计时,先把每笔仓位的关键字段固化成一个结构体,后续回测或复盘才不会乱。下面这组字段覆盖了从开平仓时间、周内天数、方向、价格到净盈亏的全链路信息,其中 pl_forDeal 已经把佣金扣掉,比单纯看 pl_oneLot 更接近真实账户表现。 datetime DT_open; // 开仓日期 datetime DT_close; // 平仓日期 ENUM_DAY_OF_WEEK day_open; // 开仓日星期几 ENUM_DAY_OF_WEEK day_close; // 平仓日星期几 double volume; // 交易量(手数) bool isLong; // true多头 false空头 double price_in; // 入场价 double price_out; // 出场价 double pl_oneLot; // 单手盈亏 double pl_forDeal; // 扣佣金后实际盈亏 string open_comment; // 开仓备注 string close_comment; // 平仓备注 加权开仓价不能简单求平均,得按手数加权。MA_price 函数用 prices[] 和 lots[] 两个引用数组,先算 ∑(price×lot) 再除以 ∑lot,返回的才是多笔加仓后的真实持仓成本。若你有三笔进场价 1.1000/1.1020/1.1050、手数 0.1/0.2/0.3,加权价约为 1.1030 而非中间的 1.1020。 calcContracts 解决的是「金字塔加仓后实际暴露多少手」的问题。它把每笔手数做累加数组,GET_REAL_CONTRACT 模式取数组最大值即历史峰值持仓,另一种模式取末值即平仓前最后一笔总仓。外汇与贵金属杠杆高,峰值持仓若超计划 2 倍,回撤可能非线性放大,用这个函数能快速揪出仓位失控的时段。
class="type">class="kw">datetime DT_open; class=class="str">"cmt">// 开仓日期 ENUM_DAY_OF_WEEK day_open; class=class="str">"cmt">// 开仓日期的周内天数 class="type">class="kw">datetime DT_close; class=class="str">"cmt">// 平仓日期 ENUM_DAY_OF_WEEK day_close; class=class="str">"cmt">// 平仓日期的周内天数 class="type">class="kw">double volume; class=class="str">"cmt">// 交易量 (手数) class="type">bool isLong; class=class="str">"cmt">// 多头/空头的符号 class="type">class="kw">double price_in; class=class="str">"cmt">// 仓位入场价格 class="type">class="kw">double price_out; class=class="str">"cmt">// 仓位立场价格 class="type">class="kw">double pl_oneLot; class=class="str">"cmt">// 交易一手的盈亏 class="type">class="kw">double pl_forDeal; class=class="str">"cmt">// 考虑到佣金的实际所获盈亏 class="type">class="kw">string open_comment; class=class="str">"cmt">// 开仓时的注释 class="type">class="kw">string close_comment; class=class="str">"cmt">// 平仓时的注释 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算加权平均开仓价 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CDealHistoryGetter::MA_price(class="type">class="kw">double &prices[],class="type">class="kw">double &lots[]) { class="type">class="kw">double summ=class="num">0; class=class="str">"cmt">// 加权价格的总和 class="type">class="kw">double delemetr=class="num">0; class=class="str">"cmt">// 权重之和 class="type">int total=ArraySize(prices); for(class="type">int i=class="num">0;i<total;i++) class=class="str">"cmt">// 求和循环 { summ+=(prices[i]*lots[i]); delemetr+=lots[i]; } class="kw">return summ/delemetr; class=class="str">"cmt">// 计算平均值 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算仓位的实际交易量,以及 | class=class="str">"cmt">//| 获取有关最后一笔仓位的交易量信息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CDealHistoryGetter::calcContracts(class="type">class="kw">double &Arr[],GetContractType type) { class="type">int total; if((total=ArraySize(Arr))>class="num">1) class=class="str">"cmt">// 如果数组大小大于 class="num">1 { class="type">class="kw">double lotArr[]; addArr(lotArr,Arr[class="num">0]); class=class="str">"cmt">// 将第一次的手数添加到数组中 for(class="type">int i=class="num">1;i<total;i++) class=class="str">"cmt">// 从第二个数组元素循环 addArr(lotArr,(lotArr[i-class="num">1]+Arr[i])); class=class="str">"cmt">// 将前一手数和当前手数的总和添加到数组中 (lotArr[i-class="num">1]+Arr[i])) if(type==GET_REAL_CONTRACT) class="kw">return lotArr[ArrayMaximum(lotArr)]; class=class="str">"cmt">// 返回实际最大交易手数 else class="kw">return lotArr[ArraySize(lotArr)-class="num">1]; class=class="str">"cmt">// 返回最后交易的手数 } else
成交边界类型的枚举与单仓记录组装
在 MT5 的成交历史回溯里,先把每一笔 deal 归类为边界点还是内部修正点,是后面算均价和盈亏的前提。这里用了一个 BorderPointType 枚举来区分:UsualInput 是 DEAL_ENTRY_IN 的普通开仓,UsualOutput 是 DEAL_ENTRY_OUT 的普通平仓,InOut 对应持仓反转,OutBy 是被反向单强平,OtherPoint 则归为余额操作、提款、保证金变动等需要忽略的项。 getDeals_forSelectedKeeper 方法接收一个 DealKeeper 引用和一个输出数组 ans[]。函数开头用 ArrayFree(ans) 清空旧数据,再取 inputParam.deals 的总长度做循环上限;循环体内对每笔成交调用 isBorderPoint 判断类型,非边界成交直接把 profit 加 commision 累进 detales.pl_forDeal,并用 calcContracts 折算成每标准手盈亏填进 pl_oneLot。 第 541 行有个容易漏的细节:如果 isAdd 为真且 PositionSelect 能选中当前 symbol,再比对 POSITION_IDENTIFIER 是否等于 inputParam.ID,相等就把 isAdd 置 false——这步是为了过滤掉还活着的仓位,只保留已了结或已失活的记录。最后在 isAdd 仍为真时,用 MA_price 对 price_In / lot_In 和 price_Out / lot_Out 求加权均价,calcContracts 取真实合约数,addArr 落盘到 ans。 开 MT5 把这段贴进 EA 的调试工程,把第 404、408、413 行的背景高亮变量打日志,能直接看到 InOut 类型如何在一次持仓 ID 下塞进多笔成交,以及均价计算是否被手数权重带偏。外汇与贵金属杠杆高,回测历史成交结构前先确认经纪商成交类型字段完整,避免把 OutBy 误算成普通平仓。
enum BorderPointType { UsualInput, class=class="str">"cmt">// 常用的入场类型(DEAL_ENTRY_IN) - 成交的开始 UsualOutput, class=class="str">"cmt">// 常用的离场类型(DEAL_ENTRY_OUT) - 成交的结束 OtherPoint, class=class="str">"cmt">// 余额操作,仓位修正,提款,变动保证金 — 将被忽略 InOut, class=class="str">"cmt">// 持仓反转 (DEAL_ENTRY_INOUT) OutBy class=class="str">"cmt">// 由相反的订单平仓的仓位 (DEAL_ENTRY_OUT_BY) }; class="type">void CDealHistoryGetter::getDeals_forSelectedKeeper(DealKeeper &inputParam,DealDetales &ans[]) { ArrayFree(ans); class="type">int total=ArraySize(inputParam.deals); DealDetales detales; detales.symbol=inputParam.symbol; detales.volume= class="num">0; detales.pl_forDeal=class="num">0; detales.pl_oneLot=class="num">0; detales.close_comment= ""; detales.open_comment = ""; detales.DT_open=class="num">0; class="type">bool isAdd=class="kw">false; class="type">bool firstPL_setted=class="kw">false; class="type">class="kw">double price_In[],price_Out[],lot_In[],lot_Out[],contracts[]; for(class="type">int i=class="num">0;i<total;i++) { BorderPointType type; class="type">class="kw">double pl_total=class="num">0; if(isBorderPoint(inputParam.deals[i],type)) { class=class="str">"cmt">// 第 class="num">413 行 } class=class="str">"cmt">// 第 class="num">414 行 else { detales.pl_forDeal+=(inputParam.deals[i].profit+inputParam.deals[i].comission); detales.pl_oneLot+=inputParam.deals[i].profit/calcContracts(contracts,GET_LAST_CONTRACT); } } if(isAdd && PositionSelect(inputParam.symbol)) { if(PositionGetInteger(POSITION_IDENTIFIER)==inputParam.ID) isAdd=class="kw">false; } if(isAdd) { detales.price_in=MA_price(price_In,lot_In); detales.price_out=MA_price(price_Out,lot_Out); detales.volume=calcContracts(contracts,GET_REAL_CONTRACT); addArr(ans,detales); } }
◍ 开平反三类成交的明细落库逻辑
在 MT5 的成交回放里,系统按成交类型分流处理:初始建仓或加仓走 UsualInput 分支,平仓走 UsualOut / OutBy 分支,持仓反转则进 InOut 分支。每一类都要把价格、手数、盈亏和注释分别塞进明细结构,否则后续统计单笔持仓的净表现会丢数据。 建仓侧先判断 detales.DT_open 是否为 0,只在首次写入开仓日期与周内天数;isLong 由 DEAL_TYPE_BUY 决定方向。pl_total 把 profit 与 comission 加总,detales.pl_forDeal 累加这个值,而 pl_oneLot 在首笔用 pl_total/volume,之后改用 profit 除以 calcContracts 取最后一格合约量——佣金只摊在第一次。 平仓侧不立即落库,因为同一仓位可能分几次离场,所以用 isAdd 标志挡住覆盖。price_Out、lot_Out 照收,pl_oneLot 在末笔用含佣金的 pl_total 除,中间笔只用 profit 除;contracts 数组以负号追加,表示仓位缩减。 别把反转当普通平仓看 InOut 分支第一步就要把 firstPL_setted 置 true,并调 calcContracts(contracts,GET_LAST_CONTRACT) 取出待平合约数,先固化旧仓再开新向。外汇与贵金属杠杆高,回测里漏掉反转的第一段保存,实盘复制时会造成盈亏错位。
if(type==UsualInput) class=class="str">"cmt">// 如果是初始入场或加仓 { if(detales.DT_open==class="num">0) class=class="str">"cmt">// 指定开仓日期 { detales.DT_open=inputParam.deals[i].DT; detales.day_open=getDay(inputParam.deals[i].DT); } detales.isLong=inputParam.deals[i].type==DEAL_TYPE_BUY; class=class="str">"cmt">// 判断仓位方向 addArr(price_In,inputParam.deals[i].price); class=class="str">"cmt">// 保存入场价格 addArr(lot_In,inputParam.deals[i].volume); class=class="str">"cmt">// 保存手数 pl_total=(inputParam.deals[i].profit+inputParam.deals[i].comission); detales.pl_forDeal+=pl_total; class=class="str">"cmt">// 考虑到佣金的成交盈亏 if(!firstPL_setted) { detales.pl_oneLot=pl_total/inputParam.deals[i].volume; class=class="str">"cmt">// 如果交易一手,考虑到佣金的成交盈亏 firstPL_setted=true; } else detales.pl_oneLot=inputParam.deals[i].profit/calcContracts(contracts,GET_LAST_CONTRACT); if(StringCompare(inputParam.deals[i].comment,"")!=class="num">0) class=class="str">"cmt">// 成交的注释 detales.open_comment+=(StringCompare(detales.open_comment,"")==class="num">0 ? inputParam.deals[i].comment : (" | "+inputParam.deals[i].comment)); addArr(contracts,inputParam.deals[i].volume); class=class="str">"cmt">// 增加交易量,且符号为“+” } if(type==UsualOut || type==OutBy) class=class="str">"cmt">// 平仓 { class=class="str">"cmt">/* 我们不会立即保存结果,因为可能有几次离场 因此,保留标志以避免数据丢失 */ if(!isAdd)isAdd=true; class=class="str">"cmt">// 用于保存仓位的标志 detales.DT_close=inputParam.deals[i].DT; class=class="str">"cmt">// 平仓日期 detales.day_close=getDay(inputParam.deals[i].DT); class=class="str">"cmt">// 平仓日期的周内天数 addArr(price_Out,inputParam.deals[i].price); class=class="str">"cmt">// 保存离场价格 addArr(lot_Out,inputParam.deals[i].volume); class=class="str">"cmt">// 保存离场交易量 pl_total=(inputParam.deals[i].profit+inputParam.deals[i].comission); class=class="str">"cmt">// 考虑到佣金的成交盈亏 detales.pl_forDeal+=pl_total; if(i==total-class="num">1) detales.pl_oneLot+=pl_total/calcContracts(contracts,GET_LAST_CONTRACT); class=class="str">"cmt">// 如果交易一手,考虑到佣金的成交盈亏 else detales.pl_oneLot+=inputParam.deals[i].profit/calcContracts(contracts,GET_LAST_CONTRACT); class=class="str">"cmt">// 如果交易一手,考虑到佣金的成交盈亏 if(StringCompare(inputParam.deals[i].comment,"")!=class="num">0) class=class="str">"cmt">// 成交注释 detales.close_comment+=(StringCompare(detales.close_comment,"")==class="num">0 ? inputParam.deals[i].comment : (" | "+inputParam.deals[i].comment)); addArr(contracts,-inputParam.deals[i].volume); class=class="str">"cmt">// 增加交易量,且符号为“-” } if(type==InOut) class=class="str">"cmt">// 持仓反转 { class=class="str">"cmt">/* 第一部分: 保存以前的仓位 */ firstPL_setted=true; class="type">class="kw">double closingContract=calcContracts(contracts,GET_LAST_CONTRACT); class=class="str">"cmt">// 平仓合约
「平仓切仓时如何重算明细与释放数组」
这段逻辑处理的是同一笔成交既平掉旧仓又开出新仓的情形,在 MT5 历史成交里很常见,尤其是部分平仓后剩余仓位反向或继续同向加仓。代码先按 closingContract 占原成交 volume 的比例,拆出属于平仓部分的盈亏与佣金,再单独为残留开仓部分建一条新明细。 double myLot=inputParam.deals[i].volume-closingContract; 这一行算出还剩多少手留在市场,addArr(contracts,-closingContract) 用负号把已平部分塞回合约数组,calcContracts 取实际存在的最大真实交易量。pl_total 用 (profit*closingContract)/volume 按比例切分,佣金同步按比例算,detales.pl_oneLot 再除以 closingContract 得到「若只做一手」的净盈亏参照。 平仓注释用 StringCompare 判断空串后拼接,多条注释以「 | 」间隔;price_in 与 price_out 都走 MA_price 按手数加权,拿到的是平均入场价与平均离场价,不是首笔成交价。 旧明细塞进 ans 数组后,立刻 ArrayFree 掉 price_In、price_Out、lot_In、lot_Out、contracts 五个动态数组,并把 open_comment、close_comment、volume 清零,避免下一条成交复用脏数据。第二部分把 myLot 重新 addArr 进 contracts,按全量 (profit+comission)*myLot/volume 算新仓盈亏,isLong 由 DEAL_TYPE_BUY 判定,新仓的 DT_open、day_open、open_comment、price_In 全部重写。外汇与贵金属杠杆高,这类切分若手数比例算错,回测权益曲线可能偏移 10% 以上,建议直接挂 MT5 策略测试器逐笔打印 detales 验证。
class="type">class="kw">double myLot=inputParam.deals[i].volume-closingContract; class=class="str">"cmt">// 开仓合约 addArr(contracts,-closingContract); class=class="str">"cmt">// 增加交易量,且符号为“-” detales.volume=calcContracts(contracts,GET_REAL_CONTRACT); class=class="str">"cmt">// 获得市场中实际存在的最大实际交易量 detales.DT_close=inputParam.deals[i].DT; class=class="str">"cmt">// 平仓日期 detales.day_close=getDay(inputParam.deals[i].DT); class=class="str">"cmt">// 平仓日期的周内天数 addArr(price_Out,inputParam.deals[i].price); class=class="str">"cmt">// 离场价格 addArr(lot_Out,closingContract); class=class="str">"cmt">// 离场交易量 pl_total=(inputParam.deals[i].profit*closingContract)/inputParam.deals[i].volume; class=class="str">"cmt">// 计算平仓成交的盈亏 class="type">class="kw">double commission_total=(inputParam.deals[i].comission*closingContract)/inputParam.deals[i].volume; detales.pl_forDeal+=(pl_total+commission_total); detales.pl_oneLot+=pl_total/closingContract; class=class="str">"cmt">// 如果交易一手,考虑到佣金的成交盈亏 if(StringCompare(inputParam.deals[i].comment,"")!=class="num">0) class=class="str">"cmt">// 保存平仓注释 detales.open_comment+=(StringCompare(detales.open_comment,"")==class="num">0 ? inputParam.deals[i].comment : (" | "+inputParam.deals[i].comment)); detales.price_in=MA_price(price_In,lot_In); class=class="str">"cmt">// 获取仓位入场价(平均) detales.price_out=MA_price(price_Out,lot_Out); class=class="str">"cmt">// 获取离场价格(平均) addArr(ans,detales); class=class="str">"cmt">// 添加已形成的仓位 if(isAdd)isAdd=class="kw">false; class=class="str">"cmt">// 如果已启用,请重置标志 class=class="str">"cmt">// 清除数据部分 ArrayFree(price_In); ArrayFree(price_Out); ArrayFree(lot_In); ArrayFree(lot_Out); ArrayFree(contracts); detales.close_comment=""; detales.open_comment=""; detales.volume=class="num">0; class=class="str">"cmt">/* 第二部分: 从 details 数组中删除数据部分后保存新仓位 */ addArr(contracts,myLot); class=class="str">"cmt">// 添加开仓手数 pl_total=((inputParam.deals[i].profit+inputParam.deals[i].comission)*myLot)/inputParam.deals[i].volume; class=class="str">"cmt">// 考虑到佣金的成交盈亏 detales.pl_forDeal=pl_total; detales.pl_oneLot=pl_total/myLot; class=class="str">"cmt">// 如果交易一手,考虑到佣金的成交盈亏 addArr(lot_In,myLot); class=class="str">"cmt">// 添加入场手数 detales.open_comment=inputParam.deals[i].comment; class=class="str">"cmt">// 保存注释 detales.DT_open=inputParam.deals[i].DT; class=class="str">"cmt">// 保存开仓日期 detales.day_open=getDay(inputParam.deals[i].DT); class=class="str">"cmt">// 保存开仓日期的周内天数 detales.isLong=inputParam.deals[i].type==DEAL_TYPE_BUY; class=class="str">"cmt">// 判断成交方向 addArr(price_In,inputParam.deals[i].price); class=class="str">"cmt">// 保存入场价格 }