外汇EA编写教程:使用TesterWithdrawal() 函数模拟利润提取

简介

投机交易的目标就是要获利。 通常来讲,交易系统在接受测试时,方方面面都要进行分析,只有一项除外:从赚得的款项中提取一部分用于生活。即便对于交易者而言,交易并非唯一的资金来源,有关某交易周期(月、季度、年)计划利润的问题迟早都会出现。MQL5的 TesterWithdrawal() 函数就专为模仿从账户提款而设计。

1. 我们可以检查什么?

关于交易和交易系统,有一些暗示在有风险的状况下提取资金过量的意见。 总资本中只有一小部分在交易账户;如此可避免某种不可预见的环境导致损失所有资金。

而事实上,它也是“资金管理”策略的一个例子。 接受管理投资的许多资金都会为每位交易者提供一个特定的限额(总资本的一部分),但从不会将全部资本提供给交易者。 换句话说,除了对于已建仓位数量的限制外,对交易者可控制的资本亦有限制。 最后,每位交易者只能动用一小部分资产进行风险投机。

测试期间赢得资产的进取性再投资又是一个问题。 优化期间,交易者的目标是让余额与资产净值直线上升。 虽然说每个人都知道,每个资金量都有特定的规则,但是,交易 100 手时相信 0.1 手会盈利是没什么意义的。

此外还有个心理问题 – 每位经验丰富的交易者都承认自己会在账户某个特定资金量处介入交易系统的运转,尽管他们曾经承诺不这样做。 以此看来,提取过量资金是交易中的一项求稳因素。

MQL5 中有一个函数专为实现“EA 交易”测试期间执行资金的提取而设计。 下文将专门讲解。

2. 如何使用此函数?

TesterWithdrawal() 专为模拟从策略测试程序中的账户提取资金而设计,不会影响到正常模式下“EA 交易”的工作。

bool TesterWithdrawal(double money);

输入参数 money 用于指定要从存入货币中提取的金额。 您可以通过返回值查看操作是否成功。 测试期间执行的每次提取操作,都通过 “Journal” 选项卡以及 “Results” 选项卡的对应条目显示出来,如图 1所示。

测试期间成功提取资产的相关条目

 图 1. 测试期间成功提取资产的相关条目

如果提款额超过了可用预付款额度,则提取不会执行,且 “Journal” 中会出现下述条目:

 测试期间提取操作未成功的相关条目

图 2. 测试期间提取操作未成功的相关条目

调用此函数时,当前的余额净值额度会根据提取款项的额度减少。 但是,策略测试程序在计算盈利和损失时并不考虑该提款,而会计算 “Withdrawal” 费用中的取款总额,如图 3 所示。

报告中的取款总额

图 3. 报告中的取款总额

请注意: MQL5 中尚且没有将资产 存入到某交易账户的逆函数,而且很可能实现不了。 谁需要那些要求存款不断增长的系统?

下面将专门讲解一个“EA 交易”示例,它不仅带有已实现的交易策略,还带有利用我们所研究的函数模拟资金提取的工具包。

3. 豚鼠测试

  
我们会就一个相当简单的“EA 交易”实施测试。 如果您对“EA 交易”的操作原则感兴趣,则可在下面对应的交易系统描述中看到。

价格的最大与最小值均以 5M 数据的指定期限进行计算,而该期限可利用 PERIOD 输入变量进行设置。 计算的水平会构成一个带支撑线(CalcLow变量) 与阻力线 (CalcHigh 变量) 的水平通道。 挂单被置于该通道的边界。 游戏就突破该通道而展开。

如果价格进入通道且不小于 INSIDE_LEVEL 输入变量的值,则下达一个挂单。 此外,一个方向只能建一个仓或挂单。

如果重新计算水平时通道变窄,且价格仍处其中,则挂单移动到更接近市场价格的位置。 移动挂单的步骤利用 ORDER_STEP 输入变量进行设置。

一宗交易的利润大小在 TAKE_PROFIT 输入变量中指定,而最大损失则利用 STOP_LOSS 指定。

“EA 交易”的意思是利用 TRAILING_STOP 输入变量中指定的一个步骤,来移动止损。

您既可以用一个固定手数,也可以用作为存款某个百分比计算的手数,来进行交易。 手数值用 LOT 输入变量设置,而手数计算方法则用 LOT_TYPE 变量设置。 有可能出现纠正手数的情况 (LOT_CORRECTION = true)。 如果您用固定手数交易,且该手数超过了建仓允许量,则手数会被纠正为最接近的允许值。

