请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3364693 新帖:14

通用EA:自定义策略和辅助交易类(第三章)

随心所致发表于:4 月 17 日 16:47回复(1)

目录

  • 简介
  • 日志,CMessage 和 CLog 类,单例模式
  • 使用MetaTrader 4的索引访问报价数据
  • 使用面向对象的指标
  • EA重载的方法
  • 使用两个移动平均线作为交易信号的EA样例
  • 基于布林带突破的EA样例
  • 加载自定义策略到交易引擎中
  • 总结

 

简介

在本文中,我们继续讨论CStrategy交易引擎。这里是强两部分内容的简介。在第一篇文章通用智能交易系统:交易策略的模式中,我们已经详细讨论过根据时间和星期几来配置EA逻辑的交易模式。在第二篇文章通用智能交易系统:事件模型和交易策略原型中,我们分析了基于集中事件处理的事件模型,以及构建自定义EA的CStrategy基类的主要算法。

在本系列文章的第三篇,我们将详细描述基于CStrategy交易引擎和一些EA开发所需的辅助算法的EA样例。特别的请注意日志记录程序。尽管它是单纯扮演支持的角色,但是日志记录对于任何复杂的系统来说都是非常重要的。一个好的日志记录器可以让你快速的理解错误的原因并定位它发生的位置。这个记录记录器使用一种特殊的编程技术编写,称为单例模式。不仅对于那些组织交易过程的人,而且对于那些创建算法用于执行非标准任务的人来说,它都是非常有意思的。

并且,本文描述了使你能够通过便捷且直观的索引来访问市场数据的算法。事实上,通过索引诸如Close[1] 和 High[0] 来访问数据是MetaTrader 4非常常用的方式。因此如果可以,为何不在MetaTrader 5继续使用呢?本文解释如何实现它,并且信息描述实现上述想法的算法。

我想用我上一篇文章的说法来作为本文的总结。CStrategy交易引擎和它所有的算法是一个相当复杂的集合。然而,并不需要完全深入的理解其执行原理。你只要知道一般原理及交易引擎的功能就行了。因此,如果本文的任一部分你不清楚的话可以略过。这是面向对象方法的基本原则之一:你可以使用一个复杂的系统而无需知道它的内部结构。

 

日志,CMessage 和 CLog 类,单例模式

记录日志是传统的复制功能之一。一般来说,简单的应用使用Print 或者 printf 函数打印一个错误消息到 MetaTrader 5 终端。

...
double closes[];
if(CopyClose(Symbol(), Period(), 0, 100, closes) < 100)
   printf("Not enough data.");
...

然而,这种简单的方法对于理解有成千上万行代码的复杂程序来说还不够。因此,对于这类任务的最好解决办法是开发一个特殊的日志记录模块 — CLog类。

最显而易见的日志记录方法是AddMessage()。例如,如果Log是我们的CLog的对象,我们可以创建如下表达式:

