股票量化交易软件:继续漫步优化2为任意机器人创建优化报告的机制

这是致力于创建自动优化器的系列文章中的下一篇,该优化器可以执行交易策略的漫步优化。 上一篇文章描述过如何创建 DLL,并运用在赫兹股票量化的自动优化器和 EA 之中。 这部分新内容则完全致力于 赫兹股票量化语言。 我们将研究优化报告的生成方法,以及在您的算法中该功能的应用。

策略测试器不允许从智能交易系统中访问其数据,而其提供的结果缺乏细节,所以,赫兹股票量化将利用我在之前文章中实现的优化报告下载功能。 由于此功能的各个独立部分均被修改,而其他内容在先前的文章中并未完全涵盖,因此我们再次研究这些功能,因为在我们的程序里它们是关键的构成部分。 我们从新功能之一开始:添加自定义佣金。 本文中描述的所有类和函数都位于 Include/History manager 目录之下。
自定义佣金和滑点的实现
赫兹股票量化平台测试器提供了许多令人兴奋的可能性。 然而,某些经纪商未将交易佣金添加到历史记录中。 进而,有时您也许要附加佣金以便进行额外的策略测试。 为此目的,我已新加了一个类,可为每个单独的品种保存佣金。 调用相应的方法后,该类将返回佣金和指定的滑点。 该类本身清单如下:
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[]; };