如欲调整“EA 交易”的算法,您可以打开编写日志函数 (WRITE_LOG_FILE = true)。 这样一来,与所有交易操作有关的条目都会被写入文本文件 log.txt。

我们向“EA 交易”添加几个输入变量,以供管理资金提取使用。

第一个参数将用作提取资金可能性的一个标志。

input bool WDR_ENABLE = true; // 允许提款

第二个参数则会确定提取的周期性。

enum   wdr_period
  {
   days      = -2, // 天
   weeks     = -1, // 周 
   months    =  1, // 月  
   quarters  =  3, // 季度
   halfyears =  6, // 半年    
   years     = 12  // 年    
  };
input wdr_period WDR_PERIOD = weeks; // 提款周期

 第三个参数会设置待提取的资金额度。

input double WDR_VALUE = 1; // 提款金额大小

第四个参数则会确定待提取额度的计算方法。

enum lot_type 
   {
      fixed,  // 固定的
      percent // 存款比例
   };
input lot_type WDR_TYPE = percent; // 计算提款数量的方法

想要计算从账户一次性提取资金的额度,则使用 wdr_valuewdr_summa 变量用于计算待提取的资金总额。

还有,我们会用 wdr_count 变量计算成功提取操作的总次数。 在形成我们有关测试结果的报告中,上述变量的值都必不可少。 提取资金的整体功能,都在下述函数中实现。

//--- 从账户中提取资产的函数
//+------------------------------------------------------------------+
bool TimeOfWithDrawal()
//+------------------------------------------------------------------+
  {
   if(!WDR_ENABLE) return(false); // 如果提款被禁止,退出
   
   if( tick.time > dt_debit + days_delay * DAY) // 指定周期的定期提款
     {
      dt_debit = dt_debit + days_delay * DAY;
      days_delay = Calc_Delay();// 更新周期值 - 两次提款操作之间的天数
      
      if(WDR_TYPE == fixed) wdr_value = WDR_VALUE;
      else wdr_value = AccountInfoDouble(ACCOUNT_BALANCE) * 0.01 * WDR_VALUE;
      if(TesterWithdrawal(wdr_value))
        {
         wdr_count++;
         wdr_summa = wdr_summa + wdr_value;
         return(true);
        }
     }
   return(false);
  }

现在,我们需要准备好自己的“EA 交易”以供优化模式下的测试。 会有多种优化参数,所以,我们来声明一个输入变量(将为其选择必要参数)。

enum opt_value
{
   opt_total_wdr,      // 取款总量
   opt_edd_with_wdr,   // 考虑提款的跌幅
   opt_edd_without_wdr // 不考虑提款的跌幅
};

input opt_value OPT_PARAM = opt_total_wdr; // 优化参数

而且,我们需要向“EA 交易”再添加一个重要函数 – OnTester() – 该函数的返回值会决定优化标准。

//--- 显示测试信息
//+------------------------------------------------------------------+
double OnTester(void)
//+------------------------------------------------------------------+
  {
   //--- 为结果报告计算参数
   CalculateSummary(initial_deposit);
   CalcEquityDrawdown(initial_deposit, true, false);
   //--- 创建报告
   GenerateReportFile("report.txt");
   //--- 返回优化准则
   if (OPT_PARAM == opt_total_wdr) return(wdr_summa);
   else return(RelEquityDrawdownPercent);
  }

接下来,我们专门讲解对于“EA 交易”测试的详细考量。

4. 测试 EA 交易程序

出于测试完整性考虑,我们首先要在禁用提取功能的前提下,获取“EA 交易”的测试结果。 为此,在测试之前,如图 5 所示,将 “Allow withdrawal” (允许提取)参数设置为 false

如果您对重复测试的结果感兴趣,下面的图 4 和图 5 中给出了“EA 交易”输入参数的设置和相关值。

“EA 交易”测试的设置

图 4. “EA 交易”测试的设置

 已禁用提取功能的“EA 交易”参数

图 5. 已禁用提取功能的“EA 交易”参数

测试结束后得到的结果,如图 6 和图 7 所示。

 “EA 交易”试运行 6 个月的余额变动

  图 6. “EA 交易”试运行 6 个月的余额变动  

 “EA 交易”运行结果表

图 7. “EA 交易”运行结果表

对于该参数,我们感兴趣的是资产净值的相对减少。 本例中是 4.75%。

现在我们来看看,如果启用资金提取,该参数会如何变化。 我们采用不同的提取额度和周期性来执行测试。

图 8 中呈现的是启用优化与提取的“EA 交易”参数,而图 9 显示的则是该测试的结果。

