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

量化交易吧 /  量化策略 帖子:3364705 新帖:26

创建一个在若干工具上交易的 EA 交易程序

萨达撒撒发表于:4 月 17 日 17:09回复(1)

简介

本文介绍为了实施一个在一张图表上启动,能够同时进行不同金融资产交易的 EA 交易程序而编写的程序代码的技术方面。一般而言,在 MQL4 中这也不是一个问题。但是只有在 MetaTrader 5 客户端出现之后,交易者才最终获得使用策略测试程序对此类自动交易系统进行完整分析的机会。

迄今为止,多货币自动交易系统比以前更加流行了,我们可以预测必将涌现出构建此类交易系统的兴趣。但是实施此类机器人的主要问题在于算术级数中程序代码扩展的最多维度,并且这并不容易获得标准程序员的拥护。

在本文中,我们将编写一个简单的多货币 EA 交易程序,在该程序中,如果有结构缺陷,也将是最小的。


1. 简单顺势交易系统的实施

事实上,我们会以最简单的交易系统开始,遵守依据技术指标三重指数移动平均线的内置客户端确定的趋势。这是一个非常简单的算法,不需要特殊注释,并且我们会将其包含到程序代码中。

但是首先,我希望对 EA 交易程序进行最综合的总结。最好以将要到来的 EA 交易程序参数块开始,在全局水平进行声明。

因此,我们必须首先选择要处理的金融资产。这可以使用能够在其中存储资产代号的行输入变量来完成。现在,最好每个金融资产都有一个交易禁止开关,允许按资产禁止交易操作。

自然地,每种资产应与它们的止损、获利、建仓数量和最大允许滑点数等单独交易参数关联在一起。并且出于显而易见的原因,每个交易品种的三重指数移动平均线指标的输入参数应该是单独的。

以下是仅针对一个依据这些参数执行的交易品种的最终输入变量块。余下的块仅在 EA 交易程序的输入参数名称内的数字中有所不同。对于本例,我将自己限定为仅 12 种金融资产,尽管在理想情况下没有对此类块数量的软件限制。

我们仅需要交易某些品种!并且更重要的,我们的 PC 必须要有足够的资源来解决此问题。

input string                Symb0 = "EURUSD";
input  bool                Trade0 = true; 
input int                    Per0 = 15; 
input ENUM_APPLIED_PRICE ApPrice0 = PRICE_CLOSE;
input int                 StLoss0 = 1000;
input int               TkProfit0 = 2000;
input double                Lots0 = 0.1;
input int               Slippage0 = 30;

现在,我们已经在全局水平声明变量,可以继续在函数 OnTick() 内构建代码了。此处最合理的选择应该是将 EA 交易程序中接收交易信号的算法和实际交易分为两个自定义函数。

并且因为 EA 交易程序同时处理 12 种金融资产,在 OnTick() 块内也必须 12 次调用这些函数。 

自然地,这些函数的第一个输入参数应该是按其列出这些交易资产的一个唯一编号。出于显而易见的原因,第二个输入参数将是交易金融资产的行名称。

对于第三个参数的角色,我们将设置一个逻辑变量来决定交易。接下来,对于确定交易信号的算法,遵循输入指标信号,并且对于交易函数而言 - 到挂单的距离、仓位大小和最大允许滑点数(建仓价格的最大允许滑点数)。

要将交易信号从一个函数传到另一个函数,应将静态数组设置为函数的参数,通过引用获取它们的值。这是建议的 OnTick() 函数的代码的最终版本。