Log.AddMessage("注意!接收到的柱形数量不足”);

然而,对于调试程序来说上述提醒的有用信息还太少。当消息被创建时你怎么知道呢?哪个函数创建了它?你如何知道其中包含了哪些重要的信息?为了避免这些问题,我们要扩展这个消息通知。除了文本,每个消息还应包含以下属性:

  • 创建时间
  • 消息来源
  • 消息类型(信息,警告,报错)

如果我们的消息还包含一些额外的详细信息,那将是非常有用的:

  • 系统错误 ID
  • 交易错误 ID (如果交易行为发生)
  • 消息创建时的交易服务器时间

所有这些信息都可以方便的被结合到CMessage类中。既然我们的消息是一个类,你可以简单的添加更多的日志处理数据和方法到类中。这里是累的声明:

//+------------------------------------------------------------------+
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>

#define UNKNOW_SOURCE "unknown"     // 已知消息源
//+------------------------------------------------------------------+
//| 消息类型                        
//+------------------------------------------------------------------+
enum ENUM_MESSAGE_TYPE
  {
   MESSAGE_INFO,                    // 信息性消息
   MESSAGE_WARNING,                 // 警告性消息
   MESSAGE_ERROR                    // 报错性消息
  };
//+------------------------------------------------------------------+
//|  传递入日志类的消息
//+------------------------------------------------------------------+
class CMessage : public CObject
  {
private:
   ENUM_MESSAGE_TYPE m_type;               // 消息类型
   string            m_source;             // 消息来源
   string            m_text;               // 消息文本
   int               m_system_error_id;    // 为系统错误创建一个ID
   int               m_retcode;            // 包含一个交易服务器的返回码
   datetime          m_server_time;        // 消息创建时的交易服务器时间
   datetime          m_local_time;         // 消息创建时的本地时间
   void              Init(ENUM_MESSAGE_TYPE type,string source,string text);
public:
                     CMessage(void);
                     CMessage(ENUM_MESSAGE_TYPE type);
                     CMessage(ENUM_MESSAGE_TYPE type,string source,string text);
   void              Type(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE Type(void);
   void              Source(string source);
   string            Source(void);
   void              Text(string text);
   string            Text(void);
   datetime          TimeServer(void);
   datetime          TimeLocal();
   void              SystemErrorID(int error);
   int               SystemErrorID();
   void              Retcode(int retcode);
   int               Retcode(void);
   string            ToConsoleType(void);
   string            ToCSVType(void);
  };

首先所有的头部都包含 ENUM_MESSAGE_TYPE。它定义被创建的消息的类型。消息可以是信息型的(MESSAGE_INFO),告警型的(MESSAGE_WARNING)以及报错通知型(MESSAGE_ERROR)的。

类中有各种 Get/Set 方法来设置或者读取不同类型的消息。为了使消息在一行中创建,CMessage提供了一个相应的可重载的构造函数,使用消息文本,类型和来源作为参数来调用。例如,我们要在OnTick函数中创建一则告警信息,来通知用户所加载的数据太少,可以使用下面的方式来实现:

void OnTick(void)
  {
   double closes[];
   if(CopyClose(Symbol(),Period(),0,100,closes)<100)
      CMessage message=new CMessage(MESSAGE_WARNING,__FUNCTION__,"Not enough data");
  }

这则消息比之前的那个包含更多的信息。除了消息本身,它还包含调用它的函数名称以及消息类型。另外,我们的消息中还有你无需在创建时填充的数据。例如,信息对象包含消息创建的时间以及交易报错代码(如果有的话)。

现在是时候来考虑CLog类了。这个类被用作存储CMessage消息。一个有趣的功能之一是能够使用SendNotification函数向移动终端推送通知。当实时监控EA不可能时,这是非常有用的功能。实际上,我们能够推送通知告诉用户哪里出错了。

日志的具体特征是对于程序的所有部分,它必须是一个独立的处理过程。如果每个函数或者类都有它自己的日志机制那会非常奇怪。因此,CLog类使用一种称为Singleton的特殊编程模式。这种模式确保只有一个某个类型的对象的副本。例如,程序使用两个指针,每个都是CLog类型对象的引用,那么它们指向的是用一个对象。一个对象实际上在类的私有方法中被创建和删除。

让我们来考虑这个类的名称以及实现 Singleton 模式的方法:

//+------------------------------------------------------------------+
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "Message.mqh"

//+------------------------------------------------------------------+
//| 实现Singleton消息日志的类
//+------------------------------------------------------------------+
class CLog
{
private:
   static CLog*      m_log;                     // 全局静态变量指针
   CArrayObj         m_messages;                // 保存消息的列表
   bool              m_terminal_enable;         //如果你想要打印接受到的消息到交易终端,则设为True
   bool              m_push_enable;             //如果为True,发送推送通知
   ENUM_MESSAGE_TYPE m_push_priority;           // 包含在终端窗口中显示的消息的特定优先级
   ENUM_MESSAGE_TYPE m_terminal_priority;       // 包含向移动终端发送推送消息的特定优先级
   bool              m_recursive;               //标记函数递归调用的标识
   bool              SendPush(CMessage* msg);
   void              CheckMessage(CMessage* msg);
                     CLog(void);                // 私有构造函数
   string            GetName(void);
   void              DeleteOldLogs(int day_history = 30);
   void              DeleteOldLog(string file_name, int day_history);
                     ~CLog(void){;}
public:
   static CLog*      GetLog(void);              // 获取静态对象的方法
   bool              AddMessage(CMessage* msg);
   void              Clear(void);
   bool              Save(string path);
   CMessage*         MessageAt(int index)const;
   int               Total(void);
   void              TerminalEnable(bool enable);
   bool              TerminalEnable(void);
   void              PushEnable(bool enable);
   bool              PushEnable(void);
   void              PushPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE PushPriority(void);
   void              TerminalPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE TerminalPriority(void);
   bool              SaveToFile(void);
   static bool       DeleteLog(void);
};
CLog* CLog::m_log;

CLog类存储了一个指向它自身静态对象的指针,作为一个私有成员。这似乎是一个奇怪的编程结构,但它是有意义的。唯一的类构造函数是私有的,不能被调用。不调用构造函数,我们可以使用GetLog方法:

//+------------------------------------------------------------------+
//| 返回logger对象
//+------------------------------------------------------------------+
static CLog* CLog::GetLog()
{
   if(CheckPointer(m_log) == POINTER_INVALID)
      m_log = new CLog();
   return m_log;
}

它检查是否静态指针指向现有CLog对象,如果是,返回一个引用。否则,它会创建一个新的对象并且会将其同内部m_log指针关联。也就是说对象只创建一次。在对GetLog方法进一步调用时,之前创建的对象将被返回。

删除对象也只执行一次。通过使用DeleteLog方法完成:

//+------------------------------------------------------------------+
//| 删除日志对象
//+------------------------------------------------------------------+
bool CLog::DeleteLog(void)
{
   bool res = CheckPointer(m_log) != POINTER_INVALID;
   if(res)
      delete m_log;
   return res;
}

如果m_log存在,它将会被删除并返回true。

所描述的日志系统看上去复杂,但是它所支持的特性却令人印象深刻。例如,你可以对消息进行分类,或者将它们作为推送通知发送。用户最终决定是否使用此系统。它在独立的Message.mqh 和 Logs.mqh,中实现,所以你可以独立于所描述的项目使用或者在其中使用。

 

使用MetaTrader 4的索引访问报价数据

相对于MetaTrader 4,MetaTrader 5的主要变化之一是访问报价和指标数据的模型。例如,如果你需要找出当前柱形的收盘价,在MetaTrader 4 你可以添加如下的程序:

double close = Close[0];

也就是说,你可以通过恰当的时间序列索引直接访问数据。在 MetaTrader 5中则需要更多的步骤来找出当前柱形的收盘价:

  1. 定义一个接收数组来复制所需的报价。
  2. 使用Copy* 函数组(访问时间序列和指标数据的函数)之一来复制所需的报价。
  3. 引用已复制数组的索引。

在MetaTrader 5中找到当前柱形的收盘价,需要下面的这些操作:

double closes[];
double close = 0.0;
if(CopyClose(Symbol(), Period(), 0, 1, closes))
   close = closes[0];
else
   printf("Failed to copy the close price.");

对于数据的访问相比 MetaTrader 4 来说更为复杂。然而,这种方式使得数据访问方式通用化:使用同一个统一的接口和机制来访问不同货币对和指标数据。

虽然这不是经常要用到的规则。通常我们只需要获取当前货币对的最新数据。这可以是柱形的开盘和收盘价,以及最高和最低价。不管怎么说,使用MetaTrader 4中的数据访问模型还是很方便的。因为MQL5面向对象的特性,可以创建带有索引的特殊类,同MetaTrader 4中的方式一样来访问交易数据。例如,可以在MetaTrader 5中这样获取收盘价:

double close = Close[0];

我们应该为收盘价添加如下封装:

//+------------------------------------------------------------------+
//| 访问柱形的收盘价格|
//+------------------------------------------------------------------+
class CClose : public CSeries
  {
public:
   double operator[](int index)
     {
      double value[];
      if(CopyClose(m_symbol, m_timeframe, index, 1, value) == 0)return 0.0;
      return value[0];
     }
  };

对于其他时间序列也需要写同样的程序,包括时间、交易量以及开盘价最高价和最低价。当然,在某些情况下,代码可能要比使用Copy*系列函数复制需要的报价数组来的更加慢些。然而,正如我们之间所述,往往我们只需要访问最新的元素,因为所有之前的元素都在移动的窗口中考虑了。

这个简单的类集包含在Series.mqh中。它提供了一个类似MetaTrader 4中的简便接口来访问报价数据,并在交易引擎中被使用。

这些类的一个显著特征是它们是独立于平台的。例如,在 MetaTrader 5中,一个EA可以调用这些类之一,并“认为”可以通过它直接引用报价数据。这种访问方式在MetaTrader 4中也起作用,不过不是采用特殊封装而是直接访问系统的时间序列,诸如 Open,High,Low或者 Close。

 

使用面向对象的指标

几乎每个指标都有一些用于配置的参数。在MetaTrader 5中操作指标和操作报价类似,唯一的区别就是需要在复制指标数据之前创建所谓的指标句柄,例如,一个特殊的指向某些含有计算值的MetaTrader 5内部对象指针。指标的参数在句柄创建的时候被设置。如果因为某些原因你需要编辑指标的其中一个参数,你应该删除原来的句柄并创建一个带有更新后参数的新句柄。指标参数应该保存在外部,例如,在EA的变量中。

因此,大多数有关指标的操作都会被传递到EA中。这并不总是很方便。让我们考虑一个简单的例子:使用MA交叉作为交易信号的EA。它很简单,移动平均线指标有六个参数需要设置:

  1. 计算移动平均的货币对
  2. 图表时间框架或周期
  3. 平滑周期
  4. 移动平均类型(简单,指数,加权等。)
  5. 指标的柱形偏移量
  6. 应用的价格(柱形的OHLC价格之一或者另一个指标计算值的缓存)

因此,如果我们想写一个交易两个移动平均线交叉的EA,并使用MA完整的参数设置,将会包含12个参数 — 快线六个参数、慢线六个参数。另外,如果用户改变时间框架或者EA正运行于的图表货币对,所被使用指标句柄也需要重新初始化。

为了使EA和指标的耦合度降低,我们应该使用面向对象的指标。通过使用面向对象的指标类,我们可以写出如下结构体:

CMovingAverageExp MAExpert;     // 创建基于两均线交叉信号进行交易的EA
//+------------------------------------------------------------------+
//| EA初始化函数             
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 配置用于EA的快速移动平均线
   MAExpert.FastMA.Symbol("EURUSD");
   MAExpert.FastMA.Symbol(PERIOD_M10);
   MAExpert.FastMA.Period(13);
   MAExpert.FastMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.FastMA.MaShift(1);
//--- 配置用于EA的慢速移动平均线
   MAExpert.SlowMA.Symbol("EURUSD");
   MAExpert.SlowMA.Symbol(PERIOD_M15);
   MAExpert.SlowMA.Period(15);
   MAExpert.SlowMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.SlowMA.MaShift(1);

   return(INIT_SUCCEEDED);
  }

终端用户仅通过EA来设置指标的参数。EA将紧紧从它们中读取数据。

使用指标对象还有一个好处。面向对象的指标隐藏了它们的实现。这就意味着它们可以计算自身的值或者通过调用适当的句柄。假如使用嵌套计算的指标并且对速度有较高要求,建议直接将指标添加到EA中。归功于面向对象的方法,无需重写EA就能实现。你只需要在合适的类中计算指标值,而无需使用句柄。

为了说明上面所述,下面就是CIndMovingAverage类的源代码,基于iMA指标:

//+------------------------------------------------------------------+
//|                                                MovingAverage.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Message.mqh>
#include <Strategy\Logs.mqh>
//+------------------------------------------------------------------+
//| 定义                                             
//+------------------------------------------------------------------+
class CIndMovingAverage
  {
private:
   int               m_ma_handle;         // 指标句柄
   ENUM_TIMEFRAMES   m_timeframe;         // 时间框架
   int               m_ma_period;         // 周期
   int               m_ma_shift;          // 位移
   string            m_symbol;            // 货币对名称
   ENUM_MA_METHOD    m_ma_method;         // 移动平均算法
   uint              m_applied_price;     // 你打算在何种价格上计算移动平均
                                          // ENUM_APPLIED_PRICE定义的枚举值之一
   CLog*             m_log;               // Logging
   void              Init(void);
public:
                     CIndMovingAverage(void);

/*参数*/
   void              Timeframe(ENUM_TIMEFRAMES timeframe);
   void              MaPeriod(int ma_period);
   void              MaShift(int ma_shift);
   void              MaMethod(ENUM_MA_METHOD method);
   void              AppliedPrice(int source);
   void              Symbol(string symbol);

   ENUM_TIMEFRAMES   Timeframe(void);
   int               MaPeriod(void);
   int               MaShift(void);
   ENUM_MA_METHOD    MaMethod(void);
   uint              AppliedPrice(void);
   string            Symbol(void);

/*输出值*/
   double            OutValue(int index);
  };
//+------------------------------------------------------------------+
//| 默认构造函数                  
//+------------------------------------------------------------------+
CIndMovingAverage::CIndMovingAverage(void) : m_ma_handle(INVALID_HANDLE),
                                             m_timeframe(PERIOD_CURRENT),
                                             m_ma_period(12),
                                             m_ma_shift(0),
                                             m_ma_method(MODE_SMA),
                                             m_applied_price(PRICE_CLOSE)
  {
   m_log=CLog::GetLog();
  }
//+------------------------------------------------------------------+
//| 初始化     
//+------------------------------------------------------------------+
CIndMovingAverage::Init(void)
  {
   if(m_ma_handle!=INVALID_HANDLE)
     {
      bool res=IndicatorRelease(m_ma_handle);
      if(!res)
        {
         string text="Realise iMA indicator failed. Error ID: "+(string)GetLastError();
         CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
         m_log.AddMessage(msg);
        }
     }
   m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price);
   if(m_ma_handle==INVALID_HANDLE)
     {
      string params="(Period:"+(string)m_ma_period+", Shift: "+(string)m_ma_shift+
                    ", MA Method:"+EnumToString(m_ma_method)+")";
      string text="Create iMA indicator failed"+params+". Error ID: "+(string)GetLastError();
      CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text);
      m_log.AddMessage(msg);
     }
  }