已启用优化与提取的“EA 交易”参数

 图 8. 已启用优化与提取的“EA 交易”参数

资产净值相对减少的计算结果

图 9. 资产净值相对减少的计算结果

测试的结果有点出乎意料,因为与完全禁用提取相比,每天或每周的周期性提取的资产减少竟然更小。

此外,每日减少的线飙升到 100%,然后又返回到 8%,并继续下降。 想要正确解读结果,您需要了解策略测试程序中的资产净值相对减少是怎样计算出来的。 下文就是相关内容。

5. 资产净值减少的计算

爱好钻研的人会很想知道,策略测试程序中如何执行资产净值计算?自己又该怎样计算此参数呢?

如果您的“EA交易”中未采用TesterWithdrawal()函数,则我们所分析参数的计算与 MetaTrader 4 中的相应计算并无不同;请参阅 《专家测试报告中的数字有何含义》一文中的相关描述;而源代码以及作为结果的测试程序报告的许多其它参数,请见《如何评估专家测试结果》一文。

下面的图 10,是策略测试程序中相对与最大减少的计算的一种形象化描述。

未考虑提取情况下的资产净值减少的计算 

图 10. 未考虑提取情况下的资产净值减少的计算

在其运行期间,策略测试程序会确定当前资产净值的最大与最小值。 随着一个新的资产最大净值的出现(图表上以蓝色钩形符号标示),减少的最大与最小值会被重新计算,而且最大值会被保持,以供作为结果的报告显示。

测试结束时,参数的最后一次重新计算非常重要,因为如果资产净值的最后一个未注册极值给出减少的最大值,就会导致状况发生。 减少最大值的变化,已分别用蓝色和红色呈现。 灰色则代表每有新的最大值出现时,已注册的减少。

要注意的是,调用 TesterWithDrawal() 函数会改变策略测试程序中减少计算的算法。 与前一变量的区别在于,不仅仅是出现新的资产净值最大值时要重新计算减少值,资产被提取时也是一样。 图 11 则是其形象化的展示。

考虑到提取的减少计算 

 图 11. 考虑到提取的减少计算

上图中的绿色钩形符号指的是提取资产的时间。 资金提取非常频繁,所以根据图表上的极值确定,不允许减少最大值,有待修复。 结果是,正如图 9 中所示,考虑到提取的减少,低于未考虑提取的减少。

如果提取资金远远多于获利所导致的资产净值增长,即可造成低减少率。 原因就是这种情况不允许图表上形成极值;而且极端情况下,它会显示出一条垂直下滑线,相对减少会趋近于零。 如图 9 中的图表所示,随着每日提取超过 1000$,这种效果就开始出现。

我们将整体算法结合到单一过程 CalcEquityDrawdown 中,通过这种方式实现 MQL5 中最大与相对减少的计算的复制,如下所述。

double RelEquityDrawdownPercent; //以百分比形式计算的相对跌幅
double MaxEquityDrawdown;        // 净值最大跌幅
//--- 计算净值的跌幅
//+------------------------------------------------------------------+
void CalcEquityDrawdown(double initial_deposit, // 初始存款
                        bool finally)          // 计算极值的标志
//+------------------------------------------------------------------+
  {
   double drawdownpercent;
   double drawdown;
   double equity;
   static double maxpeak = 0.0, minpeak = 0.0;
   //--- 计算跌幅时不包括提取的利润
   if(wdr_ignore) equity = AccountInfoDouble(ACCOUNT_EQUITY) + wdr_summa;
   else equity = AccountInfoDouble(ACCOUNT_EQUITY);
   if(maxpeak == 0.0) maxpeak = equity;
   if(minpeak == 0.0) minpeak = equity;
   //--- 检查极值条件
   if((maxpeak < equity)||(finally))
    {
      //---计算跌幅
      drawdown = maxpeak - minpeak;
      drawdownpercent = drawdown / maxpeak * 100.0;
      //--- 保存跌幅的最大值
      if(MaxEquityDrawdown < drawdown) MaxEquityDrawdown = drawdown;
      if(RelEquityDrawdownPercent < drawdownpercent) RelEquityDrawdownPercent = drawdownpercent;
    
      //--- 清除极值
      maxpeak = equity;
      minpeak = equity;
    
}
   if(minpeak > equity) minpeak = equity;
 
}

为保证计算精确,每当有新的数据出现、且带有下面给定的参数时,我们都需要从“EA 交易”的主体中调用上述过程。

CalcEquityDrawdown(initial_deposit, false);

