
继续漫步优化(第二部分):为任意机器人创建优化报告的机制
概述
这是致力于创建自动优化器的系列文章中的下一篇,该优化器可以执行交易策略的漫步优化。 上一篇文章描述过如何创建 DLL,并运用在我们的自动优化器和 EA 之中。 这部分新内容则完全致力于 MQL5 语言。 我们将研究优化报告的生成方法,以及在您的算法中该功能的应用。
策略测试器不允许从智能交易系统中访问其数据,而其提供的结果缺乏细节,所以,我们将利用我在之前文章中实现的优化报告下载功能。 由于此功能的各个独立部分均被修改,而其他内容在先前的文章中并未完全涵盖,因此我们再次研究这些功能,因为在我们的程序里它们是关键的构成部分。 我们从新功能之一开始:添加自定义佣金。 本文中描述的所有类和函数都位于 Include/History manager 目录之下。
自定义佣金和滑点的实现
MetaTrader 5 平台测试器提供了许多令人兴奋的可能性。 然而,某些经纪商未将交易佣金添加到历史记录中。 进而,有时您也许要附加佣金以便进行额外的策略测试。 为此目的,我已新加了一个类,可为每个单独的品种保存佣金。 调用相应的方法后,该类将返回佣金和指定的滑点。 该类本身清单如下:
class CCCM { private: struct Keeper { string symbol; double comission; double shift; }; Keeper comission_data[]; public: void add(string symbol,double comission,double shift); double get(string symbol,double price,double volume); void remove(string symbol); };
已为该类创建了 Keeper 结构,该结构存储指定资产的佣金和滑点。 已创建一个数组来存储所有传入的佣金和滑点值。 声明三个方法:添加、接收和删除数据。 资产加入方法实现如下:
void CCCM::add(string symbol,double comission,double shift) { int s=ArraySize(comission_data); for(int i=0;i<s;i++) { if(comission_data[i].symbol==symbol) return; } ArrayResize(comission_data,s+1,s+1); Keeper keeper; keeper.symbol=symbol; keeper.comission=MathAbs(comission); keeper.shift=MathAbs(shift); comission_data[s]=keeper; }
此方法实现了初步检查是否早前已添加同一资产,之后会根据结果向集合中添加新资产。 请注意,滑点和佣金已添加模量化。 因此,当所有成本累加时,数值符号将不会影响计算。 另一点要注意的是计量单位。
- 佣金:根据资产类型,佣金可以按获利货币、或交易量的百分比加收。
- 滑点:始终按点数为单位指定。
还请注意,加收数值并非覆盖每笔完整仓位(即开仓+平仓),而是每次交易(每次开仓或平仓)都要加收。 因此,仓位将含有以下数值:n*佣金 + n*滑点,其中 n 是一笔仓位之中所有成交的次数。
remove 方法删除所选资产。 品种名作为关键字。
void CCCM::remove(string symbol) { int total=ArraySize(comission_data); int ind=-1; for(int i=0;i<total;i++) { if(comission_data[i].symbol==symbol) { ind=i; break; } } if(ind!=-1) ArrayRemove(comission_data,ind,1); }
如果找不到相应的品种,则该方法不会删除任何资产,并终止。
get 方法用于获取所选的偏移值和佣金。 针对不同的资产类型,该方法的实现方式是不同的。
double CCCM::get(string symbol,double price,double volume) { int total=ArraySize(comission_data); for(int i=0;i<total;i++) { if(comission_data[i].symbol==symbol) { ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE); double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE); double ans; switch(mode) { case SYMBOL_CALC_MODE_FOREX : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_FUTURES : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFD : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFDINDEX : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFDLEVERAGE : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_STOCKS : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_FUTURES : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_BONDS : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_SERV_COLLATERAL : ans=(comission_data[i].comission+shift)*volume; break; default: ans=0; break; } if(ans!=0) return -ans; } } return 0; }
在数组中搜索指定的品种。 鉴于针对不同的品种类型要运用不同的佣金计算类型,因此为佣金设置的类型也有所区别。 例如,将股票和债券佣金设置为营业额的百分比,而将计算出的营业额设为手数乘以每手的合约数量和成交价格。
结果就是,我们得到了所执行操作的货币等价值。 该方法的执行结果始终是佣金和滑点的总和(以货币计)。 滑点是根据即时报价值计算的。 进而,所描述的类会在接下来的下载报告类中用到。 每种资产的佣金参数可以是硬编码的,也可以从数据库中自动请求; 亦或,可以将其作为输入传递给 EA。 在我的算法中,我首选后一种方法。
在 CDealHistoryGetter 类里的创新
在本部分里进一步讨论的类,曾在之前的文章中提到过。 这就是为什么我不会深入讨论早前所讨论的类。 然而,我尝试全面论述新类,因为交易报告下载算法中的关键算法是创建所下载的报告。
我们从 CDealHistoryGetter 类开始,自第一篇 文章以来,该类的运用有了一些修改。 第一篇文章主要致力于阐述这个类。 最新版本随附于后。 它包括一些新功能,意即次要的修复。 在第一篇文章中,曾以易于阅读的形式详细论述过下载报告的机制。 在本文中,我们将更详细地研究向报告里添加佣金和滑点。 根据 OOP(面向对象编程)原则,这意味着一个对象必须执行一个特定的指定用途,创建该对象是为了接收所有类型的交易报告结果。 它包含以下公开方法,每个方法都扮演其特定角色:
- getHistory — 此方法允许下载按仓位分组的交易历史记录。 如果我们在循环里用标准方法下载交易历史记录,且不用任何过滤器,则我们会收到按照 DealData 结构呈现的交易描述:
struct DealData { long ticket; // Deal ticket long order; // The number of the order that opened the position datetime DT; // Position open date long DT_msc; // Position open date in milliseconds ENUM_DEAL_TYPE type; // Open position type ENUM_DEAL_ENTRY entry; // Position entry type long magic; // Unique position number ENUM_DEAL_REASON reason; // Order placing reason long ID; // Position ID double volume; // Position volume (lots) double price; // Position entry price double comission; // Commission paid double swap; // Swap double profit; // Profit / loss string symbol; // Symbol string comment; // Comment specified when at opening string ID_external; // External ID };
收到的数据将按开仓时间排序,且不会以其他任何方式分组。 本篇文章包含一些示例,展示出以这种形式读取报告的困难之处,因为若遵照多种算法交易时,交易之间可能会发生混淆。 尤其是当您运用持仓递增技术时,会根据底层算法为资产追加多头或空头仓位。 结果则是,我们得到了大量的入场和出场成交,而这些并不能反映出全面情况。
我们的方法是按仓位对这些成交进行分组。 尽管订单十分混乱,但我们会把不涉及所分析仓位的不必要成交剔除。 结果会按照如上所示的成交结构保存在结构数组当中。
struct DealKeeper { DealData deals[]; /* List of all deals for this position (or several positions in case of position reversal)*/ string symbol; // Symbol long ID; // ID of the position (s) datetime DT_min; // Open date (or the date of the very first position) datetime DT_max; // Close date };
请注意,该类不会在分组中考虑魔幻数字,因为当一笔持仓由两个或多个算法进行交易时,通常会交错出现不同数字。 至少在莫斯科交易所,完全分离从技术层面是不可能的,故而我主要为此编写算法。 此外,该工具设计用于下载的交易结果,或测试/优化结果。 在第一种情况下,所选品种的统计信息就足够了;而在第二种情况下,魔幻数字并不重要,因为策略测试器每次运行只采用一种算法。
自第一篇文章以来,方法核心的实现未发生变化。 如今,我们将自定义佣金加入其内。 为此任务,上面讨论的 CCCM 类会通过引用传递给类构造函数,并将其保存在相应的字段中。 然后,在填充 DealData 结构时,即在填充佣金时,在所传递 CCCM 类中的自定义佣金会被保存。
#ifndef ONLY_CUSTOM_COMISSION if(data.comission==0 && comission_manager != NULL) { data.comission=comission_manager.get(data.symbol,data.price,data.volume); } #else data.comission=comission_manager.get(data.symbol,data.price,data.volume); #endif
所添加的佣金是有针对性的和有条件的。 在机器人中,如果该类之前读取过我们在文件里定义的 ONLY_CUSTOM_COMISSION 参数,则佣金字段将始终包含所传递的佣金,取代经纪商提供的数值。 如果未定义此参数,则将有条件地添加所传递佣金:仅在经纪商未提供该值报价的情况下。 在所有其他情况下,用户佣金值将被忽略。
- getIDArr — 返回在请求的时间范围内为所有交易品种的开仓 ID 数组。 仓位 ID 可以将所有成交合并到我们方法中的仓位。 实际上,这是 DealData.ID 字段的唯一列表。
- getDealsDetales — 该方法类似于 getHistory,但是它提供的细节较少。 该方法的思路是提供一种易于阅读的仓位列表,其中每一行对应一笔特定的成交。 每笔仓位由以下结构描述:
struct DealDetales { string symbol; // Symbol datetime DT_open; // Open date ENUM_DAY_OF_WEEK day_open; // Open day datetime DT_close; // Cloe date ENUM_DAY_OF_WEEK day_close; // Close day double volume; // Volume (lots) bool isLong; // Long/Short double price_in; // Position entry price double price_out; // Position exit price double pl_oneLot; // Profit / loss is trading one lot double pl_forDeal; // Real profit/loss taking into account commission string open_comment; // Comment at the time of opening string close_comment; // Comment at the time of closing };
它们代表按平仓日期排序的仓位表。 这些数值的数组将在接下来的类中用于计算系数。 另外,我们还将得到基于所示数据得最终测试报告。 甚而,基于此类数据,测试器可以在交易后创建盈亏曲线图。
对于测试器,请注意,在进一步的计算中,终端计算出的恢复因子会与根据接收数据计算出的恢复因子有所不同。 这一事实出于尽管数据下载正确,且计算公式相同,但源数据却不同。 测试器使用绿线(即详细报告)计算恢复因子,而我们将使用蓝线(即忽略开仓和平仓之间发生的价格波动数据)进行计算。
- getBalance — 此方法旨在获取余额数据,且不考虑指定日期的交易操作。
double CDealHistoryGetter::getBalance(datetime toDate) { if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent()))) { int total=HistoryDealsTotal(); // Get the total number of positions double balance=0; for(int i=0; i<total; i++) { long ticket=(long)HistoryDealGetTicket(i); ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE); if(dealType==DEAL_TYPE_BALANCE || dealType == DEAL_TYPE_CORRECTION || dealType == DEAL_TYPE_COMMISSION) { balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT); if(toDate<=0) break; } } return balance; } else return 0; }
为了达成任务,首先从最开始的时间段调取至指定时间段的所有成交的历史记录。 之后,在循环里保存余额,同时将所有入金和出金增减到初始余额中,并考虑经纪商提供的佣金和调整。 如果日期传递零值作为输入,则仅从第一个日期开始调取余额。
- getBalanceWithPL — 该方法与前一种方法类似,但它另行考虑了所执行操作的盈亏造成的对余额变化,包括根据上述原则的佣金。
创建优化报告的类 — 计算中使用的结构
先前文章中曾提到的另一个类是 CReportCreator。 在文章100 个最佳优化通测里的“计算部分”章节中对此进行了简要说明。 现在是时候提供更详细的论述了,因为该类计算所有系数,自动优化器将基于这些系数来确定算法参数的组合是否与所请求的标准相符。
首先让我们描述在类实现中使用的方法的基本思想。 我在第一篇文章中曾实现了一个相似的类,只是功能较少。 但它非常慢,因为要计算下一组要求的参数或下一张图表,它必须重新下载所有交易历史,并循环遍历。 这是在每次参数请求时完成的。
有时,如果数据太多,该方法可能需要花费几秒钟。 为了提升计算速度。 我用另一个类来实现,它另外提供了更多数据(包括标准优化结果中未提供的一些数据)。 您可能会注意到,许多系数的计算都需要类似的数据,例如,最大利润/亏损,或累计利润/亏损等。
所以,通过在一个循环中计算系数并将其保存在类的字段中,我们可以将该数据进一步用于需要依赖这些数据进行计算的所有其他参数。 因此,我们得到一个类,该类循环一次遍历下载的历史记录,计算所有必需的参数,并存储数据直至一次计算。 然后,当我们需要获取所需的参数时,该类将复制已保存的数据,而不必重新计算它,从而大大加快了操作速度。
现在我们看看参数如何计算。 我们从存储计算数据的对象开始。 这些被创建的对象,是为私密作用域中声明的嵌套类对象。 这样做出于两个原因。 首先,防止使用此功能的其他类调用它们。 大量已声明的结构和类令人混淆:其中一些是外部计算所必需的,而另一些是技术性的,即用于内部计算。 因此,第二个原因是强调其纯粹的技术目的。
PL_Keeper 结构:
struct PL_keeper
{
PLChart_item PL_total[];
PLChart_item PL_oneLot[];
PLChart_item PL_Indicative[];
};
创建此结构是为了存储所有可能的盈亏图形。 我在第一篇文章里对它们进行了详细阐述(请参见上面的链接)。 在结构声明之下,创建其实例:
PL_keeper PL,PL_hist,BH,BH_hist;
每个实例针对不同源数据存储 4 种表现形式的图表类型。 前缀为 PL 的数据是基于早前提到的终端所提供的盈亏图之蓝线计算的。 前缀为 BH 的数据是基于买入并持有策略获得的盈亏图计算的。 后缀为 'hist' 的数据是基于盈亏直方图计算的。
DailyPL_keeper structure:
// The structure of Daily PL graphs struct DailyPL_keeper { DailyPL avarage_open,avarage_close,absolute_open,absolute_close; };
该结构存储四种可能的每日盈亏图类型。 前缀为 “average” 的 DailyPL 结构实例则基于平均盈亏数据计算的。 前缀为 “absolute” 的数据则使用盈亏总计值。 相应地,它们之间的差别也是显而易见的。 在第一种情况下,它反映了整个交易期间的平均日盈利;在第二种情况下,则展示的是总利润。 前缀为 “open” 的数据依据其开仓日期按天数排序,而前缀为 “close” 的数据依据其平仓日期排序。 下面的代码示意该结构的实例声明。
这是 RationTable_keeper 声明:
// Table structure of extreme points struct RatioTable_keeper { ProfitDrawdown Total_max,Total_absolute,Total_percent; ProfitDrawdown OneLot_max,OneLot_absolute,OneLot_percent; };
此结构由 ProfitDrawdown 结构的若干实例组成。
struct ProfitDrawdown { double Profit; // In some cases Profit, in other Profit / Loss double Drawdown; // Drawdown };
它根据某些标准存储盈亏率。 前缀为 “Total” 的数据是由盈亏图计算的,构建时考虑了手数变化。 前缀为 “OneLot” 的数据在计算时就好像全部时间都在以一手交易。 非标准的单手计算思路曾在前述第一篇文章中论述过。 简言之,创建此方法是为了评估交易系统的结果。 它能够评估大多数结果的来源:及时的手数管理,或系统本身的逻辑。 后缀为 “max” 表示该实例所含数据是交易历史时段中遇到的最高利润+回撤。 后缀为 “absolute” 表示该实例含有整个交易历史里利润与回撤数据的总和。 后缀 ``percent' '表示利润和回撤值是相对于测试时间范围内盈亏曲线上最大值的百分率计算的。 结构声明很简单,并展示在本文所附的代码中。
下一组声明的结构并未作为类字段,而是在主要的 Create 方法中作为局部声明。 所有已描述的结构都组合在一起,如此我们来查看它们的所有声明。
// Structures for calculating consecutive profits and losses struct S_dealsCounter { int Profit,DD; }; struct S_dealsInARow : public S_dealsCounter { S_dealsCounter Counter; }; // Structures for calculating auxiliary data struct CalculationData_item { S_dealsInARow dealsCounter; int R_arr[]; double DD_percent; double Accomulated_DD,Accomulated_Profit; double PL; double Max_DD_forDeal,Max_Profit_forDeal; double Max_DD_byPL,Max_Profit_byPL; datetime DT_Max_DD_byPL,DT_Max_Profit_byPL; datetime DT_Max_DD_forDeal,DT_Max_Profit_forDeal; int Total_DD_numDeals,Total_Profit_numDeals; }; struct CalculationData { CalculationData_item total,oneLot; int num_deals; bool isNot_firstDeal; };
S_dealsCounter 和 S_dealsInARowow 结构本质上是单个实例。 这种关联和继承的奇怪组合与参数的特定计算有关。 创建 S_dealsInARowow 结构用于存储并计算连串数量的交易(实际上是为了计算仓位,即从开仓到平仓),既有盈利也有亏损。 声明的 S_dealsCounter 结构嵌套实例,用于存储中间计算结果。 继承的字段存储总和。 稍后我们将返回到计算盈利/亏损成交的操作。
CalculationData_item 结构包含计算系数所需的字段。
- R_arr — 连续盈利/亏损成交序列的数组,分别显示为 1 / 0。 该数组用于 Z 分数计算。
- DD_percent — 回撤百分比。
- Accomulated_DD, Accomulated_Profit — 存储亏损和利润值总和。
- PL — 盈利 / 亏损。
- Max_DD_forDeal, Max_Profit_forDeal — 顾名思义,它们存储所有成交中的最大亏损和盈利。
- Max_DD_byPL, Mаx_Profit_byPL — 存储由盈亏图计算出的最大亏损和利润。
- DT_Max_DD_byPL, DT_Max_Profit_byPL — 存储由盈亏图得来的最高亏损和最高盈利的日期。
- DT_Max_DD_forDeal, DT_Max_Profit_forDeal — 最高亏损和获利成交的日期。
- Total_DD_numDeals, TotalProfit_numDeals — 获利和亏损交易的总数。
基于以上数据所做的进一步计算。
CalculationData 是一个累加结构,它合并了所有描述过的结构。 它存储所有必需的数据。 它还包含 num_deals 字段,该字段实际上是 CalculationData_item::Total_DD_numDeals 和 CalculationData_item::TotalProfit_numDeals 的总和。 sNot_firstDeal 字段是一个技术标志,它意味着本次计算并非是第一笔成交。
CoefChart_keeper 结构:
struct CoefChart_keeper
{
CoefChart_item OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
CoefChart_item OneLot_WinCoef_chart[],Total_WinCoef_chart[];
CoefChart_item OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
CoefChart_item OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
CoefChart_item OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
};
它旨在存储系数图表。 由于该类不仅创建利润和手数图形,还创建一些系数图形,且还为所述数据类型创建了另一种结构。 前缀 “OneLot” 表明,如果仅交易一手,实例会存储来自盈利/亏损分析的数据。 “Total”意指计算时考虑了手数管理。 如果策略中未使用任何的手数管理,则两个图表将相同。
СHistoryComparer 类:
类似地,定义了要在数据排序时用到的类。 文章 “100 次最佳优化通测” 包含 CGenericSorter 类的论述,该类可以按降序和升序对任何数据类型进行排序。 它另外需要一个能够比较所传递类型的类。 这样的类是 СHisoryComparer。
class CHistoryComparer : public ICustomComparer<DealDetales> { public: int Compare(DealDetales &x,DealDetales &y); };
方法实现很简单:它比较平仓日期,因为已按平仓日期执行了排序:
int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y) { return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close ? 1 : -1)); }
还有一个相似的类,用于对系数图表进行排序。 这两个类和排序器类在所描述的 CReportCreator 类里作为全局字段进行实例化。 除了所描述的对象外,还有两个其他字段。 它们的类型描述为单独非嵌套对象:
PL_detales PL_detales_data; DistributionChart OneLot_PDF_chart,Total_PDF_chart;
PL_detales 结构包含有关获利和亏损仓位的简要交易信息:
//+------------------------------------------------------------------+ struct PL_detales_PLDD { int orders; // Number of deals double orders_in_Percent; // Number of orders as % of total number of orders int dealsInARow; // Deals in a row double totalResult; // Total result in money double averageResult; // Average result in money }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct PL_detales_item { PL_detales_PLDD profit; // Information on profitable deals PL_detales_PLDD drawdown; // Information on losing deals }; //+-------------------------------------------------------------------+ //| A brief PL graph summary divided into 2 main blocks | //+-------------------------------------------------------------------+ struct PL_detales { PL_detales_item total,oneLot; };
第二个结构 DistributionChart 包含一些 VaR 值,以及基于这些系数计算得出的分布图。 分布计算为正态分布。
//+------------------------------------------------------------------+ //| Structure used for saving distribution charts | //+------------------------------------------------------------------+ struct Chart_item { double y; // y axis double x; // x axis }; //+------------------------------------------------------------------+ //| Structure contains the VaR value | //+------------------------------------------------------------------+ struct VAR { double VAR_90,VAR_95,VAR_99; double Mx,Std; }; //+------------------------------------------------------------------+ //| Structure - it is used to store distribution charts and | //| the VaR values | //+------------------------------------------------------------------+ struct Distribution_item { Chart_item distribution[]; // Distribution chart VAR VaR; // VaR }; //+------------------------------------------------------------------+ //| Structure - Stores distribution data. Divided into 2 blocks | //+------------------------------------------------------------------+ struct DistributionChart { Distribution_item absolute,growth; };
VaR 系数是根据以下公式计算得出:历史 VaR,可能不够准确,但是非常适合当前的实现。
计算系数的方法可以描述交易结果
现在我们已研究了数据存储结构,您可以想象由该类所计算出的海量统计信息。 我们逐一查看计算所述值的特定方法,它们均在 CReportCreator 类中被命名。
创建的 CalcPL 用于计算盈亏图。 它的实现如下:
void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type) { PLChart_item item; ZeroMemory(item); item.DT=deal.DT_close; // Saving the date if(type!=_Indicative) { item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Saving the profit item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Saving the drawdown } else // Calculating the indicative chart { if(data.isNot_firstDeal) { if(data.total.PL!=0) { if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0) item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal); else if(data.total.PL<0 && data.total.Max_Profit_forDeal>0) item.Profit=data.total.PL/data.total.Max_Profit_forDeal; } } } // Adding data to array int s=ArraySize(pl_out); ArrayResize(pl_out,s+1,s+1); pl_out[s]=item; }
从实现中可以看出,其所有计算都基于先前描述的结构数据,这些数据作为输入传递。
如果您需要计算非指示性盈亏图,只需复制已知数据即可。 否则,计算将受两个条件的影响:在循环中未找到第一次迭代,且盈亏图非零。 根据以下逻辑执行计算:
- 如果盈亏大于零且回撤较小,则将当前盈亏值除以回撤值。 因此,我们获得一个系数,该系数表示为了将当前盈亏减至零,需要多少连续的最大回撤次数。
- 如果盈亏小于零,且所有成交的最大利润都大于零,则我们将盈亏值(当前为亏损)除以获得的最大利润。 由此,我们得到一个系数,该系数表示为了将当前回撤降至零,需要多少连续的最大盈利次数。
下一个方法 CalcPLHist 基于类似的机制,但是它利用其他结构字段进行计算:data.oneLot.Accomulated_DD,data.total.Accomulated_DD 和 data.oneLot.Accomulated_Profit,data.total.Accomulated_Profit。 我们之前已研究过它的算法,因此,我们进入下面的两种方法。
CalcData 和 CalcData_item:
这些方法计算所有辅助系数和主要系数。 我们从 CalcData_item 开始。 其目的是计算上述附加系数,再基于这些附加系数计算主要系数。
//+------------------------------------------------------------------+ //| Calculating auxiliary data | //+------------------------------------------------------------------+ void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out, bool isOneLot) { double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL int n=0; // Number of profits and losses if(pl>=0) { out.Total_Profit_numDeals++; n=1; out.dealsCounter.Counter.DD=0; out.dealsCounter.Counter.Profit++; } else { out.Total_DD_numDeals++; out.dealsCounter.Counter.DD++; out.dealsCounter.Counter.Profit=0; } out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD); out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit); // Series of profits and losses int s=ArraySize(out.R_arr); if(!(s>0 && out.R_arr[s-1]==n)) { ArrayResize(out.R_arr,s+1,s+1); out.R_arr[s]=n; } out.PL+=pl; //Total PL // Max Profit / DD if(out.Max_DD_forDeal>pl) { out.Max_DD_forDeal=pl; out.DT_Max_DD_forDeal=deal.DT_close; } if(out.Max_Profit_forDeal<pl) { out.Max_Profit_forDeal=pl; out.DT_Max_Profit_forDeal=deal.DT_close; } // Accumulated Profit / DD out.Accomulated_DD+=(pl>0 ? 0 : pl); out.Accomulated_Profit+=(pl>0 ? pl : 0); // Extreme profit values double maxPL=MathMax(out.Max_Profit_byPL,out.PL); if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// another check is needed to save the date { out.DT_Max_Profit_byPL=deal.DT_close; out.Max_Profit_byPL=maxPL; } double maxDD=out.Max_DD_byPL; double DD=0; if(out.PL>0) DD=out.PL-maxPL; else DD=-(MathAbs(out.PL)+maxPL); maxDD=MathMin(maxDD,DD); if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// another check is needed to save the date { out.Max_DD_byPL=maxDD; out.DT_Max_DD_byPL=deal.DT_close; } out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0)); }
首先,在第 i 次迭代中计算盈亏值。 然后,如果在此迭代中有盈利,则增加盈利成交的计数器,并将连续亏损的计数器清零。 另外,将变量 n 的值设置为 1,这表示该笔成交有盈利。 如果盈亏低于零,则增加亏损计数器,并令盈利成交计数器为零。 之后,分配连串最大盈利和亏损的次数。
下一步是计算连串盈利和亏损的成交。 连串意味着连续的获胜或失败交易。 在此数组中,零始终后跟一,而一始终后跟零。 这表示获胜和亏损交易交替,但是 0 或 1 可能意味着多笔交易。 该数组将用于计算 Z 得分,该得分展示交易的随机程度。 下一步是分配最大利润/回撤值,以及计算出的累计利润/亏损。 在此方法结束时,计算极值点数,即填充结构的最大盈亏和亏损值。
CalcData 数据用得到的中间数据来计算所需的系数,并在每次迭代时更新计算。 它的实现如下:
void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH) { out.num_deals++; // Counting the number of deals CalcData_item(deal,out.oneLot,true); CalcData_item(deal,out.total,false); if(!isBH) { // Fill PL graphs CalcPL(deal,out,PL.PL_total,_Total); CalcPL(deal,out,PL.PL_oneLot,_OneLot); CalcPL(deal,out,PL.PL_Indicative,_Indicative); // Fill PL Histogram graphs CalcPLHist(deal,out,PL_hist.PL_total,_Total); CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative); // Fill PL graphs by days CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal); CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal); // Fill Profit Factor graphs ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true); ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false); // Fill Recovery Factor graphs RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true); RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false); // Fill winning coefficient graphs WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true); WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false); // Fill Sharpe Ration graphs ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/); ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/); // Fill Z Score graphs AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals, (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals, (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal); AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals, (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals, (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal); } else // Fill PL Buy and Hold graphs { CalcPL(deal,out,BH.PL_total,_Total); CalcPL(deal,out,BH.PL_oneLot,_OneLot); CalcPL(deal,out,BH.PL_Indicative,_Indicative); CalcPLHist(deal,out,BH_hist.PL_total,_Total); CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative); } if(!out.isNot_firstDeal) out.isNot_firstDeal=true; // Flag "It is NOT the first deal" }
首先,调用所述方法计算一手和手数管理交易系统这两种数据类型的中间系统。 然后,将计算拆分为 BH 和相反类型数据的系数。 在每个模块内计算可解释的系数。 买入并持有策略仅计算图形,因此未调用系数计算方法。
下一组方法按天数切分计算盈利/亏损:
//+------------------------------------------------------------------+ //| Create a structure of trading during a day | //+------------------------------------------------------------------+ void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal) { cmpDay(deal,MONDAY,out.Mn,calcBy); cmpDay(deal,TUESDAY,out.Tu,calcBy); cmpDay(deal,WEDNESDAY,out.We,calcBy); cmpDay(deal,THURSDAY,out.Th,calcBy); cmpDay(deal,FRIDAY,out.Fr,calcBy); } //+------------------------------------------------------------------+ //| Save resulting PL/DD for the day | //+------------------------------------------------------------------+ void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy) { ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open); if(day==etalone) { if(deal.pl_forDeal>0) { ans.Profit+=deal.pl_forDeal; ans.numTrades_profit++; } else if(deal.pl_forDeal<0) { ans.Drawdown+=MathAbs(deal.pl_forDeal); ans.numTrades_drawdown++; } } } //+------------------------------------------------------------------+ //| Average resulting PL/DD for the day | //+------------------------------------------------------------------+ void CReportCreator::avarageDay(PLDrawdown &day) { if(day.numTrades_profit>0) day.Profit/=day.numTrades_profit; if(day.numTrades_drawdown > 0) day.Drawdown/=day.numTrades_drawdown; }
按天划分盈利/回撤的主要工作由 cmpDay 方法执行,该方法首先检查日期是否与所请求的日期相对应,然后加入盈利和亏损值。 亏损是合计模量数。 CalcDailyPL 是一种汇总方法,尝试将当前通测盈亏添加到五个工作日之一。 在主要的 Create 方法中调用 avarageDay 方法来平均盈利/亏损。 此方法不执行任何特定操作,它仅基于早前计算出的绝对盈利/亏损值来计算平均值。
盈利因子计算方法
//+------------------------------------------------------------------+ //| Calculate Profit Factor | //+------------------------------------------------------------------+ void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); if(dd==0) item.coef=0; else item.coef=profit/dd; int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
该方法计算的图形能反映整体交易的盈利因子变化。 最后一个值会显示在测试报告中。 公式很简单= 累计利润/累计亏损。 如果回撤为零,则系数将等于零,因为在经典算术中,不可未加限制就除以零,且同样的规则在语言当中也适用。 所以,我们将为所有算术运算添加除数检查。
恢复因子的计算原理类似:
//+------------------------------------------------------------------+ //| Calculate Recovery Factor | //+------------------------------------------------------------------+ void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double pl=(isOneLot ? data.oneLot.PL : data.total.PL); double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL); if(dd==0) item.coef=0;//ideally it should be plus infinity else item.coef=pl/dd; int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
系数计算公式:第 i 次迭代的盈利/第 i 次迭代的回撤。 还请注意,由于在系数计算期间盈利可为零或负数,因此系数本身可以为零或负数。
胜率
//+------------------------------------------------------------------+ //| Calculate Win Rate | //+------------------------------------------------------------------+ void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals); int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals); if(n_dd == 0 || n_profit == 0) item.coef = 0; else item.coef=(profit/n_profit)/(dd/n_dd); int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
胜率计算公式 =(盈利 / 盈利交易的数量)/(回撤 / 亏损交易的数量)。如果计算时没有盈利,则该系数也可以为负数。
锋锐率的计算有点复杂:
//+------------------------------------------------------------------+ //| Calculate Sharpe Ratio | //+------------------------------------------------------------------+ double CReportCreator::ShartRatio_calc(PLChart_item &data[]) { int total=ArraySize(data); double ans=0; if(total>=2) { double pl_r=0; int n=0; for(int i=1; i<total; i++) { if(data[i-1].Profit!=0) { pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit; n++; } } if(n>=2) pl_r/=(double)n; double std=0; n=0; for(int i=1; i<total; i++) { if(data[i-1].Profit!=0) { std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2); n++; } } if(n>=2) std=MathSqrt(std/(double)(n-1)); ans=(std!=0 ?(pl_r-r)/std : 0); } return ans; }
在 第一个循环中,由盈亏图计算出平均盈利能力,其中,第 i 个盈利能力计算为盈亏相对于先前盈亏值的增长比率 。 该计算基于评估时间序列的价格序列常规化示例。
在下一个循环中,使用相同的常规化盈利序列来计算波动率。
之后,利用公式(平均利润 - 无风险比率)/ 波动率(标准回报偏差)计算系数本身。
也许我在序列常规化时,甚至公式中应用了非传统方法,但是这种计算似乎很合理。 如果您发现了任何错误,请为文章添加评论。
计算 VaR 和正态分布图。 这部分包括三种方法。 其中有两个正在计算,第三个将所有计算汇总。 我们来研究这些方法。
//+------------------------------------------------------------------+ //| Distribution calculation | //+------------------------------------------------------------------+ void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]) { double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0; int total=ArraySize(data); ZeroMemory(out.absolute); ZeroMemory(out.growth); ZeroMemory(out.absolute.VaR); ZeroMemory(out.growth.VaR); ArrayFree(out.absolute.distribution); ArrayFree(out.growth.distribution); // Calculation of distribution parameters if(total>=2) { int n=0; for(int i=0; i<total; i++) { Mx_absolute+=data[i].Profit; if(i>0 && data[i-1].Profit!=0) { Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit; n++; } } Mx_absolute/=(double)total; if(n>=2) Mx_growth/=(double)n; n=0; for(int i=0; i<total; i++) { Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2); if(i>0 && data[i-1].Profit!=0) { Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2); n++; } } Std_absolute=MathSqrt(Std_absolute/(double)(total-1)); if(n>=2) Std_growth=MathSqrt(Std_growth/(double)(n-1)); // Calculate VaR out.absolute.VaR.Mx=Mx_absolute; out.absolute.VaR.Std=Std_absolute; out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute); out.growth.VaR.Mx=Mx_growth; out.growth.VaR.Std=Std_growth; out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth); out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth); out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth); // Calculate distribution for(int i=0; i<total; i++) { Chart_item item_a,item_g; ZeroMemory(item_a); ZeroMemory(item_g); item_a.x=data[i].Profit; item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit); if(i>0) { item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0); item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x); } int s=ArraySize(out.absolute.distribution); ArrayResize(out.absolute.distribution,s+1,s+1); out.absolute.distribution[s]=item_a; s=ArraySize(out.growth.distribution); ArrayResize(out.growth.distribution,s+1,s+1); out.growth.distribution[s]=item_g; } // Ascending sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer); sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer); } } //+------------------------------------------------------------------+ //| Calculate VaR | //+------------------------------------------------------------------+ double CReportCreator::VaR(double quantile,double Mx,double Std) { return Mx-quantile*Std; } //+------------------------------------------------------------------+ //| Distribution calculation | //+------------------------------------------------------------------+ double CReportCreator::PDF_calc(double Mx,double Std,double x) { if(Std!=0) return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std); else return 0; }
VaR 计算方法是最简单的方法。 它在计算中利用了历史 VaR 模型。
常规化分布计算方法是 Matlab 统计分析软件包中提供的一种方法。
常规化分布计算和图形构建方法是一种汇总方法,其中应用了上述方法。 在第一次循环中,计算平均利润值。 在第二次循环里,计算回报的标准偏差。 图形的回报率和按增长计算的 VaR 也会作为常规化时间序列计算。 此外,在填充 VaR 值之后,使用上述方法计算正态分布图。 作为 x 轴,我们将盈利性用于增长基准图表,并将绝对利润值用于利润基准图表。
为了计算 Z 分值,我使用了一个公式,该公式来自本站点的文章之一。 完整的实现可在附件中找到。
请注意,所有计算均从调用 Calculate 方法开始,并携带以下调用参数
void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);
前面提到的文章 “100次 最佳优化通测” 里论述了它的实现。 所有公开方法都不执行任何逻辑运算,但它们充当取值器,取决于输入参数(指示所需信息的类型)形成请求的数据。
结束语
在上一篇文章中,我们研究了利用 C# 语言开发函数库的过程。 在本文中,我们移入下一步 — 创建交易报告,我们可利用所创建的方法获得报告。 在早前的文章中已经研究过报告的生成机制。 但它已有所改进和修订。 本文表述了这些开发出的最新版本。 提供的解决方案已在各种优化和测试流程中进行了测试。
附件存档中有两个文件夹。 将二者解压缩到 MQL/Include 目录。
附件中包含以下文件:
- CustomGeneric
- GenericSorter.mqh
- ICustomComparer.mqh
- History manager
- CustomComissionManager.mqh
- DealHistoryGetter.mqh
- ReportCreator.mqh
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/7452
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



这篇文章很棒,但我甚至还没开始学习 mql5。有一次,我试图在 mql4 上做同样的事情,但想法失败了,尽管我直到最后都没有放弃。所以问题是:有可能在 mql5 上实现类似的功能吗?
我认为可以,但我尽量不在 MQL4 中编写,我认为最好使用最新版本的产品,目前是 MQL5。