概述
目前,有若干种编程方法: 模块化,面向对象和结构化。 在本文中,我们将讨论有关交易机器人的模块化编程。
模块化编程是一种程序开发方法,涉及到将程序拆分为独立的模块。
模块化编程的主要原则是 "切分和规则"。 使用模块化架构的便利性在于它具有更新 (替换) 模块时无需更改系统其余部分的能力。
模块化编程的核心有三个基本概念。
- 帕纳斯 (Parnas) 掩盖信息原则。 模块用于隐藏信息,并形成用于解决特定问题的算法。 模块以后可以用另一个模块替换。
- 科恩 (Cohen) 的模块化公理。 模块是执行程序特定功能的独立程序单元。
- Tseytin 的装配编程。 模块是组成程序的 "砖块"。
唯一可替代模块化的是单体程序。 虽然这不是很方便。 如果您需要更改或补充某些程序函数,您需要编辑智能交易系统的代码,在大多数情况下,这可以由代码作者或其他有经验的程序员来完成。 此外,如果是编译单体程序,则只能由版权所有者来编辑。 由您自己或聘请第三方开发人员来更行重要程序函数应该会更方便。
图例 1. 模块化交易机器人的抽象示意图
多模块化原理
模块化编程是将任务切分成众多子任务的技术,这些子任务作为单独的模块 (文件) 实现。 通常,程序模块是单独的程序,或功能完备且自主编译的程序单元以某种方式与被调用模块识别并组合。 换言之,模块是已完成的程序功能片段,是为其它程序中使用而设计开发的单独编译文件。
在确定实现特定算法功能的模块集合时,应考虑以下因素:
- 每个模块都由父模块调用执行,并在完成其工作后,将控制权返回给调用它的模块;
- 算法中的主要决策是在层次结构中的最高级别进行;
- 模块在数据方面彼此独立;
- 模块不依赖于访问它们的历史。
总结以上所有内容,模块化程序其逻辑结构的任何部分都可以在不引发其它部分变化的情况下进行变更。
主要模块参数:
- 一个输入和一个输出 — 在输入端,程序模块接收一组初始数据,处理它们并返回一组结果数据,从而实现 IPO 原则 (输入-处理-输出);
- 功能完整性 — 为了执行单独的功能,模块执行所规定操作的完整列表,足以彻底完成处理;
- 逻辑独立 — 程序模块的结果仅依赖于源数据。 它不依赖于其它模块的操作;
- 与其它程序模块弱数据链接 — 模块之间的数据交换应尽可能减少。
MQL5 语言可以开发三类程序: 智能交易系统, 指标或脚本。 管理所有交易功能的模块最适合作为智能交易系统的主模块。而其它模块作为指标实现。 适于作为指标形式的模块: 用给定算法计算的数据可以存储在指标缓冲区中,并在必要时传递给多模块智能交易系统。 反之,智能交易系统可以依据任务选用或忽略这些数据。 在某些项目中,使用智能交易系统作为外部模块是合理的,但与此同时,有必要详细考虑数据交换机制。
许多如您一样的人肯定在自己的智能交易系统中应用了模块化技术: 例如,自定义指标 作为生成和整理交易信号的模块。
而我认为,最合理的解决方案如此这般: 所有基本功能都集中在主模块中,且不需要外部参与。 反之,所需的外部模块只是用来适应不同的市场条件并改善交易策略。 程序功能集合应由用户确定,而不是由代码或策略开发者。 重点要注意,不可有任何人违反彼此的合法权利。
主模块 — 智能交易系统
主模块是整个项目的管控所在,是智能交易系统层次结构中最重要的模块。 它应该包含交易功能。 没有它们,任何交易策略都是毫无意义的。
我们使用代码库的特定 示例 来开发多模块智能交易系统。 初始智能交易系统依据 iBands 指标通道以固定手数进行交易,它会在通道边界上逆向开仓。 该智能交易系统完全自给自足,不需要任何外部程序。
并非每个智能交易系统都是多模块的。
我们应该将什么添加到代码中以便将其转换为模块项目?
- 声明用户随后要用到的外部模块 (指标)。
- 为集成添加必要的功能。
- 为外部模块开发人员准备文档 (启用在单独文件中生成文档的功能)。 开发外部模块可能需要有关正确使用主模块数据结构的信息。 例如,在此示例中,资金管理模块应将手数大小传递给智能交易系统,且将距离当前价格的点数传递给持仓跟踪模块。
变换的结果就是,我们获得了模块化智能交易系统,允许集成多达七个外部模块。
- 模块 1 — 资金管理模块。 它提供手数。
- 模块 2 — 跟踪持仓并设置止损。 它提供距开仓价格的止损点数。
- 模块 3 — 跟踪持仓并设置止盈。 它提供距开仓价格的止盈点数。
- 模块 4 — 跟踪持仓并设置尾随停止。 它提供距当前价格的止损点数。
- 模块 5 — 交易信号生成。 它提供一个信号值。
- 模块 6 — 用于整理交易信号的模块。 它提供过滤值。
- 模块 7 — 跟踪持仓并设置盈亏平衡。 它提供距开仓价格的止损点数。
图例 2. OnInit() 函数和初始化外部模块
图例 3. OnTick() 函数和从外部模块读取数据
图例 4. OnTrade() 函数和从外部模块读取数据
图例 5. 用于生成交易信号以及从外部模块读取数据的函数
//****** project (module expert): test_module_exp.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 TEST 主模块。" #property link "该项目使用 7 个外部模块。" //--- #include <Trade\Trade.mqh> //--- MqlTick last_tick; CTrade trade; //--- input int e_bands_period=80; // 移动均线周期 int e_bands_shift=0; // 位移 input double e_deviation=3.0; // 标准偏差 input ENUM_APPLIED_PRICE e_applied_price=PRICE_CLOSE; // 价格类型 input bool on_module=false; // 是否使用插件 //--- double lot=0.01; // 固定手数 double min_lot=0.01; // 最低允许手数 bool on_trade=false; // 交易功能标志 //--- 用于存储 iBands 指标句柄的变量 int handle_Bands; //--- 模块 1 bool on_lot=false; int handle_m1; //--- 模块 2 bool on_SL=false; int handle_m2; //--- 模块 3 bool on_TP=false; int handle_m3; //--- 模块 4 bool on_Trail=false; int handle_m4; //--- 模块 5 bool on_signals=false; int handle_m5; //--- 模块 6 bool on_Filter=false; int handle_m6; //--- 模块 7 bool on_Breakeven=false; int handle_m7; //+------------------------------------------------------------------+ //| 交易信号结构 | //+------------------------------------------------------------------+ struct sSignal { bool Buy; // 买入信号 bool Sell; // 卖出信号 }; //+------------------------------------------------------------------+ //| 交易信号生成器 | //+------------------------------------------------------------------+ sSignal Buy_or_Sell() { sSignal res={false,false}; //--- 模块 5 if(on_signals) { // 如果有额外模块 double buffer_m5[]; ArraySetAsSeries(buffer_m5,true); if(CopyBuffer(handle_m5,0,0,1,buffer_m5)<0) return(res); if(buffer_m5[0]<-1) res.Sell=true; if(buffer_m5[0]>1) res.Buy=true; } //--- 模块 6 if(on_Filter) { // 如果有额外模块 double buffer_m6[]; ArraySetAsSeries(buffer_m6,true); if(CopyBuffer(handle_m6,0,0,1,buffer_m6)<0) return(res); lot=buffer_m6[0]; if(buffer_m6[0]<1) res.Buy=false; if(buffer_m6[0]>-1) res.Sell=false; } //--- //--- 指标缓存区 double UpperBuffer[]; double LowerBuffer[]; double MiddleBuffer[]; ArraySetAsSeries(MiddleBuffer,true); CopyBuffer(handle_Bands,0,0,1,MiddleBuffer); ArraySetAsSeries(UpperBuffer,true); CopyBuffer(handle_Bands,1,0,1,UpperBuffer); ArraySetAsSeries(LowerBuffer,true); CopyBuffer(handle_Bands,2,0,1,LowerBuffer); //--- 时间序列 double L[]; double H[]; ArraySetAsSeries(L,true); CopyLow(_Symbol,_Period,0,1,L); ArraySetAsSeries(H,true); CopyHigh(_Symbol,_Period,0,1,H); if(H[0]>UpperBuffer[0]&& L[0]>MiddleBuffer[0]) res.Sell=true; if(L[0]<LowerBuffer[0] && H[0]<MiddleBuffer[0]) res.Buy=true; //--- return(res); } //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 创建指标句柄 handle_Bands=iBands(_Symbol,_Period,e_bands_period,e_bands_shift,e_deviation,e_applied_price); if(handle_Bands==INVALID_HANDLE) return(INIT_FAILED); else on_trade=true; if(on_module) { //--- 模块 1 //--- 检查: 是否有外部模块? handle_m1=iCustom(NULL,0,"Market\\test_module_MM"); if(handle_m1!=INVALID_HANDLE) on_lot=true; //--- 模块 2 //--- 检查: 是否有外部模块? handle_m2=iCustom(NULL,0,"Market\\test_module_SL"); if(handle_m2!=INVALID_HANDLE) on_SL=true; //--- 模块 3 //--- 检查: 是否有外部模块? handle_m3=iCustom(NULL,0,"Market\\test_module_TP"); if(handle_m3!=INVALID_HANDLE) on_TP=true; //--- 模块 4 //--- 检查: 是否有外部模块? handle_m4=iCustom(NULL,0,"Market\\test_module_Trail"); if(handle_m4!=INVALID_HANDLE) on_Trail=true; //--- 模块 5 //--- 检查: 是否有外部模块? handle_m5=iCustom(NULL,0,"Market\\test_module_signals"); if(handle_m5!=INVALID_HANDLE) on_signals=true; //--- 模块 6 //--- 检查: 是否有外部模块? handle_m6=iCustom(NULL,0,"Market\\test_module_Filter"); if(handle_m6!=INVALID_HANDLE) on_Filter=true; //--- 模块 7 //--- 检查: 是否有外部模块? handle_m7=iCustom(NULL,0,"Market\\test_module_Breakeven"); if(handle_m7!=INVALID_HANDLE) on_Breakeven=true; } //--- 交易操作允许的最小交易量 min_lot=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逐笔报价函数 | //+------------------------------------------------------------------+ void OnTick() { double Equity=AccountInfoDouble(ACCOUNT_EQUITY); //--- 模块 1 if(on_lot) { // 如果有额外模块 double buffer_m1[]; ArraySetAsSeries(buffer_m1,true); if(CopyBuffer(handle_m1,0,0,1,buffer_m1)<0) return; lot=buffer_m1[0]; } //--- 模块 4 if(on_Trail) if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_PROFIT)>0) { // 如果有额外模块 double buffer_m4[]; ArraySetAsSeries(buffer_m4,true); if(CopyBuffer(handle_m4,0,0,1,buffer_m4)<0) return; double TR=buffer_m4[0]; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) if(PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point>PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point; if(price_SL>PositionGetDouble(POSITION_SL)) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),0); } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) if(PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point<PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point; if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),0); } } //--- 模块 7 if(on_Breakeven) if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_PROFIT)>0) { // 如果有额外模块 double buffer_m7[]; ArraySetAsSeries(buffer_m7,true); if(CopyBuffer(handle_m7,0,0,1,buffer_m7)<0) return; double TRB=buffer_m7[0]; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) if(PositionGetDouble(POSITION_PRICE_CURRENT)-TRB*_Point>PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-5*_Point; if(price_SL>PositionGetDouble(POSITION_SL)) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP)); } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) if(PositionGetDouble(POSITION_PRICE_CURRENT)+TRB*_Point<PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+5*_Point; if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP)); } } //--- if(lot<min_lot) lot=min_lot; //--- if(on_trade) { sSignal signal=Buy_or_Sell(); //--- 所需以及可用保证金的值 double margin,free_margin=AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- 买入 if(signal.Buy) { if(!PositionSelect(_Symbol)) { SymbolInfoTick(_Symbol,last_tick); if(OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,NormalizeDouble(lot,2),last_tick.ask,margin)) if(margin<Equity) trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,NormalizeDouble(lot,2),last_tick.ask,0,0,"买入: 新仓"); } else { if(PositionGetDouble(POSITION_PROFIT)<0) return; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { trade.PositionClose(_Symbol); SymbolInfoTick(_Symbol,last_tick); if(OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,NormalizeDouble(lot,2),last_tick.ask,margin)) if(margin<Equity) trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,NormalizeDouble(lot,2),last_tick.ask,0,0,"买入: 逆向"); } } } //--- 卖出 if(signal.Sell) { if(!PositionSelect(_Symbol)) { SymbolInfoTick(_Symbol,last_tick); if(OrderCalcMargin(ORDER_TYPE_SELL,_Symbol,NormalizeDouble(lot,2),last_tick.bid,margin)) if(margin<Equity) trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,NormalizeDouble(lot,2),last_tick.bid,0,0,"卖出: 新仓"); } else { if(PositionGetDouble(POSITION_PROFIT)<0) return; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { trade.PositionClose(_Symbol); SymbolInfoTick(_Symbol,last_tick); if(OrderCalcMargin(ORDER_TYPE_SELL,_Symbol,NormalizeDouble(lot,2),last_tick.bid,margin)) if(margin<Equity) trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,NormalizeDouble(lot,2),last_tick.bid,0,0,"卖出: 逆向"); } } } } } //+------------------------------------------------------------------+ //| 交易函数 | //+------------------------------------------------------------------+ void OnTrade() { if(on_SL && on_TP) // 如果有额外模块 { //--- 模块 2 double buffer_m2[]; ArraySetAsSeries(buffer_m2,true); if(CopyBuffer(handle_m2,0,0,1,buffer_m2)<0) return; double SL=buffer_m2[0]; //--- 模块 3 double buffer_m3[]; ArraySetAsSeries(buffer_m3,true); if(CopyBuffer(handle_m3,0,0,1,buffer_m3)<0) return; double TP=buffer_m3[0]; //--- 持仓修改 if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_SL)==0) { //--- 买入 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)+TP*_Point; double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)-SL*_Point; trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits())); } //--- 卖出 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)-TP*_Point; double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)+SL*_Point; trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits())); } } } }
如果程序中设置的目录未包含必要的模块 (文件),则交易机器人应用默认功能。 因此,缺少外部模块不会对智能系统的性能产生严重影响。
最重要的模块
如何从单体程序制作多模块智能系统? 模块化项目从分析一般任务和定义功能的封闭片段开始,这可在以后将其形式转化为编译模块。 在这种情况下,您需要选择可以显著改变智能交易系统操作的最典型函数,且这些函数可基于各种算法。 众所周知,大多数智能交易系统都采用相同的过程:
- 资金 (风险) 管理模块;
- 持仓跟踪模块 (止损和止盈);
- 尾随停止模块;
- 跟踪信号生成模块;
- 信号过滤模块。
实现每个列举的模块均有很多选项。 在本文中,我们向您展示最简单的解决方案,因为模块化编程的方法对于我们来说比多字符串功能更重要。
开发辅助模块
辅助 (外部) 模块是执行特定功能并将输入数据放置到指标缓冲区的指标。 如有必要,主模块使用这些数据。 因此,智能交易系统要适应交易者运用此交易策略的需求。 相同的初始智能交易系统可为每种特定金融产品或经纪商重新搭配。 事实上,这是一个工具包,允许交易者搭配出无限数量的交易机器人。
编程是一个耗费人力的过程。 尽管它具有创造性,但它所包含许多平凡操作,也许您希望能够自动化。 除此以外,自动化可提升生产力并减少误差。
下面附带的模块生成器允许您在几秒钟内形成多达八个连接至一个多模块项目的文件。 这极大地简化并加速了开发和组装 (参见视频) 工作。
视频 1. 管理多模块项目生成器的面板
该面板允许您设置项目生成的特定模块。 在我们的示例中,创建了一个 "test" 项目。根据所选的模块组合,生成器自动生成代码,而不含不必要的版块和文件。
生成的文件将放置到 Files 文件夹中 (参见图例 2)。 "test_module_exp.mq5" 主模块的名称由项目名称 ("test") 和 "_module_exp.mq5" 前缀组成。 将其放在 Experts 文件夹中,而外部模块的其余文件应位于 Indicators\Market 之中。
图例 6. 生成的 "test" 项目文件
之后,编译所有文件并继续测试多模块项目。
视频 2. 编译 'test' 项目的生成文件
手工创建类似项目需要花费很多时间。 显然,我们要从主模块开始。 以后可以连接到项目的外部模块定义完毕后,继续进行开发和编程。 需要跟踪的主要内容是主模块所需等待的辅助模块的输出数据。 由于模块是指标,指标缓冲区 包含的数值均为实数类型,因此在主模块中,必须提供从实数类型到相应类型变量的转换算法。
应当设计外部模块,以便可以在没有输入的情况下在主模块中调用它们,即默认值。 这种调用机制简化了外部数据管理系统的开发。
我们来更详细地研究哪些外部模块在交易策略中最重要。
示例 1: 资金管理模块
此外部模块计算开单的手数。 您可以在下面看到计算交易量的最简单实现方法 (以存款资金的可用百分比计算):
//****** project (module MM): test_module_MM_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 资金管理模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- input double lot_perc=0.1; // 净值百分比值 double Buffer1[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double Equity=AccountInfoDouble(ACCOUNT_EQUITY); //--- 依据净值计算手数 Buffer1[0]=NormalizeDouble(Equity*lot_perc/1000.0,2); // 确定手数的函数 if(Buffer1[0]<0.01) Buffer1[0]=0.01; return(rates_total); };
如果我们默认调用此模块 (不设置输入参数的值),则主模块按照可用资金的 0.1% 获取允许的手数,以便执行交易。 从主程序调用此模块的示例:
//+------------------------------------------------------------------+ //| 智能系统逐笔报价函数 | //+------------------------------------------------------------------+ void OnTick() { double Equity=AccountInfoDouble(ACCOUNT_EQUITY); //--- 模块 1 if(on_lot) { // 如果有额外模块 double buffer_m1[]; ArraySetAsSeries(buffer_m1,true); if(CopyBuffer(handle_m1,0,0,1,buffer_m1)<0) return; lot=buffer_m1[0]; } ... }
示例 2: 持仓跟踪模块 (止损,止盈和尾随)
设置止损 (SL) 和止盈 (TP) 是跟踪持仓的方法之一。 由于不同的交易策略应用了各种放置止损和止盈计算方法的组合,因此将其分成两个模块 (止损和止盈) 的选项似乎是最实用的。 但是如果我们仍然决定在单个模块中组合止损和止盈,它们的值应该放在不同的 指标缓冲区 中。
放置止损模块:
//****** project (module SL): test_module_SL_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 止损模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- double Buffer1[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double SL=100; // SL in points //--- 计算止损 Buffer1[0]=SL; return(rates_total); };
放置止盈模块:
//****** project (module TP): test_module_TP_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 止盈模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- double Buffer1[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double TP=100; // TP in points //--- 计算止盈 Buffer1[0]=TP; return(rates_total); };
代码显示了计算止损和止盈数值的最明显选项 — 以点数为单位。 实际上,它们不是计算的,而是由常数设定。 数值直接在程序中指定,而不是在输入中指定。 这样做是为了展示无输入外部模块的实现。 任何新入门的程序员都可以编写这样一个 "粗略" 的代码。
我相信,OnTrade 函数是存储上述模块调用的最佳位置。 近似样子如下:
//+------------------------------------------------------------------+ //| 交易函数 | //+------------------------------------------------------------------+ void OnTrade() { if(on_SL && on_TP) // 如果有额外模块 { //--- 模块 2 double buffer_m2[]; ArraySetAsSeries(buffer_m2,true); if(CopyBuffer(handle_m2,0,0,1,buffer_m2)<0) return; double SL=buffer_m2[0]; //--- 模块 3 double buffer_m3[]; ArraySetAsSeries(buffer_m3,true); if(CopyBuffer(handle_m3,0,0,1,buffer_m3)<0) return; double TP=buffer_m3[0]; //--- 持仓修改 if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_SL)==0) { //--- 买入 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)+TP*_Point; double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)-SL*_Point; trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits())); } //--- 卖出 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)-TP*_Point; double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)+SL*_Point; trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits())); } } } }
除了在开仓后立即设置静态止损和止盈值,通常也会应用尾随停止或浮动止损。 大多数情况下,一笔持仓开始盈利后才会设定。 我们来看看最明显的实现: 我们设置止损与当前价格的距离。
//****** project (module Trail): test_module_Trail_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 尾随模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- double Buffer1[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double TR=50; // Trail in points //--- 计算尾随 Buffer1[0]=TR; return(rates_total); };
与之前的止损和止盈代码一样,尾随停止计算的距离设置为常量,以便简化程序和读取。
尾随停止模块的调用应该在 OnTick 函数中实现,因为当前价格会依据当前逐笔报价变化,并且应该连续跟踪止损价位。 主模块决定是否应该修改。 在接收到点数距离后,智能交易系统修改持仓,并顺盈利增长方向移动止损。
//+------------------------------------------------------------------+ //| 智能系统逐笔报价函数 | //+------------------------------------------------------------------+ void OnTick() { ... //--- 模块 4 if(on_Trail) if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_PROFIT)>0) { // 如果有额外模块 double buffer_m4[]; ArraySetAsSeries(buffer_m4,true); if(CopyBuffer(handle_m4,0,0,1,buffer_m4)<0) return; double TR=buffer_m4[0]; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) if(PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point>PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point; if(price_SL>PositionGetDouble(POSITION_SL)) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),0); } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) if(PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point<PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point; if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),0); } } ... }
还有另一种持仓跟踪方法 — 将止损置于盈亏平衡点。 当止损被激活时,持仓将以零利润或预定利润平仓。 该模块可能看起来像这样:
//****** project (module Breakeven): test_module_Breakeven_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 盈亏平衡模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- double Buffer1[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double Breakeven=100; // Breakeven in points //--- 计算盈亏平衡点 Buffer1[0]=Breakeven; return(rates_total); };
此模块设置当前价格与开仓价格的距离,以便将止损设置到盈亏平衡点。 调用盈亏平衡模块也应位于 OnTick 函数中。
//+------------------------------------------------------------------+ //| 智能系统逐笔报价函数 | //+------------------------------------------------------------------+ void OnTick() { ... //--- 模块 7 if(on_Breakeven) if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_PROFIT)>0) { // 如果有额外模块 double buffer_m7[]; ArraySetAsSeries(buffer_m7,true); if(CopyBuffer(handle_m7,0,0,1,buffer_m7)<0) return; double TRB=buffer_m7[0]; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) if(PositionGetDouble(POSITION_PRICE_CURRENT)-TRB*_Point>PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-5*_Point; if(price_SL>PositionGetDouble(POSITION_SL)) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP)); } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) if(PositionGetDouble(POSITION_PRICE_CURRENT)+TRB*_Point<PositionGetDouble(POSITION_PRICE_OPEN)) { double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+5*_Point; if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP)); } } ... }
示例 3: 交易信号生成模块
就实现而言,也许这是最复杂的模块。它生成执行交易的信号: 下单,平仓等。 它的开发复杂性源于几乎所有的指标均要适应交易条件的事实。 不存在使用同一套输入值即可为不同的金融产品产生交易信号的指标。
显然,主程序不应自行配置信号模块,因为太多的信号模块也许会完全破坏模块项目操作。 因此,在连接到正常项目之前,应预先准备产生交易信号的指标。 稍后我们会在多模块智能交易系统优化的专门章节中讨论这个问题。 现在,我们来看看交易信号模块的代码:
//****** project (module signals): test_module_signals_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 交易信号模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- double Buffer1[]; //--- 用于存储 iBands 指标句柄的变量 int handle_Bands; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { handle_Bands=iBands(_Symbol,_Period,20,0,3.5,PRICE_CLOSE); //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double signal=0.0; ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); //--- 指标缓存区 double UpperBuffer[]; double LowerBuffer[]; ArraySetAsSeries(UpperBuffer,true); CopyBuffer(handle_Bands,1,0,1,UpperBuffer); ArraySetAsSeries(LowerBuffer,true); CopyBuffer(handle_Bands,2,0,1,LowerBuffer); //--- 计算交易信号 if(high[0]>UpperBuffer[0]) signal=-2.0; if(low[0]<LowerBuffer[0]) signal=2.0; Buffer1[0]=signal; return(rates_total); };
以下数值将写入信号模块的指标缓冲区:
- 2.0 — 如果买入信号形成;
- 0.0 — 如果无交易信号;
- -2.0 — 如果卖出信号形成。
最好在主模块的指定函数中使用获取交易信号模块的值 - 例如,像这样:
//+------------------------------------------------------------------+ //| 交易信号生成器 | //+------------------------------------------------------------------+ sSignal Buy_or_Sell() { sSignal res={false,false}; //--- 模块 5 if(on_signals) { // 如果有额外模块 double buffer_m5[]; ArraySetAsSeries(buffer_m5,true); if(CopyBuffer(handle_m5,0,0,1,buffer_m5)<0) return(res); if(buffer_m5[0]<-1) res.Sell=true; if(buffer_m5[0]>1) res.Buy=true; } //--- 模块 6 if(on_Filter) { // 如果有额外模块 double buffer_m6[]; ArraySetAsSeries(buffer_m6,true); if(CopyBuffer(handle_m6,0,0,1,buffer_m6)<0) return(res); lot=buffer_m6[0]; if(buffer_m6[0]<1) res.Buy=false; if(buffer_m6[0]>-1) res.Sell=false; } //--- //--- 指标缓存区 double UpperBuffer[]; double LowerBuffer[]; double MiddleBuffer[]; ArraySetAsSeries(MiddleBuffer,true); CopyBuffer(handle_Bands,0,0,1,MiddleBuffer); ArraySetAsSeries(UpperBuffer,true); CopyBuffer(handle_Bands,1,0,1,UpperBuffer); ArraySetAsSeries(LowerBuffer,true); CopyBuffer(handle_Bands,2,0,1,LowerBuffer); //--- 时间序列 double L[]; double H[]; ArraySetAsSeries(L,true); CopyLow(_Symbol,_Period,0,1,L); ArraySetAsSeries(H,true); CopyHigh(_Symbol,_Period,0,1,H); if(H[0]>UpperBuffer[0]&& L[0]>MiddleBuffer[0]) res.Sell=true; if(L[0]<LowerBuffer[0] && H[0]<MiddleBuffer[0]) res.Buy=true; //--- return(res); }
有很多交易策略,每个策略都有自己的信号。 因此,有必要组织交易信号模块的工作,令它们适合给定的策略。 交易策略应在智能交易系统的文档中加以描述,以便信号模块开发人员按照模块项目的技术需求行事。
示例 4: 信号过滤模块
交易者经常使用交易信号过滤器来提高交易机器人的盈利能力。 他们可能会考虑各种因素,包括趋势,交易时间,消息,其它信号指标,等等。
//****** project (module Filter): test_module_Filter_ind.mq5 //+------------------------------------------------------------------+ //| 程序代码由模块化项目生成器生成 | //| 版权所有 2010-2017, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2010-2017, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 过滤模块" //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //--- double Buffer1[]; //--- 用于存储 iBands 指标句柄的变量 int handle_Bands; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { handle_Bands=iBands(_Symbol,_Period,35,0,4.1,PRICE_CLOSE); //--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int &spread[]) { double filtr=0.0; ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); //--- 指标缓存区 double MiddleBuffer[]; ArraySetAsSeries(MiddleBuffer,true); CopyBuffer(handle_Bands,0,0,1,MiddleBuffer); //--- 过滤计算 if(high[0]<MiddleBuffer[0]) filtr=2.0; if(low[0]>MiddleBuffer[0]) filtr=-2.0; Buffer1[0]=filtr; return(rates_total); };
因此,我们研究了实现外部模块的选项,以及将它们集成到多模块智能交易系统中的原则。
优化多模块智能交易系统
优化多模块智能交易系统可能是最关键的问题之一。 那么,如何在策略测试器中优化外部模块的输入参数? 如果它们未在主模块中设置,则我们的选择似乎有限。 我们可以尝试离散地指定外部模块的输入数据,然后测试智能交易系统。 然而,这是一项乏味且极有可能毫无意义的工作。 我们可以做什么?
其中一个可能的选择是使用自优化指标作为外部模块。 有很多关于自动优化的文献。 我也将为这个话题做出贡献。 我们使用文章 "指标盈利能力的可视化测试和警报" 中的思路。 作者建议使用蜡烛值作为虚拟交易执行价格: 最大蜡烛值应用于买入,而最小值应用于卖出。 因此,选择最差的交易条件,并通过这种近似价格优化输入。 推测利用获取的最优值,在实际交易中的结果不会更差 (在相同的历史数据区间上)。 在实际交易中,任何优化后的参数都无法保证盈利。
我们的智能交易系统基于布林带指标内部的交易策略,在其边界处逆向开仓。 我们来替换这个指标,将通道基于轨道线指标。 自均线指标的固定距离形成等距边界。 新信号指标将在使用前自行优化。 展现最大盈利的优化值将用作输入。 两个参数 — МА 周期和自均线的边界距离 — 被选中进行优化。
开发自动优化信号指标的算法:
- 定义参数和优化准则。 在我们的例子中,输入是均线周期和边界位移距离,而最大盈利作为准则。
- 在指标中创建优化版块。 所提议的示例使用固定步幅,在指定范围内进行输入数据的完整搜索。 均线周期为 10-100,步幅为 10。 在 1000-10000 的间隔内搜索移位值,步幅为 1000。
//+------------------------------------------------------------------+ //| 版权所有 2018, Sergey Pavlov (DC2008) | //| http://www.mql5.com/en/users/dc2008 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, Sergey Pavlov (DC2008)" #property link "http://www.mql5.com/en/users/dc2008" #property link "1.00" #property link "多模块智能系统示例: 项目 test 交易信号模块" //--- #include <MovingAverages.mqh> //--- 在图表窗口中显示指标 #property indicator_chart_window //--- 指标计算的缓冲区数量 #property indicator_buffers 1 //--- 指标中的图形序列数量 #property indicator_plots 1 //+------------------------------------------------------------------+ //| 优化结果结构 | //+------------------------------------------------------------------+ struct Opt { int var1; // 参数 1 优化值 int var2; // 参数 2 优化值 double profit; // 盈利 }; //--- double Buffer1[]; bool optimum=false; Opt test={NULL,NULL,NULL}; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { ArraySetAsSeries(Buffer1,true); SetIndexBuffer(0,Buffer1,INDICATOR_DATA); optimum=false; return(INIT_SUCCEEDED); }; //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { double signal=0.0; Buffer1[0]=signal; //--- 输入参数优化 if(!optimum) { ArraySetAsSeries(close,false); int count=rates_total; int total=0; int total_profit=0; for(int d=1000;d<10001;d+=1000) for(int j=10;j<101;j+=10) { double shift=d*_Point; bool open_buy=false; bool open_sell=false; double price_buy=0; double price_sell=0; double profit=0; int order=0; for(int i=j+1;i<count;i++) { double ma=SimpleMA(i,j,close); double sell=ma+shift; double buy=ma-shift; //--- 买入 if(buy>close[i] && !open_buy) { price_buy=high[i]+spread[i]*_Point; if(order==0) profit=0; else profit+=price_sell-price_buy; order++; open_buy=true; open_sell=false; } //--- 卖出 if(sell<close[i] && !open_sell) { price_sell=low[i]-spread[i]*_Point; if(order==0) profit=0; else profit+=price_sell-price_buy; order++; open_sell=true; open_buy=false; } //--- } if(profit>0) if(profit>test.profit) { test.var1=j; test.var2=d; test.profit=profit; total_profit++; } //--- Comment("优化输入..."," 通关=",total," // 盈利 =",total_profit); total++; } //--- Print(" 优化完成: ",test.var1," ",test.var2); Comment("优化完成"); optimum=true; } //--- if(optimum) if(test.profit>0) { ArraySetAsSeries(close,true); double ma=SimpleMA(0,test.var1,close); double sell=ma+test.var2*_Period; double buy=ma-test.var2*_Period; //--- 计算交易信号 if(buy>close[0]) signal=2.0; if(sell<close[0]) signal=-2.0; } //--- 指标缓存区 Buffer1[0]=signal; return(rates_total); };
优化需要花费一些时间,在此期间智能交易系统将无法进行交易。 如果模块交易机器人全天候工作,则自动优化产生的延迟不应对总交易时间产生重大影响。
结束语
- 事实证明,开发多模块智能交易系统不仅是可行的,而且有时也是有用的,并有商业价值。
- 本文展示带有外部模块的交易机器人的原始概念。 然而,模块化编程技术能够开发涉及第三方团队的复杂项目。 在创建模块时,开发人员可自由地掩盖他们的代码,从而保护他们对算法的版权。
- 优化模块项目仍然存在的问题。 用作信号模块或滤波器的信号指标,其自我优化是需要深入推进的主题。
注意: 附加文件允许在所需配置中生成模块项目的源代码。