使用TesterWithdrawal() 函数模拟利润提取·进阶篇
💸

使用TesterWithdrawal() 函数模拟利润提取·进阶篇

(2/3)· 多数回测只盯着曲线上扬,却从没问过账户真被抽走生活费后系统还扛不扛得住

含代码示例 第 2/3 篇
回测里净值翻倍不代表你能落袋。把利润一直滚在账户里测试,等于默认自己从不为生活开销取款,现实里很少成立。用 TesterWithdrawal() 在策略测试里模拟提款,才能看清资金抽离后系统的真实韧性。

「给通道突破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跑一遍才有感觉。

MQL5 / C++
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 那种吻合度就能确认算法没写歪。外汇与贵金属杠杆高,回撤算法再准也只是历史概率,实盘仍可能走更深的坑。

MQL5 / C++
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 日期拼进表头,跑完你能在文件里直接看到和终端不同的净利润结构。

MQL5 / C++
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) + " - " +
交给小布盯盘看回测缺口
这些提款模拟的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到哪些 EA 在满额再投资下其实经不起抽资。把重复劳动交给小布,你专注决策。

常见问题

不会。该函数仅在策略测试器中模拟从账户提取资金,正常模式下运行的 EA 不受影响,返回值只反映测试内的操作成败。
提取不会执行,Journal 选项卡会出现未成功条目,余额与净值均不减少,报告中的 Withdrawal 费用也不计入该笔。
调用后当前余额和净值按提取额下降,但测试器计算盈利亏损时不扣除提款,只在 Withdrawal 项汇总取款总额,需自行对照解读。
小布盯盘内置的 AIGC 诊断可读取这类提款模拟的回测报告,标记出资金抽离后的回撤异动,不用你手动逐条比对 Journal。
目前没有,也大概率不会提供。设计逻辑是避免鼓励依赖不断存款来维持的系统,回测应聚焦既有资金的真实表现。