void OnTick()
  {
//--- 声明交易信号的变量数组 
   static bool UpSignal[12], DnSignal[12], UpStop[12], DnStop[12];
  
//--- 取得交易信号
   TradeSignalCounter( 0, Symb0,  Trade0,  Per0,  ApPrice0,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 1, Symb1,  Trade1,  Per1,  ApPrice1,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 2, Symb2,  Trade2,  Per2,  ApPrice2,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 3, Symb3,  Trade3,  Per3,  ApPrice3,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 4, Symb4,  Trade4,  Per4,  ApPrice4,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 5, Symb5,  Trade5,  Per5,  ApPrice5,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 6, Symb6,  Trade6,  Per6,  ApPrice6,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 7, Symb7,  Trade7,  Per7,  ApPrice7,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 8, Symb8,  Trade8,  Per8,  ApPrice8,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter( 9, Symb9,  Trade9,  Per9,  ApPrice9,  UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter(10, Symb10, Trade10, Per10, ApPrice10, UpSignal, DnSignal, UpStop, DnStop);
   TradeSignalCounter(11, Symb11, Trade11, Per11, ApPrice11, UpSignal, DnSignal, UpStop, DnStop);
  
//--- 进行交易操作
   TradePerformer( 0, Symb0,  Trade0,  StLoss0,  TkProfit0,  Lots0,  Slippage0,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 1, Symb1,  Trade1,  StLoss1,  TkProfit1,  Lots1,  Slippage1,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 2, Symb2,  Trade2,  StLoss2,  TkProfit2,  Lots2,  Slippage2,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 3, Symb3,  Trade3,  StLoss3,  TkProfit3,  Lots3,  Slippage3,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 4, Symb4,  Trade4,  StLoss4,  TkProfit4,  Lots4,  Slippage4,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 5, Symb5,  Trade5,  StLoss5,  TkProfit5,  Lots5,  Slippage5,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 6, Symb6,  Trade6,  StLoss6,  TkProfit6,  Lots6,  Slippage6,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 7, Symb7,  Trade7,  StLoss7,  TkProfit7,  Lots7,  Slippage7,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 8, Symb8,  Trade8,  StLoss8,  TkProfit8,  Lots8,  Slippage8,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 9, Symb9,  Trade9,  StLoss9,  TkProfit9,  Lots9,  Slippage9,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer(10, Symb10, Trade10, StLoss10, TkProfit10, Lots10, Slippage10, UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer(11, Symb11, Trade11, StLoss11, TkProfit11, Lots11, Slippage11, UpSignal, DnSignal, UpStop, DnStop); 
//---
  }

在 TradeSignalCounter() 函数内,仅需要在每个品种的开始处以及每次柱改变时获得三重指数移动平均线技术指标的句柄来计算交易信号。

这一相对简单的方案以及在代码中的实施正在开始通过微小细节呈现出来。

bool TradeSignalCounter(int Number,
                        string Symbol_,
                        bool Trade,
                        int period,
                        ENUM_APPLIED_PRICE ApPrice,
                        bool &UpSignal[],
                        bool &DnSignal[],
                        bool &UpStop[],
                        bool &DnStop[])
  {
//--- 检查交易是否被禁止
   if(!Trade)return(true);

//--- 声明变量用于保存最后变量数组的大小
   static int Size_=0;

//--- 声明数组用于以静态变量形式保存指标句柄
   static int Handle[];

   static int Recount[],MinBars[];
   double TEMA[4],dtema1,dtema2;

//--- 初始化
   if(Number+1>Size_) // 只有第一次开始时进入初始化区块
     {
      Size_=Number+1; // 禁止此号进入该区块

      //--- 改变变量数组大小
      ArrayResize(Handle,Size_);
      ArrayResize(Recount,Size_);
      ArrayResize(MinBars,Size_);

      //--- 决定足够用于计算的最小柱数 
      MinBars[Number]=3*period;

      //--- 把数组元素清零
      DnSignal[Number] = false;
      UpSignal[Number] = false;
      DnStop  [Number] = false;
      UpStop  [Number] = false;

      //--- 设置数组为时间序列
      ArraySetAsSeries(TEMA,true);

      //--- 取得指标句柄
      Handle[Number]=iTEMA(Symbol_,0,period,0,ApPrice);
     }

//--- 检查柱数是否够用于计算 
   if(Bars(Symbol_,0)<MinBars[Number])return(true);
//--- 取得交易信号 
   if(IsNewBar(Number,Symbol_,0) || Recount[Number]) // 在柱有改变或者复制数据失败时进入此区块
     {
      DnSignal[Number] = false;
      UpSignal[Number] = false;
      DnStop  [Number] = false;
      UpStop  [Number] = false;

      //--- 使用指标句柄把指标
      //--- 缓冲区数值复制到特别准备的静态数组中
      if(CopyBuffer(Handle[Number],0,0,4,TEMA)<0)
        {
         Recount[Number]=true; //由于没有收到数据,我们应该返回 
                               // 进入此区块(当收到交易信号时)在下一个订单号!
         return(false);        // 没有收到交易信号则退出TradeSignalCounter() 函数
        }

      //--- 所有从指标缓冲区复制操作成功完成
      Recount[Number]=false; // 我们可以直到下一个柱的改变时再回到此区块

      int Digits_ = int(SymbolInfoInteger(Symbol_,SYMBOL_DIGITS)+4);
      dtema2 = NormalizeDouble(TEMA[2] - TEMA[3], Digits_);
      dtema1 = NormalizeDouble(TEMA[1] - TEMA[2], Digits_);

      //---- 决定输入信号
      if(dtema2 > 0 && dtema1 < 0) DnSignal[Number] = true;
      if(dtema2 < 0 && dtema1 > 0) UpSignal[Number] = true;

      //---- 决定输出信号
      if(dtema1 > 0) DnStop[Number] = true;
      if(dtema1 < 0) UpStop[Number] = true;
     }
//----+
   return(true);
  }

在这方面,TradePerformer() 函数的代码变得非常简单。

bool TradePerformer(int    Number,
                    string Symbol_,
                    bool   Trade,
                    int    StLoss,
                    int    TkProfit,
                    double Lots,
                    int    Slippage,
                    bool  &UpSignal[],
                    bool  &DnSignal[],
                    bool  &UpStop[],
                    bool  &DnStop[])
  {
//--- 检查交易是否被禁止
   if(!Trade)return(true);

//--- 平掉持仓 
   if(UpStop[Number])BuyPositionClose(Symbol_,Slippage);
   if(DnStop[Number])SellPositionClose(Symbol_,Slippage);

//--- 建新仓位
   if(UpSignal[Number])
      if(BuyPositionOpen(Symbol_,Slippage,Lots,StLoss,TkProfit))
         UpSignal[Number]=false; //这个柱上不会有更多此交易信号!
//---
   if(DnSignal[Number])
      if(SellPositionOpen(Symbol_,Slippage,Lots,StLoss,TkProfit))
         DnSignal[Number]=false; //这个柱上不会有更多此交易信号!
//---
   return(true);
  }
但是这仅仅是因为执行交易操作的实际命令被封装在另外四个函数中:
BuyPositionClose();
SellPositionClose();
BuyPositionOpen();
SellPositionOpen();

所有四个函数的工作方式都是完全类似的,因此我们可以将我们限制为只检查其中一个:

bool BuyPositionClose(const string symbol,ulong deviation)
  {
//--- 声明交易请求结构和交易结果
   MqlTradeRequest request;
   MqlTradeResult result;
   ZeroMemory(request);
   ZeroMemory(result);

//--- 检查是否有买入持仓
   if(PositionSelect(symbol))
     {
      if(PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_BUY) return(false);
     }
   else  return(false);

//--- 初始化 MqlTradeRequest 结构用于平掉买入持仓
   request.type   = ORDER_TYPE_SELL;
   request.price  = SymbolInfoDouble(symbol, SYMBOL_BID);
   request.action = TRADE_ACTION_DEAL;
   request.symbol = symbol;
   request.volume = PositionGetDouble(POSITION_VOLUME);
   request.sl = 0.0;
   request.tp = 0.0;
   request.deviation=(deviation==ULONG_MAX) ? deviation : deviation;
   request.type_filling=ORDER_FILLING_FOK;
//---
   string word="";
   StringConcatenate(word,
                     "<<< ============ BuyPositionClose():   平买入持仓,交易品种 ",
                     symbol," ============ >>>");
   Print(word);

//--- 发送订单平仓交易服务器
   if(!OrderSend(request,result))
     {
      Print(ResultRetcodeDescription(result.retcode));
      return(false);
     }
//----+
   return(true);
  }

基本而言,这几乎是整个多货币 EA 交易程序 (Exp_TEMA.mq5)!

除了提到的函数以外,它还包含另外两个用户函数:

bool IsNewBar(int Number, string symbol, ENUM_TIMEFRAMES timeframe);
string ResultRetcodeDescription(int retcode);

第一个函数依据选择的交易品种和时间框架,在柱改变时返回 true 值,第二个函数按从交易请求结构 MqlTradeResult 的字段 retcode 获得的交易处理的结果代码返回行。 

EA 交易程序准备就绪,是时候开始测试了!在多货币 EA 交易程序的测试与单货币 EA 交易程序的测试之间没有看得见的重大区别。

在策略测试程序的 "Parameters"(参数)选项卡上确定配置:

图 1. 策略测试程序的 "Settings"(设置)选项卡

图 1. 策略测试程序"Settings"(设置)选项卡

如有必要,在 "Input parameters"(输入参数)选项卡中调整输入参数的值:

图 2. 策略测试程序的 "Parameters"(参数)选项卡

图 2. 策略测试程序的 "Parameters"(参数)选项卡

然后在策略测试程序的 "Settings"(设置)选项卡上单击 "Start"(开始)按钮:

图 3. 运行 EA 交易程序测试

图 3. 运行 EA 交易程序测试

由于要载入所有 12 个交易品种的历史记录,EA 交易程序的第一次测试可能需要很长的时间。在策略测试程序中完成测试以后,打开 "Results"(结果)选项卡:

图 4. 测试结果

图 4. 测试结果

然后使用 "Chart"(图表)选项卡的内容分析数据:

图 5. 余额动态和资产净值表

图 5. 余额动态和资产净值表

"Journal"(日志):

图 6. 策略测试程序的日志

图 6. 策略测试程序的日志

很自然地,此 EA 交易程序的市场进入和退出的算法的本质过于简单,在使用第一个随机参数时就期待非常显著的结果,这个想法比较天真。但是我们在这里的目的是为了说明以可能的最简单的方式构建一个多货币 EA 交易程序。

至于优化,由于太多输入参数,此 EA 交易程序可能带来某些不便。传统的优化算法要求这些参数的数量要少很多,因此应针对每个交易品种单独优化 EA 交易程序,禁用余下交易品种的输入参数 TradeN。

现在,在指出方法的本质之后,您可以开始处理更有趣的针对多货币机器人的决策算法。


2. 金融市场中的共振及它们在交易系统中的应用

总体而言,考虑不同金融资产之间的相互关系并不是新的想法,并且它对精确依据此类趋势分析的算法的实施会有吸引力。在本文中,我将依据 Vasily Yakimkin 所著一文 "Resonances - a New Class of Technical Indicators"(共振 - 新一类的技术指标)实施一个多货币自动交易系统,该文于 2001 年 4 月 5 日发表在俄文版期刊《Currency Speculator》(货币投机客)中。

此方法的本质如下所述。例如,为了研究 EUR / USD,我们不仅仅使用有关金融资产的某些指标的结果,还使用与 EUR/USD 资产有关的相同指标的结果 - EUR/JPY 和 USD/JPY。最好使用其值在相同的变化范围内进行规范化的指标,从而简化且便于衡量和计算。

考虑到这些要求,随机动量指标就非常适合。尽管实际上其他指标的使用也没有差异。对于趋势方向,我们将考虑随机动量指标 Stoh 及其信号线 Sign 的值之差。

图 7. 确定趋势方向

图 7. 确定趋势方向

对于交易品种的变量 dStoh,有一张表列出所有的可能组合以及对当前趋势方向的解释:

图 8. 交易品种的变量 dStoh 和趋势方向的组合

图 8. 交易品种的变量 dStoh 和趋势方向的组合

如果资产 EUR / JPY 和 USD / JPY 具有相反的值,我们应确定它们的和值,并且如果和值大于零,则将信号视为正,否则视为负。

因此,如果要建买入持仓,使用趋势向上的情形,要退出,则使用向下趋势,或者使用主资产 EUR / USD 的指标信号是负值的趋势。同样,如果主资产没有信号,并且剩余资产的变量 dStoh 小于零,则退出买入持仓。对于卖出持仓,一切都是非常类似的,仅情形相反。

最合理的解决方案是将 EA 交易程序的整个分析部分放在多货币指标中,并且对于来自指标缓存的 EA 交易程序,仅采用准备好的信号进行交易控制。指标 MultiStochastic.mq5 代表了此指标类型,对市场情形提供了一种可视化分析。

图 9. 多重随机动量指标

图 9. 多重随机动量指标

绿色柱是创建和保持买入持仓的信号,相应地,红色柱是卖出持仓的信号。图表上部的粉红色点和浅绿色点表示退出买入持仓和卖出持仓的信号。

此指标可直接用于在 EA 交易程序中接收信号,但是最好还是使其工作更简单,除去所有不必要的缓存和可视化元素,仅剩下提供交易信号所直接涉及的部分。这正是在 MultiStochastic_Exp.mq5 指标中完成的部分。

在此 EA 交易程序中,我仅交易三个品种,因此 OnTick() 函数的代码变得非常简单:

void OnTick()
  {
//--- 声明交易信号的变量数组
  static bool UpSignal[], DnSignal[], UpStop[], DnStop[];
  
//--- 取得交易信号
  TradeSignalCounter(0, Trade0, Kperiod0, Dperiod0, slowing0, ma_method0, price_0, SymbolA0, SymbolB0, SymbolC0, UpSignal, DnSignal, UpStop, DnStop);
  TradeSignalCounter(1, Trade1, Kperiod1, Dperiod1, slowing1, ma_method1, price_1, SymbolA1, SymbolB1, SymbolC1, UpSignal, DnSignal, UpStop, DnStop);
  TradeSignalCounter(2, Trade2, Kperiod2, Dperiod2, slowing2, ma_method2, price_2, SymbolA2, SymbolB2, SymbolC2, UpSignal, DnSignal, UpStop, DnStop);
                             
//--- 进行交易操作
   TradePerformer( 0, SymbolA0,  Trade0,  StopLoss0,  0,  Lots0,  Slippage0,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 1, SymbolA1,  Trade1,  StopLoss1,  0,  Lots1,  Slippage1,  UpSignal, DnSignal, UpStop, DnStop);
   TradePerformer( 2, SymbolA2,  Trade2,  StopLoss2,  0,  Lots2,  Slippage2,  UpSignal, DnSignal, UpStop, DnStop);
//---
  }

但是,TradeSignalCounter() 函数的代码变得有点复杂:事实在于,多货币指标直接处理三个不同金融工具的时间序列,因此,我们使用 Rates_Total() 函数,对三个时间序列之一中最少数量的柱的适当性实施巧妙的验证。

此外,使用 SynchroCheck() 函数对时间序列的同步进行了额外的验证,以保证确定柱在所有时间序列中同时发生改变的时间的精确性。

bool TradeSignalCounter(int Number,
                        bool Trade,
                        int Kperiod,
                        int Dperiod,
                        int slowing,
                        ENUM_MA_METHOD ma_method,
                        ENUM_STO_PRICE price_,
                        string SymbolA,
                        string SymbolB,
                        string SymbolC,
                        bool &UpSignal[],
                        bool &DnSignal[],
                        bool &UpStop[],
                        bool &DnStop[])
  {
//--- 检查交易是否被禁止
   if(!Trade)return(true);
//--- 声明变量用于保存变量数组的大小
   static int Size_=0;
//--- 声明数组用于以静态变量形式保存指标句柄
   static int Handle[];
   static int Recount[],MinBars[];
//---
   double dUpSignal_[1],dDnSignal_[1],dUpStop_[1],dDnStop_[1];
//--- 改变变量数组大小
   if(Number+1>Size_)
     {
      uint size=Number+1;
      //----
      if(ArrayResize(Handle,size)==-1
         || ArrayResize(Recount,size)==-1
         || ArrayResize(UpSignal, size) == -1
         || ArrayResize(DnSignal, size) == -1
         || ArrayResize(UpStop, size) == -1
         || ArrayResize(DnStop, size) == -1
         || ArrayResize(MinBars,size) == -1)
        {
         string word="";
         StringConcatenate(word,"TradeSignalCounter( ",Number,
                           " ): 错误!!!无法改变变量数组大小!!!");
         int error=GetLastError();
         ResetLastError();
         //---
         if(error>4000)
           {
            StringConcatenate(word,"TradeSignalCounter( ",Number," ): Error code ",error);
            Print(word);
           }
         Size_=-2;
         return(false);
        }

      Size_=int(size);
      Recount[Number] = false;
      MinBars[Number] = Kperiod + Dperiod + slowing;

      //--- 取得指标句柄
      Handle[Number]=iCustom(SymbolA,0,"MultiStochastic_Exp",
                             Kperiod,Dperiod,slowing,ma_method,price_,
                             SymbolA,SymbolB,SymbolC);
     }
//--- 检查柱数是否够用于计算 
   if(Rates_Total(SymbolA,SymbolB,SymbolC)<MinBars[Number])return(true);
//--- 检查时间序列同步
   if(!SynchroCheck(SymbolA,SymbolB,SymbolC))return(true);
//--- 取得交易信号 
   if(IsNewBar(Number,SymbolA,0) || Recount[Number])
     {
      DnSignal[Number] = false;
      UpSignal[Number] = false;
      DnStop  [Number] = false;
      UpStop  [Number] = false;

      //--- 使用指标句柄,把指标 
      //--- 缓冲区值复制到特别准备的静态数组
      if(CopyBuffer(Handle[Number], 1, 1, 1, dDnSignal_) < 0){Recount[Number] = true; return(false);}
      if(CopyBuffer(Handle[Number], 2, 1, 1, dUpSignal_) < 0){Recount[Number] = true; return(false);}
      if(CopyBuffer(Handle[Number], 3, 1, 1, dDnStop_  ) < 0){Recount[Number] = true; return(false);}
      if(CopyBuffer(Handle[Number], 4, 1, 1, dUpStop_  ) < 0){Recount[Number] = true; return(false);}

      //--- 把获得的数值转换为交易命令的逻辑变量值
      if(dDnSignal_[0] == 300)DnSignal[Number] = true;
      if(dUpSignal_[0] == 300)UpSignal[Number] = true;
      if(dDnStop_  [0] == 300)DnStop  [Number] = true;
      if(dUpStop_  [0] == 300)UpStop  [Number] = true;

      //--- 所有从指标缓冲区复制的操作都已成功完成
      //--- 直到下次柱改变之前都没有必要回到此区块了
      Recount[Number]=false;
     }
//----+
   return(true);
  }

由于依据相同的功能组件进行编译,此 EA 交易程序 (Exp_ResonanceHunter.mq5) 的代码没有其他根本性的意识形态差异。因此,我并不认为有必要在其内部结构上花更多时间。


总结

我认为,以 MQL5 语言编写的多货币 EA 交易程序的代码与普通 EA 交易程序的代码非常类似。


全部回复

0/140

量化课程

    移动端课程