//+------------------------------------------------------------------+
//| 设置时间框架
//+------------------------------------------------------------------+
void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| 返回当前时间框架
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(void)
  {
   return m_timeframe;
  }
//+------------------------------------------------------------------+
//| 设置移动平均周期          
//+------------------------------------------------------------------+
void CIndMovingAverage::MaPeriod(int ma_period)
  {
   m_ma_period=ma_period;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| 返回当前移动平均的平滑周期
//+------------------------------------------------------------------+
int CIndMovingAverage::MaPeriod(void)
  {
   return m_ma_period;
  }
//+------------------------------------------------------------------+
//| 设置移动平均类型                 
//+------------------------------------------------------------------+
void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method)
  {
   m_ma_method=method;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| 返回移动平均类型           
//+------------------------------------------------------------------+
ENUM_MA_METHOD CIndMovingAverage::MaMethod(void)
  {
   return m_ma_method;
  }
//+------------------------------------------------------------------+
//| 返回移动平均偏移           
//+------------------------------------------------------------------+
int CIndMovingAverage::MaShift(void)
  {
   return m_ma_shift;
  }
//+------------------------------------------------------------------+
//| 设置移动平均偏移                  
//+------------------------------------------------------------------+
void CIndMovingAverage::MaShift(int ma_shift)
  {
   m_ma_shift=ma_shift;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| 设置用于计算MA的价格类型   
//+------------------------------------------------------------------+
void CIndMovingAverage::AppliedPrice(int price)
  {
   m_applied_price = price;
   if(m_ma_handle != INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| 返回用于计算MA的价格类型   
//+------------------------------------------------------------------+
uint CIndMovingAverage::AppliedPrice(void)
  {
   return m_applied_price;
  }
//+------------------------------------------------------------------+
//| 设置要计算指标的货币对
//+------------------------------------------------------------------+
void CIndMovingAverage::Symbol(string symbol)
  {
   m_symbol=symbol;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//|  返回指标计算的货币对
//+------------------------------------------------------------------+
string CIndMovingAverage::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| 返回索引为“index”的MA值
//+------------------------------------------------------------------+
double CIndMovingAverage::OutValue(int index)
  {
   if(m_ma_handle==INVALID_HANDLE)
      Init();
   double values[];
   if(CopyBuffer(m_ma_handle,0,index,1,values))
      return values[0];
   return EMPTY_VALUE;
  }

这个类非常简单。它的主要的任务是如果参数之一有变化,重新初始化指标,同时通过index返回计算的值。用Init方法重新初始化句柄,由OutValue返回所需要的值。Out前缀的方法返回指标值之一。当在诸如 MetaEditor 这类提供智能参数替换的编辑器中编程时,方便了搜索所需的函数。

交易引擎包中包含了许多面向对象的指标。这将帮助你理解它们是如何运作的,并创建你自己的面向对象的经典指标。自定义EA开发的部分将揭示它们的运行原理。

 

EA重载的方法

在第一篇文章通用智能交易系统:交易策略的模式中,我们已经详细考虑了策略的交易模式以及需被重载的主要方法。现在,是做练习的时候了。

每一个使用CStrategy引擎创建的EA必须重载用于设置EA属性和行为的虚拟方法。我们将以三列表的形式列出所有需重载的方法。狩猎包含虚拟方法的名称,第二列是需要追踪的事件或执行的行为。第三列包含使用方法的目标描述。下面就是表格:

虚拟方法 事件/行为 目标
OnSymbolChanged 当交易货币对名称改变时被调用 当你改变交易标的,EA的指标需要被重新初始化。此事件允许执行EA指标的重新初始化。
OnTimeframeChanged 改变运行时间框架 当你改变运行时间框架,EA的指标应该被重新初始化。此事件允许执行EA指标的重新初始化。
ParseXmlParams 解析通过XML文件加载的策略的自定义参数 策略应能识别传递入方法的XML参数,并进行相应的配置。
ExpertNameFull 返回EA的完整名称 有策略名组成的完整的EA名称,一般是一个唯一的策略参数集。一个策略的实例必须独立决定它的全名。这个名称也在可视化面板的下拉列表中使用。
OnTradeTransaction 如果交易事件发生 某些策略需要分析操作引起的交易事件。这传递一个交易事件到EA中并分析它。
InitBuy 发起买入操作 必须要重载的基本方法之一。如果合适的交易条件形成,你应执行买入操作。
InitSell 发起卖出操作 必须要重载的基本方法之一。如果合适的交易条件形成,在此方法中你应执行卖出操作。
SupportBuy 管理一个先前建立的多头头寸 一个未平仓的多头头寸需要被管理。例如,你应该设置止损或者在出场信号发生时平仓。所有这些步骤都必须在这个方法中执行。
SupportSell 管理一个先前建立的空头头寸 一个未平仓空头头寸需要被管理。例如,你应该设置止损或者在出场信号发生时平仓。所有这些步骤都必须在这个方法中执行。

 表 1. 虚拟方法和它们的用途

你必须重载的最重要的方法是InitBuyInitSellSupportBuySupportSell。它们列在表中了。如果你忘了重载,例如,InitBuy,那么自定义策略就无法买入开仓。如果你不重写Support方法之一,那么一个未平仓头寸可能永远无法平仓。因此,当创建一个EA时,请注意重写这些方法。

如果你想交易引擎从一个XML文件自动加载策略,并根据文件中提供的设置配置参数,你也将需要重写parsexmlparams方法。在这种方法中,一个策略应确定传递给它的参数,并且了解如何根据这些参数改变它自己的设置。使用XML参数将在此系列的第四部分:通用EA:组合交易及管理策略组合(第四章)中详细讨论。一个重写ParseXmlParams的例子将包含在基于布林带的策略列表中。

 

使用两个移动平均线作为交易信号的EA样例

现在是时候用CStrategy类来创建我们的第一个EA了。为了使源代码简单、紧凑,我们将不在其中使用日志记录功能。让我们简要描述下我们EA中需要执行的操作:

  • 当切换时间框架和货币对时,通过重写OnSymbolChanged 和 OnTimeframeChanged方法,改变快速和慢速移动平均的设置。
  • 重写InitBuy,InitSell,SupportBuy 和 SupportSell方法。在这些方法中定义EA的交易逻辑(头寸的建立和管理规则)。

EA剩下的工作应该交由EA的交易引擎和指标来完成。这里是EA的源码:

//+------------------------------------------------------------------+
//|                                                      Samples.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>
//+------------------------------------------------------------------+
//| 基于两个移动平均线的经典策略样例。
//| 如果快线从下往上穿越慢线
//| — 买入,从上往下穿越 — 卖出。          
//+------------------------------------------------------------------+
class CMovingAverage : public CStrategy
  {
private:
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
public:
   CIndMovingAverage FastMA;        // 快速移动平均
   CIndMovingAverage SlowMA;        // 慢速移动平均
                     CMovingAverage(void);
   virtual string    ExpertNameFull(void);
  };
//+------------------------------------------------------------------+
//| 初始化     
//+------------------------------------------------------------------+
CMovingAverage::CMovingAverage(void)
  {
  }
//+------------------------------------------------------------------+
//| 响应货币对变化
//+------------------------------------------------------------------+
void CMovingAverage::OnSymbolChanged(string new_symbol)
  {
   FastMA.Symbol(new_symbol);
   SlowMA.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| 响应时间框架变化
//+------------------------------------------------------------------+
void CMovingAverage::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   FastMA.Timeframe(new_tf);
   SlowMA.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| 当快速MA在慢速MA之下卖出     
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // 仅处理所需的事件
   if(positions.open_buy > 0) return;                    // 如果已经存在至少一个未平仓的买单,无需再买入
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // 如果没有买单,检查快速MA是否在慢速之上:
      Trade.Buy(MM.GetLotFixed(), ExpertSymbol(), "");   // 如果是,买入。
  }
//+------------------------------------------------------------------+
//| 当快速MA在慢速之下,
//| 平仓多头头寸        
//+------------------------------------------------------------------+
void CMovingAverage::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // 仅处理所需的事件
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // 如果快速MA在慢速之下 
      pos.CloseAtMarket("Exit by cross over");           // 平仓
  }
//+------------------------------------------------------------------+
//| 当快速MA在慢速之下我们卖出     
//+------------------------------------------------------------------+
void CMovingAverage::InitSell(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // 仅处理所需的事件
   if(positions.open_sell > 0) return;                   // 如果已经有空头头寸,无需再次卖出
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // 如果没有未平仓买单,检查快速MA是否在慢速之下
      Trade.Sell(1.0, ExpertSymbol(), "");               // 如果是,卖出。
  }
//+------------------------------------------------------------------+
//| 当快速MA在慢速之上
//| 平仓空头头寸                                
//+------------------------------------------------------------------+
void CMovingAverage::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // 仅处理所需的事件
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // 如果快速MA在慢速之上
      pos.CloseAtMarket("Exit by cross under");          // 平仓
  }
//+------------------------------------------------------------------+
//| 过滤到来的事件如果传入的事件没被 
//| 策略处理,返回false;如果已处理,   
//| 返回 true                  
//+------------------------------------------------------------------+
bool CMovingAverage::IsTrackEvents(const MarketEvent &event)
  {
//--- 我们仅处理EA所运行货币对和时间框架上的新到来的柱形
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }

上面的代码非常易于理解。然而,我们需要澄清一些地方。CStrategy 交易引擎根据发生的事件调用方法 InitBuy,InitSell,SupportBuy 和 SuportSell(交易逻辑的方法),事件如市场深度变化,新报价的到来或者时间变化。通常来说,这些方法经常会被调用。然而,一个EA使用的事件种类非常有限。这个EA仅使用新柱形形成事件。因此,所有调用交易逻辑方法的其他事件都应被忽略。IsTrackEvents方法正是用作此功能的。它检查传入的事件是否是被追踪的事件,如果是 — 返回true,否则返回false。

positions结构体被用作为辅助变量。它包含属于当前策略的多单和空单的数量。CStrategy引擎会进行统计,因此策略无需遍历所有未平仓头寸来计算它们的数量。EA的开仓策略其实简化到检验如下的条件:

  1. 交易事件是新柱形的开始。
  2. 没有其他同向未平仓头寸。
  3. 快速移动平均线在慢速之上(买入)或之下(卖出)。

平仓条件更为简单:

  1. 交易事件是新柱形的开始。
  2. 快速均线在慢速均线之下(平仓多单)或者之上(平仓空单)。

这种情况下,没有必要检查未平仓订单,因为使用当前头寸作为参数调用SupportBuy和SupportSell方法,意味着EA存在未平仓头寸并且被传入其中。

EA的实际逻辑,不考虑方法的声明和它的类,仅用18行代码描述。此外,一半的行(卖出条件)是另一半的镜像(买入条件)。这样简洁的逻辑只有当使用像CStrategy这样的辅助类库时才有可能。

 

基于布林带突破的EA样例

我们继续使用CStrategy交易引擎来创建交易策略。在第二个例子中,我们将创建一个基于布林带突破的交易策略。如果当前价格超过布林带上轨,我们买入。反之,如果当前柱形的收盘价在布林带下轨之下,我们卖出。我们将在价格首次触及指标中轨时平仓。

这次我们将使用标准的iBands指标。直接显示运用此指标的交易模型,无需创建特殊的面向对象的指标类。然而,在这里我们需要在EA中确定两个主要的指标参数 — 平均周期以及标准差的值。这里是策略的源代码:

//+------------------------------------------------------------------+
//|                                                ChannelSample.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
//+------------------------------------------------------------------+
//| 定义                                             
//+------------------------------------------------------------------+
class CChannel : public CStrategy
  {
private:
   int               m_handle;   // 我们将要使用的指标句柄
   int               m_period;   // 布林带周期
   double            m_std_dev;  // 标准差值
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual bool      ParseXmlParams(CXmlElement *params);
   virtual string    ExpertNameFull(void);
public:
                     CChannel(void);
                    ~CChannel(void);
   int               PeriodBands(void);
   void              PeriodBands(int period);
   double            StdDev(void);
   void              StdDev(double std);
  };
//+------------------------------------------------------------------+
//| 默认构造函数
//+------------------------------------------------------------------+
CChannel::CChannel(void) : m_handle(INVALID_HANDLE)
  {
  }
//+------------------------------------------------------------------+
//| 析构函数释放指标句柄
//+------------------------------------------------------------------+
CChannel::~CChannel(void)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
  }
//+------------------------------------------------------------------+
//| 响应货币对变化
//+------------------------------------------------------------------+
void CChannel::OnSymbolChanged(string new_symbol)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(new_symbol,Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| 响应时间框架变化
//+------------------------------------------------------------------+
void CChannel::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| 返回指标周期
//+------------------------------------------------------------------+
int CChannel::PeriodBands(void)
  {
   return m_period;
  }
//+------------------------------------------------------------------+
//| 设置指标周期
//+------------------------------------------------------------------+
void CChannel::PeriodBands(int period)
  {
   if(m_period == period)return;
   m_period=period;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| 设置标准差值
//+------------------------------------------------------------------+
double CChannel::StdDev(void)
  {
   return m_std_dev;
  }
//+------------------------------------------------------------------+
//| 设置标准差值
//+------------------------------------------------------------------+
void CChannel::StdDev(double std)
  {
   if(m_std_dev == std)return;
   m_std_dev=std;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| 多单开仓规则
//+------------------------------------------------------------------+
void CChannel::InitBuy(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // 仅在新的柱形开始时启动交易逻辑
   if(positions.open_buy > 0)return;                  // 不要下超过一个多单
   double bands[];
   if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]>bands[0])
      Trade.Buy(1.0,ExpertSymbol());
  }
//+------------------------------------------------------------------+
//| 多单平仓规则
//+------------------------------------------------------------------+
void CChannel::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;                    // 仅在新的柱形开始时启动交易逻辑
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]<bands[0])
      pos.CloseAtMarket();
  }