此外,想获得可靠的减少值,还应在“EA 交易”结束时在 OnTester() 函数中调用它,且带有表明该计算已完成的 finally = true 参数。 还有,如果当前的不固定减少大于注册的最大值,则其会替代最大值,并显示于作为结果的报告中。

CalcEquityDrawdown(initial_deposit, true);

如果提取算法已在“EA 交易”中实施,那么,为了正确计算亏损,每当有提取操作时,您都需要调用带有 finally=true 参数的 CalcEquityDrawdown 函数。 可按如下顺序调用:

//--- 提取资产并计算净值的跌幅
if(TimeOfWithDrawal())
    CalcEquityDrawdown(initial_deposit, true);
else 
    CalcEquityDrawdown(initial_deposit, false);

为确保上述方法的正确性,我们将算得的数据与策略测试程序的数据做一下对比。 为此,我们需要让 OnTester() 函数返回我们想要核对的参数值 - 存储于 RelEquityDrawdownPercent 变量中的资产净值的相对减少。

它是通过设置 “Optimization by the parameter” (按参数优化)输入参数中的 “Drawdown with consideration of withdrawal” (已考虑提取的减少)值,在“EA 交易”代码中完成实施。 如图 8 所示,其余参数应保留不变。而该测试的结果如图 12 所示。

 我们的计算结果与策略测试程序数据的对比

 图 12. 我们的计算结果与策略测试程序数据的对比

通过获取结果的对比,证明了该相对减少的算法的正确性。

我们再向减少计算添加一个变量。 我们要利用它来去除资金提取对于资产净值的影响,并观测资产净值相对减少的变化。

为此,我们向“EA 交易”添加一个变量,并用它来确定计算“EA 交易”参数时是否有必要考虑提取。

bool wdr_ignore; // 计算价格而不考虑提款

如果 “Optimization by the parameter” 变量被设置为 “Drawdown without consideration of withdrawal”,则 wdr_ignore 值等于 true

此外,您还需要通过向其添加该参数的处理来纠正 CalcEquityDrawdown 减少计算的过程,如下所示。

if (wdr_ignore) 
  equity = AccountInfoDouble(ACCOUNT_EQUITY) + wdr_summa;
else 
  equity = AccountInfoDouble(ACCOUNT_EQUITY);

现在,减少新值的获取已经万事俱备了。 启用计算的新算法,我们一起来执行测试吧。 图 13 中所示为测试结果。

不考虑提取情况下的减少的计算结果 

图 13. 不考虑提取情况下的减少的计算结果

结果显示:无论是提取资产的事实,还是提取资金的额度,都不依减少而定。 由此,我们得到了一个不受 TesterWithDrawal() 函数影响的纯粹因素。 但是,为使用此类计算,我们需要纠正盈利与损失值,因为它们的实际值已有更改,而且作为结果的测试程序报告中的这些因素也不正确。

我们来计算盈利、损失及其比率(盈利能力),并将获取值保存为文本文件格式的报告。 所列参数的计算过程如下。

double SummaryProfit;     // 所有的净利润
double GrossProfit;       // 毛利
double GrossLoss;         // 毛损
double ProfitFactor;      // 盈利因子

//--- 为结果报告计算参数
//+------------------------------------------------------------------+
void CalculateSummary(double initial_deposit)
//+------------------------------------------------------------------+
  {
   double drawdownpercent, drawdown;
   double maxpeak = initial_deposit, 
          minpeak = initial_deposit, 
          balance = initial_deposit;
          
   double profit = 0.0;
   
   //--- 选择整个历史
   HistorySelect(0, TimeCurrent());
   int trades_total = HistoryDealsTotal();
   //--- 在历史中查找成交
   for(int i=0; i < trades_total; i++)
     {
      long ticket = HistoryDealGetTicket(i);
      long type   = HistoryDealGetInteger(ticket, DEAL_TYPE);
      //--- 不考虑初始存款
      if((i == 0)&&(type == DEAL_TYPE_BALANCE)) continue;
      //--- 计算利润
      profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) +
                 HistoryDealGetDouble(ticket, DEAL_COMMISSION) +
               HistoryDealGetDouble(ticket, DEAL_SWAP);
      
      balance += profit;
      if(minpeak > balance) minpeak = balance;
      //---
      if((!wdr_ignore)&&(type != DEAL_TYPE_BUY)&&(type != DEAL_TYPE_SELL)) continue;
      //---
      if(profit < 0) GrossLoss   += profit;
      else           GrossProfit += profit;
      SummaryProfit += profit;
     }
   if(GrossLoss < 0.0) GrossLoss *= -1.0;
   //--- 盈利率
   if(GrossLoss > 0.0) ProfitFactor = GrossProfit / GrossLoss;
  }

