EA 交易运行期间平衡曲线斜率的控制·进阶篇
(2/3)·恒定手数 EA 在连亏段容易把回撤拉爆,用斜率反馈自动削量才是进阶风控的缺口
◍ 账户余额历史的过滤与基类骨架
TBalanceHistory 是后续多个统计类的基类,核心职责只有一件:把 MT5 账户的交易历史按条件捞出来,供斜率、回撤等指标复用。它本身不管画图,只管把“哪些交易算数”这件事定清楚。 过滤条件通过 SetFiltrParams 落地,三个输入参数直接决定采样范围:magic 填 0 代表不区分 EA 幻数;type 接受 DEAL_TYPE_BUY、DEAL_TYPE_SELL 或 -1(长短线通吃);limit 为 0 时拉取全部可用历史,非零则限制回溯深度。对象构造时默认值即 magic=0、type=-1、limit=0,也就是“全量无过滤”起步。 真正产生数据的是 GetTradeResultsArray,它用 max_trades 控制写入数组的交易条数——这个值的下限是 2,因为算平衡曲线坡度至少得两个坐标点;填 0 则分析整段历史。方法内部先校验品种有效性与参数,再用 HistoryDealGetTicket 逐笔取成交 tick,借 HistoryDealGetInteger(DEAL_ENTRY) 筛出 DEAL_ENTRY_OUT 的平仓记录,按幻数、类型、品种、时间四项过关后才写入原始数组。 原始数组就绪后,SortMasterSlaveArray 用冒泡法对时间做升序,并让结果数组同步位移;再把同一分钟内的平仓归组进 group_datetime_array 与 group_result_array,消除秒级“颤振”。最后反转顺序使最早交易对应零号元素,并把逐笔结果替换为累积余额。外汇与贵金属账户历史受 broker 留存限制,实盘验证时 limit=0 未必能拿到早年全部 tick,建议先用策略测试器跑一段再比对数组长度。
<span class="comment">class=class="str">"cmt">//---------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">// 结余历史的操作:</span> <span class="comment">class=class="str">"cmt">//---------------------------------------------------------------------</span> <span class="keyword">class</span> TBalanceHistory { <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">long</span> current_magic; <span class="comment">class=class="str">"cmt">// 当存取历史合约时的 "幻数" ( class="num">0 - 任意数字 )</span> <span class="keyword">class="type">long</span> current_type; <span class="comment">class=class="str">"cmt">// 合约类型 ( -class="num">1 - 全部 )</span> <span class="keyword">class="type">int</span> current_limit_history; <span class="comment">class=class="str">"cmt">// 历史深度限制 ( class="num">0 - 所有历史 )</span> <span class="keyword">class="type">class="kw">datetime</span> monitoring_begin_date; <span class="comment">class=class="str">"cmt">// 监视合约历史的开始时间</span> <span class="keyword">class="type">int</span> real_trades; <span class="comment">class=class="str">"cmt">// 已执行的实际交易数量</span> <span class="keyword">class="kw">protected</span>: TradeSymbol trade_symbol; <span class="comment">class=class="str">"cmt">// 工作交易品种的操作</span> <span class="keyword">class="kw">protected</span>: <span class="comment">class=class="str">"cmt">// "原始" 数组:</span> <span class="keyword">class="type">class="kw">double</span> org_datetime_array[ ]; <span class="comment">class=class="str">"cmt">// 交易日期时间</span> <span class="keyword">class="type">class="kw">double</span> org_result_array[ ]; <span class="comment">class=class="str">"cmt">// 交易结果</span> <span class="comment">class=class="str">"cmt">// 以时间归组的数据数组:</span>
「交易历史类的字段与取数接口」
做账户净值曲线或交易分布图时,先把要存的数据结构定清楚。下面这组声明把每笔交易的日期时间和结果分开放进双精度数组,X 轴用 last_datetime_array 存时间戳,Y 轴用 last_result_array 存盈亏点数,两组必须同步排序否则画图会错位。
类里公开了几个直接能用的接口:SetTradeSymbol() 改监控品种,SetMonitoringBeginDate() 限定统计起点,SetFiltrParams() 按 magic number、订单类型、笔数上限过滤。GetTradeResultsArray(int _max_trades) 是核心取数函数,返回实际读到的交易笔数,且不会超过传入的 _max_trades。
私有方法 SortMasterSlaveArray() 负责把主数组和从数组按升序一起重排,调用方不用自己管下标对齐。想在 MT5 里验证,新建 EA 包含这个类,传 _max_trades=500 拉最近 500 笔,看返回的 count 是否等于 HistoryDealsTotal() 中符合过滤条件的真实数目。
外汇与贵金属杠杆高,历史统计只反映过去,样本外的分布可能明显偏离,结论仅作概率参考。
class="type">class="kw">double group_datetime_array[ ]; class=class="str">"cmt">// 交易日期时间 class="type">class="kw">double group_result_array[ ]; class=class="str">"cmt">// 交易结果 class="type">class="kw">double last_result_array[ ]; class=class="str">"cmt">// 保存最后交易结果的数组 ( Y 轴上的点数 ) class="type">class="kw">double last_datetime_array[ ]; class=class="str">"cmt">// 存最后交易时间的数组 ( X 轴上的点数 ) class="kw">private: class="type">void SortMasterSlaveArray( class="type">class="kw">double& _m[ ], class="type">class="kw">double& _s[ ] ); class=class="str">"cmt">// 同步两个升序数组 class="kw">public: class="type">void SetTradeSymbol( class="type">class="kw">string _symbol ); class=class="str">"cmt">// 设置/改变 工作交易品种 class="type">class="kw">string GetTradeSymbol( ); class=class="str">"cmt">// 获取工作交易品种 class="type">void RefreshSymbolInfo( ); class=class="str">"cmt">// 刷新工作交易品种的市场情报 class="type">void SetMonitoringBeginDate( class="type">class="kw">datetime _dt ); class=class="str">"cmt">// 设置监视开始时间 class="type">class="kw">datetime GetMonitoringBeginDate( ); class=class="str">"cmt">// 获取监视开始时间 class="type">void SetFiltrParams( class="type">long _magic, class="type">long _type = -class="num">1, class="type">int _limit = class="num">0 );class=class="str">"cmt">// 设置合约过滤参数 class="kw">public: class=class="str">"cmt">// 得到最后交易结果: class="type">int GetTradeResultsArray( class="type">int _max_trades ); class="kw">public: class="type">void TBalanceHistory( ); class=class="str">"cmt">// 构造器 class="type">void ~TBalanceHistory( ); class=class="str">"cmt">// 析构器 }; class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 读取数组最近(按时间)交易的结果: class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// - 返回实际读取交易数量但不超过指定数量; class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int TBalanceHistory::GetTradeResultsArray( class="type">int _max_trades ) { class="type">int index, limit, count; class="type">long deal_type, deal_magic, deal_entry;
历史成交的过滤与数组装载
这段逻辑干的事很直接:从 MT5 历史里捞平仓单,按品种、幻数、类型和时间窗口筛一遍,再把合格记录塞进原始数组供后续统计。外汇与贵金属杠杆高,历史回测结果仅代表过去样本,实盘复制可能偏离。 先卡两道硬门槛:_max_trades 小于 2 直接返回 0,工作品种 symbol 为空也返回 0。接着 HistorySelect 拉从 monitoring_begin_date 到当前的所有历史,失败就退出;HistoryDealsTotal 拿到总数 count,若 count 小于所需交易数同样放弃。 若设了 current_limit_history 且历史多于它,就从尾部截掉多余部分(limit = count - current_limit_history),否则 limit 为 0。ArrayResize 把 org_datetime_array 和 org_result_array 调到 count - limit 大小,避免越界。 倒序遍历时每层都做 continue 过滤:DEAL_ENTRY 不是 OUT(未平仓)跳过;幻数不匹配跳过;品种不对跳过;类型不符或不是纯买卖单跳过;平仓时间早于监控起点也跳过。留下的才是真实样本。 让小布替你跑这套 把 monitoring_begin_date 设成最近 90 天、current_magic 填你 EA 的幻数,能快速验证这段过滤是否漏掉了手动平仓单。
class="type">class="kw">datetime deal_close_time, current_time; class="type">ulong deal_ticket; class=class="str">"cmt">// 合约的单号 class="type">class="kw">double trade_result; class="type">class="kw">string symbol, deal_symbol; real_trades = class="num">0; class=class="str">"cmt">// 交易数量不得少于 class="num">2: if( _max_trades < class="num">2 ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 如果工作交易品种未指定, 不做任何事: symbol = trade_symbol.GetTradeSymbol( ); if( symbol == NULL ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 请求从指定时间至当前时刻的历史合约与订单: if( HistorySelect( monitoring_begin_date, TimeCurrent( )) != true ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 计算交易数量: count = HistoryDealsTotal( ); class=class="str">"cmt">// 如果历史交易数量少于必要数量, 则退出: if( count < _max_trades ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 如果历史交易数量多于必要数量, 则限制它们: if( current_limit_history > class="num">0 && count > current_limit_history ) { limit = count - current_limit_history; } else { limit = class="num">0; } class=class="str">"cmt">// 如果需要, 调整 "原始" 数组的维度至指定的交易数量: if(( ArraySize( org_datetime_array )) != ( count - limit )) { ArrayResize( org_datetime_array, count - limit ); ArrayResize( org_result_array, count - limit ); } class=class="str">"cmt">// 以历史交易填充 "原始" 数组: real_trades = class="num">0; for( index = count - class="num">1; index >= limit; index-- ) { deal_ticket = HistoryDealGetTicket( index ); class=class="str">"cmt">// 如果合约未平仓, 不要继续: deal_entry = HistoryDealGetInteger( deal_ticket, DEAL_ENTRY ); if( deal_entry != DEAL_ENTRY_OUT ) { class="kw">continue; } class=class="str">"cmt">// 检查合约的 "幻数",如果必要: deal_magic = HistoryDealGetInteger( deal_ticket, DEAL_MAGIC ); if( current_magic != class="num">0 && deal_magic != current_magic ) { class="kw">continue; } class=class="str">"cmt">// 检查合约交易品种: deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL ); if( symbol != deal_symbol ) { class="kw">continue; } class=class="str">"cmt">// 检查合约类型,如果必要: deal_type = HistoryDealGetInteger( deal_ticket, DEAL_TYPE ); if( current_type != -class="num">1 && deal_type != current_type ) { class="kw">continue; } else if( current_type == -class="num">1 && ( deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL )) { class="kw">continue; } class=class="str">"cmt">// 检查合约平仓时间: deal_close_time = ( class="type">class="kw">datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME ); if( deal_close_time < monitoring_begin_date ) { class="kw">continue; }
◍ 按平仓时间归并历史成交明细
做历史成交统计时,先把每笔平仓时间和单笔盈亏(利润除以成交量)记进原始数组,再用 SortMasterSlaveArray 按时间主从排序,避免后续分组错乱。若实际交易数 real_trades 小于预设的 _max_trades,函数直接 return(0),说明样本不足,外汇与贵金属品种在高波动时段可能频繁出现样本断层,需调大监控窗口。 分组阶段以平仓时间为键:遍历原始数组,若输出数组当前时间戳大于0且与时点差值的绝对值大于0.0,就下移指针写入新元素;否则把盈亏累加进同一时间槽。注意此处用 group_datetime_array[real_trades] 作判等基准,若 real_trades 初值未对齐易漏并,开 MT5 跑这段代码时建议先打印 real_trades 与 index 的对应值。 最后把分组结果逆序写回 last_result_array,并对前 _max_trades 项做前缀累加,使数组第 n 位表示前 n 笔的累计盈亏。下面这段是原文核心逻辑的代码原文,可直接复制到 MQ5 文件里逐行验证。
org_datetime_array[ real_trades ] = deal_close_time / class="num">60; org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME ); real_trades++; } class=class="str">"cmt">// 如果此时少于必要交易数, 返回: if( real_trades < _max_trades ) { class="kw">return( class="num">0 ); } count = real_trades; class=class="str">"cmt">// 以订单关闭时间对 "原始" 数组排序: SortMasterSlaveArray( org_datetime_array, org_result_array ); class=class="str">"cmt">// 如果必要, 调整 group 数组的维度至指定的点数: if(( ArraySize( group_datetime_array )) != count ) { ArrayResize( group_datetime_array, count ); ArrayResize( group_result_array, count ); } ArrayInitialize( group_datetime_array, class="num">0.0 ); ArrayInitialize( group_result_array, class="num">0.0 ); class=class="str">"cmt">// 以分组数据填充输出数组 ( 平仓时间为标识的分组 ): for( index = class="num">0; index < count; index++ ) { class=class="str">"cmt">// 得到其它交易: deal_close_time = ( class="type">class="kw">datetime )org_datetime_array[ index ]; trade_result = org_result_array[ index ]; class=class="str">"cmt">// 现在检查是否在输出数组中存在时间相同: current_time = ( class="type">class="kw">datetime )group_datetime_array[ real_trades ]; if( current_time > class="num">0 && MathAbs( current_time - deal_close_time ) > class="num">0.0 ) { real_trades++; class=class="str">"cmt">// 移动指针至下一个元素 group_result_array[ real_trades ] = trade_result; group_datetime_array[ real_trades ] = deal_close_time; } else { group_result_array[ real_trades ] += trade_result; group_datetime_array[ real_trades ] = deal_close_time; } } real_trades++; class=class="str">"cmt">// 现在是唯一元素的数量 class=class="str">"cmt">// 如果此时少于必要交易数, 退出: if( real_trades < _max_trades ) { class="kw">return( class="num">0 ); } if( ArraySize( last_result_array ) != _max_trades ) { ArrayResize( last_result_array, _max_trades ); ArrayResize( last_datetime_array, _max_trades ); } class=class="str">"cmt">// 以逆向索引写积累的数据到输出数组: for( index = class="num">0; index < _max_trades; index++ ) { last_result_array[ _max_trades - class="num">1 - index ] = group_result_array[ index ]; last_datetime_array[ _max_trades - class="num">1 - index ] = group_datetime_array[ index ]; } class=class="str">"cmt">// 在输出数组中,以积累的总和取代单个交易的结果: for( index = class="num">1; index < _max_trades; index++ ) { last_result_array[ index ] += last_result_array[ index - class="num">1 ]; } class="kw">return( _max_trades ); } class=class="str">"cmt">// 请求从指定时间至当前时刻的历史合约与订单: if( HistorySelect( monitoring_begin_date, TimeCurrent( )) != true ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 计算交易数量: count = HistoryDealsTotal( ); class=class="str">"cmt">// 如果历史交易数量少于必要数量, 则退出: if( count < _max_trades ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 以历史交易填充 "原始" 数组: real_trades = class="num">0; for( index = count - class="num">1; index >= limit; index-- ) { deal_ticket = HistoryDealGetTicket( index ); class=class="str">"cmt">// 如果交易未关闭, 不要继续:
「按平仓时间归并历史成交明细」
在回测或实盘统计里,直接遍历 HistoryDeal 拿到的每笔成交是散的。上面这段代码先过滤:只保留 DEAL_ENTRY_OUT 的平仓单,再按幻数、品种、类型和时间窗一层层 continue 掉不符的,把有效平仓的「分钟级时间」和「单手利润」分别塞进 org_datetime_array 与 org_result_array。 单手利润用 HistoryDealGetDouble(deal_ticket, DEAL_PROFIT) 除以同笔 DEAL_VOLUME 得到,这一步把不同手数的成交归一化,方便横向比。若 real_trades 小于传入的 _max_trades,函数直接 return(0),说明样本不够,后续分布检验无意义。 归并逻辑在第二层循环:以平仓时间为 key,若 group_datetime_array 里当前元素与这笔成交时间差绝对值大于 0.0(即不同分钟),就新开一组;否则把 trade_result 累加进同组。最终 real_trades 自增后代表唯一时间组的数量,外汇与贵金属杠杆高,这种按分钟聚合的方式可能掩盖滑点导致的秒级偏差,开 MT5 跑时建议把 0.0 阈值换成具体秒数验证。
deal_entry = HistoryDealGetInteger( deal_ticket, DEAL_ENTRY ); if( deal_entry != DEAL_ENTRY_OUT ) { class="kw">continue; } class=class="str">"cmt">// 检查合约的 "幻数",如果必要: deal_magic = HistoryDealGetInteger( deal_ticket, DEAL_MAGIC ); if( _magic != class="num">0 && deal_magic != _magic ) { class="kw">continue; } class=class="str">"cmt">// 检查合约交易品种: deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL ); if( symbol != deal_symbol ) { class="kw">continue; } class=class="str">"cmt">// 检查合约类型,如果必要: deal_type = HistoryDealGetInteger( deal_ticket, DEAL_TYPE ); if( _type != -class="num">1 && deal_type != _type ) { class="kw">continue; } else if( _type == -class="num">1 && ( deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL )) { class="kw">continue; } class=class="str">"cmt">// 检查合约平仓时间: deal_close_time = ( class="type">class="kw">datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME ); if( deal_close_time < monitoring_begin_date ) { class="kw">continue; } class=class="str">"cmt">// 所以, 我们可以读其它交易: org_datetime_array[ real_trades ] = deal_close_time / class="num">60; org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME ); real_trades++; } class=class="str">"cmt">// 如果此时少于必要交易数, 退出: if( real_trades < _max_trades ) { class="kw">return( class="num">0 ); } class=class="str">"cmt">// 检查合约平仓时间: deal_close_time = ( class="type">class="kw">datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME ); if( deal_close_time < monitoring_begin_date ) { class="kw">continue; } class=class="str">"cmt">// 以分组数据填充输出数组 ( 平仓时间为标识的分组 ): real_trades = class="num">0; for( index = class="num">0; index < count; index++ ) { class=class="str">"cmt">// 得到其它交易: deal_close_time = ( class="type">class="kw">datetime )org_datetime_array[ index ]; trade_result = org_result_array[ index ]; class=class="str">"cmt">// 现在检查是否在输出数组中存在时间相同: current_time = ( class="type">class="kw">datetime )group_datetime_array[ real_trades ]; if( current_time > class="num">0 && MathAbs( current_time - deal_close_time ) > class="num">0.0 ) { real_trades++; group_result_array[ real_trades ] = trade_result; group_datetime_array[ real_trades ] = deal_close_time; } else { group_result_array[ real_trades ] += trade_result; group_datetime_array[ real_trades ] = deal_close_time; } } real_trades++; class=class="str">"cmt">// 以逆向索引写积累的数据到输出数组: for( index = class="num">0; index < _max_trades; index++ ) {
把单笔结果翻成累计曲线
上面这段逻辑干的事很直接:先把最近 _max_trades 笔交易的盈亏和开仓时间,从临时分组数组倒序塞进 last_result_array 与 last_datetime_array。倒序写入的偏移量是 _max_trades - 1 - index,保证数组头部是最早那笔、尾部是最近一笔。 随后一个从 index=1 开始的循环,把 last_result_array 里每一项都加上前一项,等价于在数组里就地算出累计盈亏序列。MT5 里把这组数丢进缓冲区画线,就能直观看到策略在窗口期内的净值爬坡,而不是被单笔毛刺带偏。 外汇与贵金属品种点差和滑点会吃掉毛刺交易,实盘前务必把 _max_trades 调到与你复盘周期匹配,否则累计曲线可能只覆盖了几根 H1 就断片。
last_result_array[ _max_trades - class="num">1 - index ] = group_result_array[ index ]; last_datetime_array[ _max_trades - class="num">1 - index ] = group_datetime_array[ index ]; } class=class="str">"cmt">// 在输出数组中,以积累的总和取代单个交易的结果: for( index = class="num">1; index < _max_trades; index++ ) { last_result_array[ index ] += last_result_array[ index - class="num">1 ]; }