//+------------------------------------------------------------------+
//| 空单开仓规则                           
//+------------------------------------------------------------------+
void CChannel::InitSell(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // 仅在新的柱形开始时启动交易逻辑
   if(positions.open_sell > 0)return;                 //不要下超过一个空单 
   double bands[];
   if(CopyBuffer(m_handle, LOWER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]<bands[0])
      Trade.Sell(1.0,ExpertSymbol());
  }
//+------------------------------------------------------------------+
//| Short position closing rules                                      |
//+------------------------------------------------------------------+
void CChannel::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;     // 仅在新柱形开始时启动交易逻辑
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]>bands[0])
      pos.CloseAtMarket();
  }
//+------------------------------------------------------------------+
//| 过滤到来的事件如果传入的事件没被 
//| 策略处理,返回false;如果已处理,   
//| 返回 true                  
//+------------------------------------------------------------------+
bool CChannel::IsTrackEvents(const MarketEvent &event)
  {
//--- 我们仅处理EA所运行货币对和时间框架上的新到来的柱形
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+------------------------------------------------------------------+
//| 策略参数在重写CStrategy中    
//| 方法的方法内部解析
//+------------------------------------------------------------------+
bool CChannel::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=="Period")
         PeriodBands((int)param.GetText());
      else if(name=="StdDev")
         StdDev(StringToDouble(param.GetText()));
      else
         res=false;
     }
   return res;
  }
