将入场信息解析到指标·进阶篇
(2/3)·把策略测试里的每笔开仓指标快照扒出来,横盘亏货也许能提前砍掉
◍ 从 HTML 报表里抠出成交明细的拆表逻辑
做账户复盘时,MT5 导出的成交报表常是一整段 HTML 表格,直接读字符串没法用。下面这段思路是:先找一个不会出现在报表正文里的 ASCII 字符当分隔符,再把表格拆开。 代码从 1 到 254 遍历 uchar,用 CharToString 转成单字符,拿 StringFind 去 html_report 里查;只要报表里没这个字符,就把它设为 delimiter 并记下 separate 的码值,随即 break。若跑完循环 delimiter 还是 NULL,说明 255 个可见字符全撞车,函数直接 return false。 拿到分隔符后,用 StringReplace 把结尾的 </table> 替换成 delimiter,再 StringSplit 切出所有表块。若切出来的块数 size<=1,同样返回失败——这意味着报表结构异常,可能是空表或被截断。 真正有用的数据在倒数第二块 html_tables[size-2] 里。再把它按 delimiter 拆行,逐行找含 "Deals" 的起始标记;找到后才开始解析每一行 <td> 内容。注意代码里硬性要求每行切出的列数 temp>=13,少于 13 列的直接 continue,这是过滤表头与合计行的关键阈值,开 MT5 跑的时候别随手改小。 方向字段读的是 columns[3],只认 "buy"/"sell" 两种字符串,映射成 POSITION_TYPE_BUY / POSITION_TYPE_SELL,其余值一律当 -1 跳过。外汇与贵金属复盘涉及真实杠杆与滑点,报表解析错一列都可能把持仓方向算反,属高风险操作,验证时建议先拿模拟账户导出文件试跑。
class="type">class="kw">string delimiter = NULL; class="type">class="kw">ushort separate = class="num">0; for(class="type">uchar tr=class="num">1;tr<class="num">255;tr++) { class="type">class="kw">string temp = CharToString(tr); if(StringFind(html_report,temp,class="num">0)>class="num">0) class="kw">continue; delimiter = temp; separate = tr; break; } if(delimiter==NULL) class="kw">return false; if(StringReplace(html_report,"</table>",delimiter)<=class="num">0) class="kw">return false; class=class="str">"cmt">//--- s_Symbol=NULL; car_Deals.Clear(); class=class="str">"cmt">//--- class="type">class="kw">string html_tables[]; class="type">int size=StringSplit(html_report,separate,html_tables); if(size<=class="num">1) class="kw">return false; if(StringReplace(html_tables[size-class="num">2],"</tr>",delimiter)<=class="num">0) class="kw">return false; size=StringSplit(html_tables[size-class="num">2],separate,html_tables); if(size<=class="num">1) class="kw">return false; class="type">bool found_start=false; class="type">class="kw">double opened_volume=class="num">0; for(class="type">int i=class="num">0;i<size;i++) { class=class="str">"cmt">//--- if(!found_start) { if(StringFind(html_tables[i],"Deals",class="num">0)>=class="num">0) found_start=true; class="kw">continue; } class="type">class="kw">string columns[]; class="type">int temp=StringFind(html_tables[i],"<td>",class="num">0); if(temp<class="num">0) class="kw">continue; if(temp>class="num">0) html_tables[i]=StringSubstr(html_tables[i],temp); StringReplace(html_tables[i],"<td>",""); StringReplace(html_tables[i],"</td>",delimiter); temp=StringSplit(html_tables[i],separate,columns); if(temp<class="num">13) class="kw">continue; class=class="str">"cmt">//--- class="type">ENUM_POSITION_TYPE e_direction = (class="type">ENUM_POSITION_TYPE)(columns[class="num">3]=="buy" ? POSITION_TYPE_BUY : columns[class="num">3]=="sell" ? POSITION_TYPE_SELL : -class="num">1); if(e_direction==-class="num">1) class="kw">continue; class=class="str">"cmt">//---
「逐笔成交里的字段清洗与平仓匹配」
解析券商成交记录时,字符串列里常混着空格和不可见字符,直接转数值会丢精度。上面这段先把时间列用 StringToTime 转成 datetime,再把成交量、佣金、库存费、盈亏这几列用 StringReplace 把空格干掉,最后 StringToDouble 落库。 成交量在原始 CSV 的第 6 列(索引 5),佣金在第 9 列(索引 8),库存费第 10 列(索引 9),盈亏第 11 列(索引 10)。注意 StringReplace 改的是原数组元素本身,调用后 columns[5] 就不再是带空格的字符串了。 平仓逻辑靠『out』关键词识别:当 opened_volume 大于 0 且第 5 列(索引 4)包含『out』,就遍历 car_Deals 里的逐笔挂单。这里用 MathMin(opened_volume, d_volume) 取实际可平量,再按 deal 未平仓部分逐笔吃。 佣金和库存费不是整笔扣,而是按 close_volume / total_volume 的比例切。比如 d_volume=0.5、close_volume=0.2,佣金就只摊 40%。外汇和贵金属杠杆高,这类按比例切分算错一位,回测权益曲线就可能漂 5% 以上,建议开 MT5 把这段塞进自己的成交解析器里单步跑一遍。
class="type">class="kw">datetime dt_time = StringToTime(columns[class="num">0]); StringReplace(columns[class="num">5]," ",""); class="type">class="kw">double d_volume = StringToDouble(columns[class="num">5]); StringReplace(columns[class="num">8]," ",""); class="type">class="kw">double d_comission = StringToDouble(columns[class="num">8]); StringReplace(columns[class="num">9]," ",""); class="type">class="kw">double d_swap = StringToDouble(columns[class="num">9]); StringReplace(columns[class="num">10]," ",""); class="type">class="kw">double d_profit = StringToDouble(columns[class="num">10]); if(s_Symbol==NULL || s_Symbol=="") { s_Symbol=columns[class="num">2]; StringTrimLeft(s_Symbol); StringTrimRight(s_Symbol); } if(opened_volume>class="num">0 && StringFind(columns[class="num">4],"out",class="num">0)>=class="num">0) { class="type">int total=car_Deals.Total(); class="type">class="kw">double total_volume=MathMin(opened_volume,d_volume); for(class="type">int d=class="num">0;(d<total && e_direction!=(-class="num">1) && total_volume>class="num">0);d++) { CDeal *deal=car_Deals.At(d); if(CheckPointer(deal)==POINTER_INVALID) class="kw">continue; class=class="str">"cmt">//--- if(deal.Type()==e_direction) class="kw">continue; class=class="str">"cmt">//--- class="type">class="kw">double deal_unclosed=class="num">0; if(deal.IsClosed(deal_unclosed)) class="kw">continue; class="type">class="kw">double close_volume = MathMin(deal_unclosed,total_volume); class="type">class="kw">double close_comission = d_comission/d_volume*close_volume; class="type">class="kw">double close_swap = d_swap/total_volume*close_volume; class="type">class="kw">double close_profit = d_profit/total_volume*close_volume; if(deal.Close(close_volume,close_profit,close_comission,close_swap))
平仓后如何扣减持仓与登记剩余成交
这段逻辑出现在成交记录回放的中段:每当识别到一笔平仓,就先把对应的成交量、佣金、库存费、盈亏从三个累计变量里减掉。 opened_volume -= close_volume 减的是当前未平总仓;d_volume -= close_volume 减的是当日成交量;total_volume -= close_volume 减的是全周期总成交量。紧随其后的 d_comission、d_swap、d_profit 三行,分别回撤该笔平仓带来的佣金、隔夜息与盈亏,保证后续统计不会被已关闭的头寸污染。 扣减完成后,代码判断 if(d_volume>0 && StringFind(columns[4],"in",0)>=0):若当日仍有未平量,且方向字段里能查到 "in" 字样,就 new 一个 CDeal 对象把剩余敞口登记进 car_Deals 数组。这里用 CheckPointer 确认指针有效,再用 car_Deals.Add 落库,任一步失败直接 return false。 把这段塞进你的 MT5 脚本跑一遍:用真实账户成交历史导出 CSV,盯住 d_volume 在多次平仓后是否精确归零;外汇与贵金属杠杆高,回放逻辑错一处,敞口统计就会偏离实际风险。
{
opened_volume -= close_volume;
d_volume -= close_volume;
total_volume -= close_volume;
d_comission -= close_comission;
d_swap -= close_swap;
d_profit -= close_profit;
}
}
if(d_volume>class="num">0 && StringFind(columns[class="num">4],"in",class="num">0)>=class="num">0)
{
CDeal *deal = new CDeal(e_direction,dt_time,d_volume,d_comission);
if(CheckPointer(deal)==POINTER_INVALID)
class="kw">return false;
if(!car_Deals.Add(deal))
class="kw">return false;
opened_volume += d_volume;
}
}
class="kw">return (car_Deals.Total()>class="num">0);
}◍ 把指标值绑到每笔成交上
做无明确趋势下的亏损单筛选,核心不是先发明新趋势判定法,而是把已成交单和当时指标数值一一对应,留待后续有意识优化。ATR 这类振荡器在趋势中波动放大、读数走高,所以至少该存两个量:最后一根收盘蜡烛的指标值(当前波动水平),以及它相对前一根的比值(波动交替的动态)。 单缓冲区指标(ATR、CCI、Volumes、Force、Chaikin、标准偏差都算)用一套通用类就够了。底层 CValue 只管一笔单:私有变量 Value、Dinamic、Deal_Ticket 分别存指标值、两值比率、成交单号,对外暴露 GetTicket / GetValue / GetDinamic / GetValues 取数。上层 COneBufferArray 拿指标句柄和 CArrayObj 数组,SaveNewValues(long ticket) 先查数组与句柄状态,再 CopyBuffer 取数,失败即返 false,成功才 new 一个 CValue 压进数组。 MACD 有主线和信号线两个缓冲区,类结构照扩,但初始化时直接把指标参数传进去、在类内部 iCustom 建句柄,不再从外部塞 handle。ADX 三个缓冲区同理加变量。缓冲区数量可任意伸缩,不过现阶段别提前删减——盈利单和哪个缓冲区数值有关联、关联多强,都还没证据。 外汇与贵金属杠杆高、滑点跳空频繁,这类绑定分析只帮你定位疑似亏损区,不预示任何方向。开 MT5 把下面类骨架丢进 EA 附件,接 ATR 句柄跑一周,看 Dinamic 极端值那几单的后续表现,比拍脑袋调参实在。
class CValue : class="kw">public CObject { class="kw">private: class="type">class="kw">double Value; class=class="str">"cmt">//指标值 class="type">class="kw">double Dinamic; class=class="str">"cmt">//指标的动态值 class="type">long Deal_Ticket; class=class="str">"cmt">//成交单号 class="kw">public: CValue(class="type">class="kw">double value, class="type">class="kw">double dinamic, class="type">long ticket); ~CValue(class="type">void); class=class="str">"cmt">//--- class="type">long GetTicket(class="type">void) { class="kw">return Deal_Ticket; } class="type">class="kw">double GetValue(class="type">void) { class="kw">return Value; } class="type">class="kw">double GetDinamic(class="type">void) { class="kw">return Dinamic; } class="type">void GetValues(class="type">class="kw">double &value, class="type">class="kw">double &dinamic); }; class COneBufferArray : CObject { class="kw">private: CArrayObj *IndicatorValues; class=class="str">"cmt">//指标数值数组 class="type">int i_handle; class=class="str">"cmt">//指标句柄 class="type">class="kw">string s_Name; class="type">class="kw">string GetIndicatorName(class="type">int handle); class="kw">public: COneBufferArray(class="type">int handle); ~COneBufferArray(); class=class="str">"cmt">//--- class="type">bool SaveNewValues(class="type">long ticket); class=class="str">"cmt">//---
「把指标缓冲塞进持仓对象里」
做多品种盯盘时,最麻烦的是把指标值和某张订单绑在一起。上面这段把单缓冲数组封装成 COneBufferArray,再用 SaveNewValues 把指标最新两根柱的值抓进对象,方便后续按 ticket 检索。 CopyBuffer 取的是偏移 1 起始的 2 根柱(即最近完整柱与倒数第二根),若返回少于 2 个值直接退出,避免脏数据入库。动态值用 ind_buffer[1]/ind_buffer[0] 计算,分母为 0 时兜底成 1,这个边界处理在贵金属跳空时可能救你一命。 CMACDValue 则是把 MACD 主线、信号线各自的数值与动态比值都存进一个 CObject 派生类,Deal_Ticket 字段保证能反查到具体成交单。外汇与贵金属杠杆高、滑点突变频繁,这类封装只是降低代码耦合,不预示任何方向。 下面这段是核心存取逻辑,注意指针无效与句柄无效的双重拦截:
class="type">bool COneBufferArray::SaveNewValues(class="type">long ticket) { if(CheckPointer(IndicatorValues)==POINTER_INVALID) class="kw">return false; if(i_handle==INVALID_HANDLE) class="kw">return false; class="type">class="kw">double ind_buffer[]; if(CopyBuffer(i_handle,class="num">0,class="num">1,class="num">2,ind_buffer)<class="num">2) class="kw">return false; CValue *object=new CValue(ind_buffer[class="num">1], (ind_buffer[class="num">0]!=class="num">0 ? ind_buffer[class="num">1]/ind_buffer[class="num">0] : class="num">1), ticket); if(CheckPointer(object)==POINTER_INVALID) class="kw">return false; if(s_Name==NULL) s_Name=GetIndicatorName(i_handle); class="kw">return IndicatorValues.Add(object); }
把 MACD 封装成可复用的取值类
上面这段把 MACD 指标包成了一个 CMACD 类,外部调用时不用再反复写 CopyBuffer,直接拿 ticket 换主值、信号值和它们的动态比值。 SaveNewValues 里先校验指针和句柄,再用 CopyBuffer 从 buffer 0 和 1 各取最近 2 根(偏移 1 起,长度 2)。若任一组不足 2 个数据就返回 false,避免后面除零或越界。 动态比值用 main[1]/main[0] 计算,若 main[0] 为 0 则兜底成 1,信号线同理。这样历史每根 K 线都能存一个 CMACDValue 对象进 IndicatorValues 数组,回测时按 ticket 取用即可。 外汇和贵金属行情跳空频繁,句柄失效和缓冲不足在实盘概率不低,务必在调用前判 INVALID_HANDLE,否则可能读到陈旧信号。
class="type">class="kw">double GetSignalValue(class="type">void) { class="kw">return Signal_Value; } class="type">class="kw">double GetSignalDinamic(class="type">void) { class="kw">return Signal_Dinamic; } class="type">void GetValues(class="type">class="kw">double &main_value, class="type">class="kw">double &main_dinamic, class="type">class="kw">double &signal_value, class="type">class="kw">double &signal_dinamic); }; class CMACD { class="kw">private: CArrayObj *IndicatorValues; class=class="str">"cmt">//指标数值数组 class="type">int i_handle; class=class="str">"cmt">//指标句柄 class="kw">public: CMACD(class="type">class="kw">string symbol, ENUM_TIMEFRAMES timeframe, class="type">uint fast_ema, class="type">uint slow_ema, class="type">uint signal, ENUM_APPLIED_PRICE applied_price); ~CMACD(); class=class="str">"cmt">//--- class="type">bool SaveNewValues(class="type">long ticket); class=class="str">"cmt">//--- class="type">class="kw">double GetMainValue(class="type">long ticket); class="type">class="kw">double GetMainDinamic(class="type">long ticket); class="type">class="kw">double GetSignalValue(class="type">long ticket); class="type">class="kw">double GetSignalDinamic(class="type">long ticket); class="type">bool GetValues(class="type">long ticket, class="type">class="kw">double &main_value, class="type">class="kw">double &main_dinamic, class="type">class="kw">double &signal_value, class="type">class="kw">double &signal_dinamic); }; class="type">bool CMACD::SaveNewValues(class="type">long ticket) { if(CheckPointer(IndicatorValues)==POINTER_INVALID) class="kw">return false; if(i_handle==INVALID_HANDLE) class="kw">return false; class="type">class="kw">double main[], signal[]; if(!CopyBuffer(i_handle,class="num">0,class="num">1,class="num">2,main)<class="num">2 || !CopyBuffer(i_handle,class="num">1,class="num">1,class="num">2,signal)<class="num">2) class="kw">return false; CMACDValue *object=new CMACDValue(main[class="num">1], (main[class="num">0]!=class="num">0 ? main[class="num">1]/main[class="num">0] : class="num">1), signal[class="num">1], (signal[class="num">0]!=class="num">0 ? signal[class="num">1]/signal[class="num">0] : class="num">1), ticket); if(CheckPointer(object)==POINTER_INVALID) class="kw">return false; class="kw">return IndicatorValues.Add(object); } class CADXValue : class="kw">public CObject { class="kw">private: class="type">class="kw">double ADX_Value; class=class="str">"cmt">//ADX 数值