使用TesterWithdrawal() 函数模拟利润提取·进阶篇
(2/3)· 多数回测只盯着曲线上扬,却从没问过账户真被抽走生活费后系统还扛不扛得住
「给通道突破EA接上提款与优化接口」
这套EA的逻辑不复杂:取5分钟周期里指定长度的最高、最低价,画出由CalcHigh和CalcLow构成的水平通道,挂单押突破。PERIOD输入决定回看窗口,INSIDE_LEVEL控制价格进入通道多深才允许挂单,同一方向只允许一个仓位或挂单。通道重新计算后变窄、价格还在里面时,挂单按ORDER_STEP向市价靠拢。 盈利和止损分别由TAKE_PROFIT、STOP_LOSS锁定,TRAILING_STOP给移动止损步长。手数可用LOT固定,也可按存款百分比由LOT_TYPE切换;当固定手数超允许上限且LOT_CORRECTION=true时,会被纠正到最近合法值。WRITE_LOG_FILE=true会把所有交易操作写进log.txt,方便你改算法时查轨迹。 资金提取是这套测试的关键增补。WDR_ENABLE开关、WDR_PERIOD周期(天到年对应-2到12)、WDR_VALUE额度、WDR_TYPE计算方式,四个量定义了提款行为。下面的TimeOfWithDrawal()里,固定模式直接取WDR_VALUE,百分比模式按余额的0.01*WDR_VALUE抽取,并累加wdr_count与wdr_summa供报告使用。 优化模式下要声明OPT_PARAM并接管OnTester()。返回值决定遗传/穷举优化的评判标准:选opt_total_wdr就追提取总额最大,选跌幅类则追回撤最小。外汇与贵金属杠杆高,回测好看不等于实盘能复制,开MT5跑一遍才有感觉。
class="kw">input class="type">bool WDR_ENABLE = true; class=class="str">"cmt">// 允许提款 enum wdr_period { days = -class="num">2, class=class="str">"cmt">// 天 weeks = -class="num">1, class=class="str">"cmt">// 周 months = class="num">1, class=class="str">"cmt">// 月 quarters = class="num">3, class=class="str">"cmt">// 季度 halfyears = class="num">6, class=class="str">"cmt">// 半年 years = class="num">12 class=class="str">"cmt">// 年 }; class="kw">input wdr_period WDR_PERIOD = weeks; class=class="str">"cmt">// 提款周期 class="kw">input class="type">class="kw">double WDR_VALUE = class="num">1; class=class="str">"cmt">// 提款金额大小 enum lot_type { fixed, class=class="str">"cmt">// 固定的 percent class=class="str">"cmt">// 存款比例 }; class="kw">input lot_type WDR_TYPE = percent; class=class="str">"cmt">// 计算提款数量的方法 class=class="str">"cmt">//--- 从账户中提取资产的函数 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool TimeOfWithDrawal() class=class="str">"cmt">//+------------------------------------------------------------------+ { if(!WDR_ENABLE) class="kw">return(false); class=class="str">"cmt">// 如果提款被禁止,退出 if( tick.time > dt_debit + days_delay * DAY) class=class="str">"cmt">// 指定周期的定期提款 { dt_debit = dt_debit + days_delay * DAY; days_delay = Calc_Delay();class=class="str">"cmt">// 更新周期值 - 两次提款操作之间的天数 if(WDR_TYPE == fixed) wdr_value = WDR_VALUE; else wdr_value = AccountInfoDouble(ACCOUNT_BALANCE) * class="num">0.01 * WDR_VALUE; if(TesterWithdrawal(wdr_value)) { wdr_count++; wdr_summa = wdr_summa + wdr_value; class="kw">return(true); } } class="kw">return(false); } enum opt_value { opt_total_wdr, class=class="str">"cmt">// 取款总量 opt_edd_with_wdr, class=class="str">"cmt">// 考虑提款的跌幅 opt_edd_without_wdr class=class="str">"cmt">// 不考虑提款的跌幅 }; class="kw">input opt_value OPT_PARAM = opt_total_wdr; class=class="str">"cmt">// 优化参数 class=class="str">"cmt">//--- 显示测试信息 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester(class="type">void) class=class="str">"cmt">//+------------------------------------------------------------------+ { class=class="str">"cmt">//--- 为结果报告计算参数 CalculateSummary(initial_deposit); CalcEquityDrawdown(initial_deposit, true, false); class=class="str">"cmt">//--- 创建报告 GenerateReportFile("report.txt"); class=class="str">"cmt">//--- 返回优化准则 if (OPT_PARAM == opt_total_wdr) class="kw">return(wdr_summa); else class="kw">return(RelEquityDrawdownPercent); }
◍ 先关提取跑一遍基准
要验证资金提取对回撤的真实影响,第一步必须拿到没有提取干扰的基准数据。在 MT5 策略测试器里加载 EA 后,把输入参数中的 "Allow withdrawal" 设为 false,如图 5 所示,这样测试期间账户不会被抽走任何资金。 跑完一轮 6 个月测试,重点看资产净值的相对减少(Relative Drawdown)。本例里禁用提取时该值是 4.75%,这就是后续对比的锚点。图 6 的余额曲线和图 7 的结果表都能直接复核对这个数字。 有了基准再开提取才有意义。我们在同一 EA 上启用优化并打开 withdrawal,分别设每日、每周不同额度重跑,结果见图 8 和图 9。反常的是:每天或每周周期性提取时,资产净值相对减少反而比完全不提取的 4.75% 更小。 每日提取那根线更诡异——相对减少一度飙到 100%,随后回落到 8% 再往下走。要解释这种跳动,得先弄清策略测试器算相对减少的公式逻辑,下节拆。
净值回撤在 MT5 里到底怎么算
不想被策略测试器报告牵着鼻子走,就得搞清楚资产净值减少(回撤)在 MT5 里是怎么跑出来的。若 EA 没调用 TesterWithdrawal(),回撤算法和 MT4 基本一致:测试器实时记录净值峰值与谷值,每出现一个新峰值(图里蓝钩),就重算一次最大回撤与相对回撤,旧峰值保持到报告输出。 一旦用了 TesterWithdrawal() 提款,算法就变了——不光新峰值要重算,每次提款(绿钩)也强制重算。实测中每日提取超 1000$ 时,图表极值被频繁打断,相对回撤可能趋近零,看起来比「不考虑提款」的回撤低一大截,但这只是算法假象。 要把这套逻辑搬进自己的 EA,核心就是下面这段 CalcEquityDrawdown。主循环里每次有新数据带 initial_deposit 调它(finally=false),EA 结束在 OnTester() 里再带 finally=true 调一次;有提款动作时也必须带 finally=true 调,才能锁住极值。 想剥离提款对净值的干扰,加个 wdr_ignore 开关:为真时 equity 用 ACCOUNT_EQUITY 加回 wdr_summa。这样算出的回撤不依提款额浮动,但报告里的总盈利/总损失得手工修——总净盈利减 Withdrawal、总损失加回去,否则测试器数值是错的。 验证办法很直接:让 OnTester() 返回 RelEquityDrawdownPercent,和测试器自带「考虑提款的回撤」对比,图 12 那种吻合度就能确认算法没写歪。外汇与贵金属杠杆高,回撤算法再准也只是历史概率,实盘仍可能走更深的坑。
class="type">class="kw">double RelEquityDrawdownPercent; class=class="str">"cmt">//以百分比形式计算的相对跌幅 class="type">class="kw">double MaxEquityDrawdown; class=class="str">"cmt">// 净值最大跌幅 class=class="str">"cmt">//--- 计算净值的跌幅 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CalcEquityDrawdown(class="type">class="kw">double initial_deposit, class=class="str">"cmt">// 初始存款 class="type">bool finally) class=class="str">"cmt">// 计算极值的标志 class=class="str">"cmt">//+------------------------------------------------------------------+ { class="type">class="kw">double drawdownpercent; class="type">class="kw">double drawdown; class="type">class="kw">double equity; class="kw">static class="type">class="kw">double maxpeak = class="num">0.0, minpeak = class="num">0.0; class=class="str">"cmt">//--- 计算跌幅时不包括提取的利润 if(wdr_ignore) equity = AccountInfoDouble(ACCOUNT_EQUITY) + wdr_summa; else equity = AccountInfoDouble(ACCOUNT_EQUITY); if(maxpeak == class="num">0.0) maxpeak = equity; if(minpeak == class="num">0.0) minpeak = equity; class=class="str">"cmt">//--- 检查极值条件 if((maxpeak < equity)||(finally)) { class=class="str">"cmt">//---计算跌幅 drawdown = maxpeak - minpeak; drawdownpercent = drawdown / maxpeak * class="num">100.0; class=class="str">"cmt">//--- 保存跌幅的最大值 if(MaxEquityDrawdown < drawdown) MaxEquityDrawdown = drawdown; if(RelEquityDrawdownPercent < drawdownpercent) RelEquityDrawdownPercent = drawdownpercent; class=class="str">"cmt">//--- 清除极值 maxpeak = equity; minpeak = equity; } if(minpeak > equity) minpeak = equity; } CalcEquityDrawdown(initial_deposit, false); CalcEquityDrawdown(initial_deposit, true); class=class="str">"cmt">//--- 提取资产并计算净值的跌幅 if(TimeOfWithDrawal()) CalcEquityDrawdown(initial_deposit, true); else CalcEquityDrawdown(initial_deposit, false); class="type">bool wdr_ignore; class=class="str">"cmt">// 计算价格而不考虑提款 if (wdr_ignore) equity = AccountInfoDouble(ACCOUNT_EQUITY) + wdr_summa;
「从成交历史里抠出盈利因子」
想在 MT5 里自己算策略的盈利因子,核心是把账户历史成交逐笔扒出来,而不是只看终端面板那个汇总值。下面这段逻辑直接遍历 HistoryDealsTotal 返回的成交数,把每笔 DEAL_PROFIT、DEAL_COMMISSION、DEAL_SWAP 加总,才是真实落袋利润。 外汇与贵金属杠杆品种回测含滑点及隔夜息,实际 GrossLoss 可能比面板显示放大 5%~15%,属高风险验证项,结论仅代表历史样本概率倾向。 循环里第 0 笔若是 DEAL_TYPE_BALANCE 直接跳过,避免初始入金污染收益率;非买卖成交(如出金)受 wdr_ignore 开关控制是否计入。GrossLoss 在循环结束后取绝对值,再用 GrossProfit / GrossLoss 得到 ProfitFactor,若分母为 0 则因子不计算。 报告文件用 FileOpen 以 FILE_TXT|FILE_ANSI 打开,StringInit 填 65 个 '-' 做分隔线,TimeToString 首 tick 日期拼进表头,跑完你能在文件里直接看到和终端不同的净利润结构。
class="type">class="kw">double SummaryProfit; class=class="str">"cmt">// 所有的净利润 class="type">class="kw">double GrossProfit; class=class="str">"cmt">// 毛利 class="type">class="kw">double GrossLoss; class=class="str">"cmt">// 毛损 class="type">class="kw">double ProfitFactor; class=class="str">"cmt">// 盈利因子 class=class="str">"cmt">//--- 为结果报告计算参数 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CalculateSummary(class="type">class="kw">double initial_deposit) class=class="str">"cmt">//+------------------------------------------------------------------+ { class="type">class="kw">double drawdownpercent, drawdown; class="type">class="kw">double maxpeak = initial_deposit, minpeak = initial_deposit, balance = initial_deposit; class="type">class="kw">double profit = class="num">0.0; class=class="str">"cmt">//--- 选择整个历史 HistorySelect(class="num">0, TimeCurrent()); class="type">int trades_total = HistoryDealsTotal(); class=class="str">"cmt">//--- 在历史中查找成交 for(class="type">int i=class="num">0; i < trades_total; i++) { class="type">long ticket = HistoryDealGetTicket(i); class="type">long type = HistoryDealGetInteger(ticket, DEAL_TYPE); class=class="str">"cmt">//--- 不考虑初始存款 if((i == class="num">0)&&(type == DEAL_TYPE_BALANCE)) class="kw">continue; class=class="str">"cmt">//--- 计算利润 profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + HistoryDealGetDouble(ticket, DEAL_COMMISSION) + HistoryDealGetDouble(ticket, DEAL_SWAP); balance += profit; if(minpeak > balance) minpeak = balance; class=class="str">"cmt">//--- if((!wdr_ignore)&&(type != DEAL_TYPE_BUY)&&(type != DEAL_TYPE_SELL)) class="kw">continue; class=class="str">"cmt">//--- if(profit < class="num">0) GrossLoss += profit; else GrossProfit += profit; SummaryProfit += profit; } if(GrossLoss < class="num">0.0) GrossLoss *= -class="num">1.0; class=class="str">"cmt">//--- 盈利率 if(GrossLoss > class="num">0.0) ProfitFactor = GrossProfit / GrossLoss; } class=class="str">"cmt">//--- 生成报告 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GenerateReportFile(class="type">class="kw">string filename) class=class="str">"cmt">//+------------------------------------------------------------------+ { class="type">class="kw">string str, msg; ResetLastError(); hReportFile = FileOpen(filename, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI); if(hReportFile != INVALID_HANDLE) { StringInit(str,class="num">65,&class="macro">#x27;-&class="macro">#x27;); class=class="str">"cmt">// 分隔符 WriteToReportFile(str); WriteToReportFile("| Period of testing: " + TimeToString(first_tick.time, TIME_DATE) + " - " +