内容简介表
- 简介
- CStrategyList 策略管理器
- 从一个XML列表中加载策略。一个策略的组合
- 使用自定义面板管理策略
- 以组交易的EA
- 在策略测试器中分析EA的效果
- 总结
简介
经常我们需要创建一个能同另一个算法共存的算法,例如,某算法的执行不能影响另一个同时再运行的算法。这种情况出现在当你需要将多个算法集成到一个可执行ex5模块中的时候。尽管它很简单,但是也有明显的“缺陷” —在创建交易策略的引擎时必须考虑算法的特征。
CStrategy交易引擎包含一组算法,能使两个或多个交易策略联合运行。我们将在本文中详细讨论它们。同时我们也将创建一个交易组合 — 一组同时交易的EA,为了分散交易风险。CStrategyList类 — CStrategy类型策略的容器 — 属于提供同时操作策略的算法。此类允许上传基于XML描述的策略,同时使用相应的方法— 一个策略仓库,动态地创建它们。
附件视频展示了在MetaTrader 5 策略测试器中测试多个策略的过程。基于所描述的交易引擎的所有策略都有一个默认的自定义面板,它可以帮助您轻松的直接从图表上控制每个策略。
CStrategyList 策略管理器
在“通用智能交易系统”系列的第二篇文章中描述了 CStrategy 类和它的主要模块。通过使用这个类以及模块中的功能函数,每一个衍生策略都有一个统一的交易逻辑。然而,使用程序组织一个交易过程不仅仅是执行交易请求。确保策略之间的协作性是非常重要的,其中包括在一个可执行ex5模块中运行多种算法。
CStrategyList类就是用于这个特殊目的的。你可以从它的名字猜到,此类提供一个CStrategy型策略的列表,但是它的运作要比一般的数据容器更为复杂些。该模块解决如下问题:
- 确保多个交易策略同时运行;
- 向每个策略实例发送交易事件;
- 从统一的策略XML列表中创建策略对象;
- 同自定义EA配置面板交互。
这里是 CStrategyList 类的头文件:
//+------------------------------------------------------------------+ //| 管理CStrategy类型策略的容器类 //+------------------------------------------------------------------+ class CStrategyList { private: CLog* Log; // 日志 CArrayObj m_strategies; // CStrategy类型策略 CLimits* m_limits; void ParseStrategies(CXmlElement* xmlStrategies, bool load_curr_symbol); void ParseLimits(CXmlElement* xmlLimits); CStrBtn StrButton; public: CStrategyList(void); ~CStrategyList(void); void LoadStrategiesFromXML(string xml_name, bool load_curr_symbol); bool AddStrategy(CStrategy* strategy); int Total(); CStrategy* At(int index); void OnTick(); void OnTimer(); void OnBookEvent(string symbol); void OnDeinit(const int reason); void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam); };
如你所见,大多数方法都是交易事件处理函数。他们有同样类型的内容。让我们分析其中一个,OnBookEvent:
//+------------------------------------------------------------------+ //| 发送 OnBookEvent 事件到所有列表策略 //+------------------------------------------------------------------+ void CStrategyList::OnBookEvent(string symbol) { for(int i=0; i<m_strategies.Total(); i++) { CStrategy *strategy=m_strategies.At(i); strategy.OnBookEvent(symbol); } }
从类的内容来看,它在列表中搜索CStrategy策略并在每个策略中调用一个对应的事件。其他事件的处理方法类似。
除了传递事件外,CStrategyList执行特殊的程序从XML文件中加载策略。关于它运作方式的更多详细信息请读下面的部分。
从一个XML列表中加载策略。一个策略的组合
如果一个ex5执行模块包含多个交易算法,我们需要能够生成策略组合的工具。假设两个具有不同参数的算法在一个执行模块中做交易。如何配置这些参数?最简单的办法就是在EA属性窗口输出每个策略的参数。但是,当使用许多策略,且每个策略都有很多参数时怎么办?这种情况下,不同的调整值,标识,字符串和备注参数列表将会非常庞大。下图就是执行三个交易策略的EA参数窗口:
图 1. 用三种策略交易的EA参数列表
一个EA甚至可以使用更多的策略。这种情况下,参数列表的规模可能难以想象。组合策略交易的第二个重要特征是:流水线化创建策略。假设我们想要运行不同参数的同一个策略。我们该怎么办?显然,除了参数设置不同,这两个策略几乎一样。我们可以把此任务交给一个独立的类,而无需手动创建每个策略。这个类可以自动创建策略对象并正确配置它。
在创建一个策略前,有必要给出它的完整描述。对其的描述需要包含如下几个方面:
- 策略名称;
- 唯一的ID或者编号;
- 策略加载的货币对;
- 时间框架;
- 策略的参数列表(每个策略一个)。
策略描述还可能包含畜类上述之外的属性。最好的方法是使用XML来描述。XML语言是一种特殊的描述工具。他是得描述复杂对象变得容易,因此一个交易策略可以被转化为一个XML文档,一个XML文档能被转化成一个策略。例如,基于一个XML文档,交易引擎能够创建一个策略并正确配置其参数。要直接使用MQL5语言处理这种类型的文档,我们可以使用Code Base中的一个特殊的XML-Parser库来实现。
这里是一个策略组合的XML描述样例,它加载三个不同参数的移动平均策略。
<Global> <Strategies> <Strategy Name="MovingAverage" Magic="100" Timeframe="PERIOD_M1" Symbol="Si"> <TradeStateStart>Stop</TradeStateStart> <Params> <FastMA>1</FastMA> <SlowMA>3</SlowMA> <Shift>0</Shift> <Method>MODE_SMA</Method> <AppliedPrice>PRICE_CLOSE</AppliedPrice> </Params> </Strategy> <Strategy Name="MovingAverage" Magic="101" Timeframe="PERIOD_M5" Symbol="SBRF"> <TradeStateStart>BuyOnly</TradeStateStart> <Params> <FastMA>15</FastMA> <SlowMA>21</SlowMA> <Shift>0</Shift> <Method>MODE_SMA</Method> <AppliedPrice>PRICE_CLOSE</AppliedPrice> </Params> </Strategy> <Strategy Name="MovingAverage" Magic="102" Timeframe="PERIOD_M15" Symbol="GAZR"> <TradeStateStart>BuyAndSell</TradeStateStart> <Params> <FastMA>12</FastMA> <SlowMA>45</SlowMA> <Shift>1</Shift> <Method>MODE_EMA</Method> <AppliedPrice>PRICE_CLOSE</AppliedPrice> </Params> </Strategy> </Strategies> </Global>
每个策略形成<Strategy>单元。下面这些属性在其中指定:Symbol, Timeframe, Magic 和 StrategyName。从上面的例子我们看到三个策略中的每一个都有其自身的货币对,编号和时间框架。除了这些必须的参数,其他策略属性在XML列表中指定。<TradeStateStart>部分用于策略加载时指定交易模式。<Params>部分包含策略的参数。
在开始时,交易引擎尝试从上面的XML文件中加载交易策略。一个策略在LoadStrategiesFromXML方法中基于CStrategyList类的XML文件中,被加载和创建。下面是其方法体以及相关方法:
//+------------------------------------------------------------------+ //| 从XML文件 "xml_name" 中加载策略 //| 如果load_curr_symbol被设置为true,它将仅加载 //| 当前货币对CurrentSymbol()所对应的 //| 策略 //+------------------------------------------------------------------+ void CStrategyList::LoadStrategiesFromXML(string xml_name,bool load_curr_symbol) { CXmlDocument doc; string err; bool res=doc.CreateFromFile(xml_name,err); if(!res) printf(err); CXmlElement *global=GetPointer(doc.FDocumentElement); for(int i=0; i<global.GetChildCount(); i++) { CXmlElement* child = global.GetChild(i); if(child.GetName() == "Strategies") ParseStrategies(child,load_curr_symbol); } } //+------------------------------------------------------------------+ //| 解析 XML 策略 //+------------------------------------------------------------------+ void CStrategyList::ParseStrategies(CXmlElement *xmlStrategies,bool load_curr_symbol) { CParamsBase *params=NULL; for(int i=0; i<xmlStrategies.GetChildCount(); i++) { CXmlElement *xStrategy=xmlStrategies.GetChild(i); if(CheckPointer(params)!=POINTER_INVALID) delete params; params=new CParamsBase(xStrategy); if(!params.IsValid() || (params.Symbol()!=Symbol() && load_curr_symbol)) continue; CStrategy *str=CStrategy::GetStrategy(params.Name()); if(str==NULL) continue; str.ExpertMagic(params.Magic()); str.ExpertSymbol(params.Symbol()); str.Timeframe(params.Timeframe()); str.ExpertName(params.Name()); string name=str.ExpertName(); CXmlElement *xml_params=xStrategy.GetChild("Params"); if(xml_params!=NULL) str.ParseXmlParams(xml_params); CXmlElement *xml_mm=xStrategy.GetChild("MoneyManagment"); if(xml_mm!=NULL) { if(!str.MM.ParseByXml(xml_mm)) { string text="Strategy "+str.ExpertName()+" (Magic: "+(string)str.ExpertMagic()+") load MM from XML failed"; CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text); Log.AddMessage(msg); } } CXmlElement *xml_regim=xStrategy.GetChild("TradeStateStart"); if(xml_regim!=NULL) { string regim=xml_regim.GetText(); if(regim=="BuyAndSell") str.TradeState(TRADE_BUY_AND_SELL); else if(regim=="BuyOnly") str.TradeState(TRADE_BUY_ONLY); else if(regim=="SellOnly") str.TradeState(TRADE_SELL_ONLY); else if(regim=="Stop") str.TradeState(TRADE_STOP); else if(regim=="Wait") str.TradeState(TRADE_WAIT); else if(regim=="NoNewEntry") str.TradeState(TRADE_NO_NEW_ENTRY); else { string text="For strategy "+str.ExpertName()+" (Magic: "+(string)str.ExpertMagic()+ ") set not correctly trade state: "+regim; CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text); Log.AddMessage(msg); } } AddStrategy(str); } if(CheckPointer(params)!=POINTER_INVALID) delete params; }
此方法最有意思的是,使用特殊的静态方法CStrategy::GetStrategy来创建一个策略。策略的名称应当作为参数传入。方法返回与此名称相对应的一个类的实例。方法被设计为静态的,以便在策略对象创建之前就能够使用。GetStrategy在一个独立的头文件中,因为不同交易引擎的其他部分,你将需要不时的编辑它,向其中添加新的策略。如果你希望从XML中加载策略,它的创建过程必须被直接添加到此方法中。该头文件的源码如下:
//+------------------------------------------------------------------+ //| StrategyFactory.mqh | //| Copyright 2015, Vasiliy Sokolov. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2015, Vasiliy Sokolov." #property link "http://www.mql5.com" /* GetStrategy是一个策略制造器。它根据特定的名称创建策略对象。 为了自动化,该方法在于一个独立的文件中。 */ #include <Strategy\Strategy.mqh> #include <Strategy\Samples\MovingAverage.mqh> #include <Strategy\Samples\ChannelSample.mqh> CStrategy *CStrategy::GetStrategy(string name) { if(name=="MovingAverage") return new CMovingAverage(); if(name=="BollingerBands") return new CChannel(); CLog *mlog=CLog::GetLog(); string text="Strategy with name "+name+" not defined in GetStrategy method. Please define strategy in 'StrategyFactory.mqh'"; CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text); mlog.AddMessage(msg); return NULL; }
一旦策略被创建,应该改用<Params>中的参数对其进行初始化。因为每个策略的参数都是不同的,就不可能在交易引擎层面对这些参数进行初始化。相反,策略的基类嫩巩固调用虚拟方法ParseXmlParams。如果策略重写此方法并且正确的解析XML节点的参数列表,就将能够确定其自身需要的参数值。作为一个例子,来看看基于两根移动平均线的CMovingAverage策略(算法已在第一章中描述过了)的ParseXmlParams方法。
//+------------------------------------------------------------------+ //| 基于两个移动平均线的经典策略样例。 //| 如果快线从下往上穿越慢线 //| — 买入,从上往下穿越 — 卖出。 //+------------------------------------------------------------------+ class CMovingAverage : public CStrategy { ... public: virtual bool ParseXmlParams(CXmlElement *params); }; //+------------------------------------------------------------------+ //| 策略参数在重写CStrategy中 //| 方法的方法内部解析 //+------------------------------------------------------------------+ bool CMovingAverage::ParseXmlParams(CXmlElement *params) { bool res=true; for(int i=0; i<params.GetChildCount(); i++) { CXmlElement *param=params.GetChild(i); string name=param.GetName(); if(name=="FastMA") { int fastMA=(int)param.GetText(); if(fastMA == 0) { string text="Parameter 'FastMA' must be a number"; CMessage *msg=new CMessage(MESSAGE_WARNING,SOURCE,text); Log.AddMessage(msg); res=false; } else FastMA.MaPeriod(fastMA); } else if(name=="SlowMA") { int slowMA=(int)param.GetText(); if(slowMA == 0) { string text="Parameter 'SlowMA' must be a number"; CMessage *msg=new CMessage(MESSAGE_WARNING,SOURCE,text); Log.AddMessage(msg); res=false; } else SlowMA.MaPeriod(slowMA); } else if(name=="Shift") { FastMA.MaShift((int)param.GetText()); SlowMA.MaShift((int)param.GetText()); } else if(name=="Method") { string smethod=param.GetText(); ENUM_MA_METHOD method=MODE_SMA; if(smethod== "MODE_SMA") method = MODE_SMA; else if(smethod=="MODE_EMA") method=MODE_EMA; else if(smethod=="MODE_SMMA") method=MODE_SMMA; else if(smethod=="MODE_LWMA") method=MODE_LWMA; else { string text="Parameter 'Method' must be type of ENUM_MA_METHOD"; CMessage *msg=new CMessage(MESSAGE_WARNING,SOURCE,text); Log.AddMessage(msg); res=false; } FastMA.MaMethod(method); SlowMA.MaMethod(method); } else if(name=="AppliedPrice") { string price=param.GetText(); ENUM_APPLIED_PRICE a_price=PRICE_CLOSE; if(price=="PRICE_CLOSE") a_price=PRICE_CLOSE; else if(price=="PRICE_OPEN") a_price=PRICE_OPEN; else if(price=="PRICE_HIGH") a_price=PRICE_HIGH; else if(price=="PRICE_LOW") a_price=PRICE_LOW; else if(price=="PRICE_MEDIAN") a_price=PRICE_MEDIAN; else if(price=="PRICE_TYPICAL") a_price=PRICE_TYPICAL; else if(price=="PRICE_WEIGHTED") a_price=PRICE_WEIGHTED; else { string text="Parameter 'AppliedPrice' must be type of ENUM_APPLIED_PRICE"; CMessage *msg=new CMessage(MESSAGE_WARNING,SOURCE,text); Log.AddMessage(msg); res=false; } FastMA.AppliedPrice(a_price); SlowMA.AppliedPrice(a_price); } } return res; }
该策略的细节在本系列的第三篇文章中消息描述过了,其中还包括自定义策略的开发。
使用从文件中创建策略的机制,可以一次性配置一组策略,然后每次从一个文件中加载它。你甚至可以更进一步,写一个能自行优化的算法,将其最优的参数集保存到一个XML文件中。交易引擎在开始运行时读取该文件,并基于此形成一些列策略。
使用自定义面板管理策略
从用户角度来看,应能方便的通过一个特别的自定义面板来控制策略。此面板在EA加载后显示在一个图表上,并将允许对每个交易算法执行简单的操作:
- 改变策略的交易模式;
- 买入或卖出期望的头寸大小,而不是由策略决定。
如果EA由于某些原因没有正确执行恰当的操作,后面一个选项就非常有用了,并且你得将其状态和当前市场情况进行同步。
创建自定义面板和对话框类的描述超出了本文讨论的范围,需要另起一篇文章来描述。我们将仅仅描述和面板连接相关的基本概念。
EA控制面板在一个独立的类CPanel中实现,类中包括各种控件,诸如列表,按钮和文本标签。用于创建GUI的所有类都在<数据文件夹>\MQL5\Include\Panel中。为了确保面板的操作,有必要在EA的mq5文件中直接处理OnChartEvent事件。图表事件处理函数在CStrategyList类中,因此可以在OnChartEvent中调用此函数:
CStrategyList Manager; ... void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { Manager.OnChartEvent(id,lparam,dparam,sparam); }
CStrategyList中的这些事件处理函数将它们直接发送到面板中。
点击面板中的任何一个按钮,定义了执行何种操作并执行之。例如,如果我们从列表中选择了一个策略,当前策略的索引值将等于选中的这个,然后你可以进行后续交易操作了。比如,你可以通过从下拉列表中选择恰当的策略模式选项,来改变选中策略的交易模式。
图 2. 选定策略的模式列表
代表选定策略的买入和卖出执行起来是一样的。策略的指针调用CStrategy基类的Buy和Sell方法。交易量由传入的参数确定。在这种情况下,执行中的magic数字对应于策略的编号,因此无法区分手动交易还是EA交易。
需要注意的是EA的交易逻辑,所有用户的开仓以一般模式被EA所保持。它管理这类头寸如同管理其自动开仓的头寸一样。
以组交易的EA
我们可以创建一个策略组合。策略必须包含解析XML参数的方法,例如,我们要重写 ParseXmlParams 方法。还需要向CStrategy::GetStrategy方法中添加创建正确策略类型的功能。最终我们需要创建一个XML文件,含有策略列表和对应的参数。之后CStrategyList类将创建策略实例并将它们添加到策略列表中。此后自定义面板将显示这些策略。
让我们创建一个包含上述EA的策略组合。解析CMovingAverage和CChannel策略XML设置的例子在3.5和4.3节中提供。
用于创建两个策略的CStrategy::GetStrategy方法的方法体如下:
//+------------------------------------------------------------------+ //| StrategyFactory.mqh | //| Copyright 2015, Vasiliy Sokolov. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2015, Vasiliy Sokolov." #property link "http://www.mql5.com" /* GetStrategy是一个策略制造器。它根据特定的名称创建策略对象。 为了自动化,该方法在于一个独立的文件中。 */ #include <Strategy\Strategy.mqh> #include <Strategy\Samples\MovingAverage.mqh> #include <Strategy\Samples\ChannelSample.mqh> CStrategy *CStrategy::GetStrategy(string name) { if(name=="MovingAverage") return new CMovingAverage(); if(name=="BollingerBands") return new CChannel(); CLog *mlog=CLog::GetLog(); string text="Strategy with name "+name+" not defined in GetStrategy method. Please define strategy in 'StrategyFactory.mqh'"; CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text); mlog.AddMessage(msg); return NULL; }
最后是重写用于EA全名的函数。为 CMovingAverage 策略进行重写:
//+------------------------------------------------------------------+ //| EA的全称 //+------------------------------------------------------------------+ string CMovingAverage::ExpertNameFull(void) { string name=ExpertName(); name += "[" + ExpertSymbol(); name += "-" + StringSubstr(EnumToString(Timeframe()), 7); name += "-" + (string)FastMA.MaPeriod(); name += "-" + (string)SlowMA.MaPeriod(); name += "-" + StringSubstr(EnumToString(SlowMA.MaMethod()), 5); name += "]"; return name; }
现在一切就绪,可以创建策略组合了。我们的组合将包含四个交易系统。每一个都在其自身加载的货币对上进行交易。两个处理基于移动平均,另外两个基于布林带。这些策略更为详细的描述在先前的文章:“通用智能交易系统:自定义策略和辅助交易类(第三章)”中介绍过了。
我们的XML组合如下:
<Global> <Strategies> <Strategy Name="MovingAverage" Magic="100" Timeframe="PERIOD_M1" Symbol="Si"> <TradeStateStart>Stop</TradeStateStart> <Params> <FastMA>1</FastMA> <SlowMA>3</SlowMA> <Shift>0</Shift> <Method>MODE_SMA</Method> <AppliedPrice>PRICE_CLOSE</AppliedPrice> </Params> </Strategy> <Strategy Name="MovingAverage" Magic="101" Timeframe="PERIOD_M5" Symbol="SBRF"> <TradeStateStart>BuyAndSell</TradeStateStart> <Params> <FastMA>15</FastMA> <SlowMA>21</SlowMA> <Shift>0</Shift> <Method>MODE_SMA</Method> <AppliedPrice>PRICE_CLOSE</AppliedPrice> </Params> </Strategy> <Strategy Name="BollingerBands" Magic="102" Timeframe="PERIOD_M15" Symbol="GAZR"> <TradeStateStart>BuyAndSell</TradeStateStart> <Params> <Period>30</Period> <StdDev>1.5</StdDev> </Params> </Strategy> <Strategy Name="BollingerBands" Magic="103" Timeframe="PERIOD_M30" Symbol="ED"> <TradeStateStart>BuyAndSell</TradeStateStart> <Params> <Period>20</Period> <StdDev>2.0</StdDev> </Params> </Strategy> </Strategies> </Global>
此文件Strategies.xml应该保存在MetaTrader平台的公用数据文件夹中。
这里是创建一个EA的mq5模块源代码:
//+------------------------------------------------------------------+ //| Expert.mq5 | //| Copyright 2015, Vasiliy Sokolov. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2015, Vasiliy Sokolov." #property link "http://www.mql5.com" #property version "1.00" #include <Strategy\StrategiesList.mqh> CStrategyList Manager; //+------------------------------------------------------------------+ //| EA初始化函数 //+------------------------------------------------------------------+ int OnInit() { Manager.LoadStrategiesFromXML(StrategiesXMLFile,LoadOnlyCurrentSymbol); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA反初始化函数 //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ //| EA的tick函数 //+------------------------------------------------------------------+ void OnTick() { Manager.OnTick(); } //+------------------------------------------------------------------+ //| BookEvent 函数 //+------------------------------------------------------------------+ void OnBookEvent(const string &symbol) { Manager.OnBookEvent(symbol); } void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { Manager.OnChartEvent(id,lparam,dparam,sparam); }
自定义变量StrategiesXMLFile 和 LoadOnlyCurrentSymbol在CStrategyList类中定义。它们在这个类中使用,用于指定加载的策略列表以及模式(是否仅允许加载同EA所运行图表名称一致的策略)。还要注意一些事件,诸如 OnBookEvent 和 OnTimer,没有被使用。这意味着它们不会被用于自定义策略。
编译应该能够成功。之后,EA(项目中的名称为 Agent.ex5)就可以使用了。让我们在图标上运行它。在此之前,我们必须确保所有被用到的货币对在 MetaTrader Market Watch 中可用。成功启动之后,EA的图标会出现在图表右上角。另一个按钮被添加到图表的左上角:它用于最大化自定义面板。如果我们在面板上选择EA(Agent)列表,将打开四个EA列表:
图 3. 已加载EA的列表
截图显示了由我们的 Strategies.xml文件创建的EA列表。过一会儿之后,策略开始交易 — 每一个策略独立运行于自己的货币对上。
在策略测试器中分析EA的效果
在生成一个策略组合之后,我们可以在策略测试器中对其运行效果进行测试。无需额外的操作,因为XML策略列表位于全局数据文件夹下,策略测试器能够访问。当在其中加载Agent.ex5 EA 模块后,所需的货币对会被自动加载。每个EA会根据其自身的交易逻辑运行,并且将独立绘制其含有的指标集。下面的视频展示了在四个不同的标的上测试策略组合的例子:
在策略测试器中,基于CStrategy的策略仿真同使用这些策略进行实际交易类似。可视化测试选项使你能够方便的检查策略进场和出场的准确性。
总结
我们已经考虑了可以创建任意交易策略集的算法。有了这些策略集或策略组合,当在同一个执行模块中管理多个交易策略时,你可以灵活有效的控制交易过程。这种算法对于使用多货币对作为交易对象的策略特别有用。使用所提出的方法,创建类似的交易算法就像开发传统交易策略一样简单。