//+------------------------------------------------------------------+
//|  EA的全称
//+------------------------------------------------------------------+
string CChannel::ExpertNameFull(void)
  {
   string name=ExpertName();
   name += "[" + ExpertSymbol();
   name += "-" + StringSubstr(EnumToString(Timeframe()), 7);
   name += "-" + (string)Period();
   name += "-" + DoubleToString(StdDev(), 1);
   name += "]";
   return name;
  }

现在这个EA能执行更多操作了。EA包含有布林带平均周期参数和它的标准差值。同时,EA在适当的方法中创建指标句柄以及销毁它们。这是因为直接使用了指标而非使用封装。剩下的代码和前一个的EA类似。它等待最近柱形的收盘价高于(买入)或者低于(卖出)布林带边界,如果满足条件则开仓。

注意在EA中我们直接使用特殊的时间序列类来访问柱形。例如,在Buy部分(InitBuy方法),这个方法被用于比较最近的柱形收盘价和布林带上轨。

double bands[];
if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
if(Close[1] > bands[0])
   Trade.Buy(1.0, ExpertSymbol());

除了我们已经熟悉的方法外,EA还包含重写的方法 ExpertNameFull 和 ParseXmlParams。第一个确定EA的唯一名称,它作为EA的名称显示在用户面板中。第二个方法从XML文件中加载布林带指标的设置。用户面板和存储在XML文件中的EA设置将在下一篇文章中讨论。EA剩下部分的操作和之前的类似。这就是所提出方法的目标:全面标准化EA的开发。

 