生成文本文件格式报告的函数如下。

//--- 生成报告  
//+------------------------------------------------------------------+
void GenerateReportFile(string filename)
//+------------------------------------------------------------------+
  {
   string str, msg;
   ResetLastError();
   hReportFile = FileOpen(filename, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(hReportFile != INVALID_HANDLE)
     {
      StringInit(str,65,'-'); // 分隔符
      WriteToReportFile(str);
      WriteToReportFile("| Period of testing: " + TimeToString(first_tick.time, TIME_DATE) + " - " +
                        TimeToString(tick.time,TIME_DATE) + "/t/t/t|");
      WriteToReportFile(str);
      //----
      WriteToReportFile("| Initial deposit /t/t/t"+DoubleToString(initial_deposit, 2));
      WriteToReportFile("| Total net profit    /t/t/t"+DoubleToString(SummaryProfit, 2));
      WriteToReportFile("| Gross profit     /t/t/t"+DoubleToString(GrossProfit, 2));
      WriteToReportFile("| Gross loss      /t/t/t"+DoubleToString(-GrossLoss, 2));
      if(GrossLoss > 0.0)
         WriteToReportFile("| Profitability       /t/t/t"+DoubleToString(ProfitFactor,2));
      WriteToReportFile("| Relative drawdown of equity /t"+
                        StringFormat("%1.2f%% (%1.2f)", RelEquityDrawdownPercent, MaxEquityDrawdown));
      if(WDR_ENABLE)
        {
         StringInit(msg, 10, 0);
         switch(WDR_PERIOD)
           {
            case day:     msg = "day";    break;
            case week:    msg = "week";  break;
            case month:   msg = "month";   break;
            case quarter: msg = "quarter"; break;
            case year:    msg = "year";     break;
           }
         WriteToReportFile(str);
         WriteToReportFile("| Periodicity of withdrawing       /t/t" + msg);
         if(WDR_TYPE == fixed) msg = DoubleToString(WDR_VALUE, 2);
         else msg = DoubleToString(WDR_VALUE, 1) + " % from deposit " + DoubleToString(initial_deposit, 2);
         WriteToReportFile("| Amount of money withdrawn     /t/t" + msg);
         WriteToReportFile("| Number of withdrawal operations /t/t" + IntegerToString(wdr_count));
         WriteToReportFile("| Withdrawn from account          /t/t" + DoubleToString(wdr_summa, 2));
        }
      WriteToReportFile(str);
      WriteToReportFile(" ");
      FileClose(hReportFile);
     }
  }

此函数的执行结果,就是创建一份填有图 14 中所示信息的文本文件。

 通过 GenerateReportFile 过程生成的报告文件

 图 14. 通过 GenerateReportFile 过程生成的报告文件

此函数的编写原旨,就是将每份新报告,添加到文件的现有内容中。 有鉴于此,我们可以将测试结果值与“EA 交易”的不同输入参数进行对比。

从报告中您可以看出,不考虑提取情况下的计算(下表),与测试程序的计算值(上表)相比,总净盈利值较小,总损失因提取资金额度而较大。 要注意的是,如果从策略测试程序中提取资产,要获取真实的盈利和损失值,您要在总净盈利的基础上减去 “Withdrawal” 参数的值,再将其加到总损失。

6. 结果分析

根据从所有已执行试验获取到的结果,我们可以得出多个结论:

  1. 使用 TesterWithDrawal() 函数会导致策略测试程序中减少的计算的算法变更。 如果多个不同的“EA 交易”中的某个包含提取资金机制,则按相对减少值进行对比可能就不正确。 利用此函数,您可以实际计算一下,根据指定且可接受的资产净值减少百分比,自己可以从账户周期性地提取多少资金。
  2. 此函数的使用可作为交易的一种综合不稳定因素实现,用于检查您“EA 交易”工作的稳定性,并对负责资金管理的代码逻辑进行调整。 如果您的“EA 交易”具备根据余额或资产净值水平做出决策的逻辑,则使用此函数会赋予您测试和调整的更多机会。
  3. 不考虑提取情况下重新计算相对减少时,利用文中所述的算法,您就可以得到不会受到此函数使用影响的相对减少的一个纯值。

总结

本文讲述了使用 TesterWithdrawal() 函数模拟从某账户提取资金的过程,以及其对策略测试程序中资产净值减少的计算算法的影响。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/131

附加的文件 |

testexpert.mq5
(61.54 KB)

 

 


MyFxtop迈投(www.myfxtop.com)-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投