加载自定义策略到交易引擎中

一旦所有的策略都被描述,我们需要创建他们的实例,使用必要的参数对它们进行初始化以及将其加入交易引擎。任何加入交易引擎的策略都需要有一些必要的返回属性(完成属性)。属性包括如下:

  • 策略的唯一标识符(magic编号)。策略的ID必须是唯一的,即使它们是同一个类的实例。要指定唯一的编号,使用策略中的 ExpertMagic() 方法。
  • 策略的时间框架(EA的运行图表周期)。即使一个策略同时在多个时间周期下运行,你仍旧需要指定运行周期。这种情况下可以是指定最经常使用的时间框架。要指定周期,使用 Timeframe 设置方法。
  • 策略货币对(运行的图表)。即使一个策略同时在多个货币图表下运行,你仍需指定运行图表货币对名称。可以是策略会用的货币对之一。
  • 策略名称。除了上述属性,每一个策略都必须有它自身的字符串(string)名称。使用 ExpertName Set 方法来确定EA的名称。这个属性是必须的,因为它用于从Strategies.xml文件自动创建策略。它还用于在用户面板中显示策略的名称,这将在下一篇文章中介绍。

如果这些属性中有一个没有指定,交易引擎会拒绝加载并且返回一个缺少指定参数的警告信息。

交易引擎有两个主要部分组成:

  • 管理策略的外部模块CStrategyList。这个模块是一个策略管理器,包含控制它们的算法。我们将在下一篇文章中讨论这个模块。
  • 一个策略的内部模块CStrategy。这个模块定义策略的基本函数。这在本文及前一篇文章“通用智能交易系统:事件模型和交易策略原型(第二篇)”中详细叙述了。

CStrategy的每一个实例都必须加载到策略管理器 CStrategyList 中。策略管理器允许以两种方式加载策略:

  • 自动的,使用 Strategies.xml 配置文件。例如,你可以在此文件中描述策略集和它们的参数。然后,当你在图表上运行一个EA时,策略管理器将创建所需的策略实例,初始化它们的参数并将其添加到列表中。这个方法将在下一篇文章中详细叙述。
  • 手动的 向执行模块中添加描述。这种情况下,使用指令集,对应的策略对象在EA的OnInit函数中被创建,然后被初始化并添加到策略管理器 CStrategyList 中。

这里是手动配置过程的描述。我们创建具有下述内容的Agent.mq5文件:

//+------------------------------------------------------------------+
//|                                                        Agent.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\StrategiesList.mqh>
#include <Strategy\Samples\ChannelSample.mqh>
#include <Strategy\Samples\MovingAverage.mqh>
CStrategyList Manager;
//+------------------------------------------------------------------+
//| EA初始化函数             
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 配置并将 CMovingAverage 添加到策略表中
   CMovingAverage *ma=new CMovingAverage();
   ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;

//--- 配置并向策略表中添加 CChannel
   CChannel *channel=new CChannel();
   channel.ExpertMagic(1216);
   channel.Timeframe(Period());
   channel.ExpertSymbol(Symbol());
   channel.ExpertName("Bollinger Bands");
   channel.PeriodBands(50);
   channel.StdDev(2.0);
   if(!Manager.AddStrategy(channel))
      delete channel;

   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);
  }

从这个列表中我们看到策略的配置在OnInit函数中被执行。如果你忘了指定策略的必要参数之一,策略管理器将会拒绝添加到列表中。这种情况下,AddStartegy 方法将反回false,并且创建的策略实例将被删除。策略管理器产生一个警告消息帮助你理解潜在的问题。让我们试着调用这个消息。为此,将编号设置指令注释掉:

//+------------------------------------------------------------------+
//| EA初始化函数             
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 配置并将 CMovingAverage 添加到策略表中
   CMovingAverage *ma=new CMovingAverage();
 //ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;
   return(INIT_SUCCEEDED);
  }

在执行模块开始后,下面的消息将显示在终端中:

2016.01.20 14:08:54.995 AgentWrong (FORTS-USDRUB,H1)    WARNING;CStrategyList::AddStrategy;The strategy should have a magic number. Adding strategy Moving Average is impossible;2016.01.20 14:09:01

消息中清楚的显示了CStrategyList::AddStartegy方法无法添加策略,因为它的编号数组没有设置。

除了配置策略,Agent.mq5文件还包含要分析的交易事件的处理过程。此处理包括事件追踪,并将其传递给 CStrategyList 类中的相应方法。

一旦创建可执行文件,就可以编译了。被分析策略的源代码在本文附件的Include\Strategy\Samples文件夹下。一个编译好的EA将可以使用了并且它包含两个策略的交易逻辑。

 

总结

我们分析了自定义策略的例子以及通过简单索引访问报价的类。此外,我们还探讨了实现日志记录的类,以及面向对象指标的例子。所提出的EA构建理念使得创建交易系统逻辑变得容易。所要做的仅仅是在一些重写方法中定义交易规则。

在第四篇文章“通用EA:组合交易及管理策略组合(第四篇)”中我们将讨论算法,使用此算法我们能够向一个可执行EA模块ex5中添加无限制数量的交易逻辑。在第四篇文章中我们还将考虑一个用户面板,使用它你可以在可执行模块中管理EA,例如,改变交易模式或者代为买入和卖出。

全部回复

0/140

量化课程

    移动端课程