English Русский Español Deutsch 日本語 Português
preview
MQL5 中的定量分析:实现有前途的算法

MQL5 中的定量分析:实现有前途的算法

MetaTrader 5交易系统 | 29 七月 2024, 10:05
38 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

什么是金融市场的定量分析

什么是金融市场的定量分析?定量分析的出现,是作为机器学习的一种先驱,实际上是统计学习的一个子部分。在计算机刚刚开始出现、占据整个房间、并研究穿孔卡片的时代,前进的思想正尝试令它们适应分析大数据和统计数据。当时,可以运行统计操作和函数的价格数据集非常小,函数本身非常简单,发现的形态也不是特别复杂。

这些研究就是简单地计算,以便判定数据中的某些关系,主要是线性的。

金融市场中最简单易学的定量分析方法是分析相关资产之间的价差。例如,我们可以绘制两种相关资产之间的价差,并运用定量分析找到该价差的平均值、最大值和中位数偏差。在获知数据的定量描述后,我们可以了解一种资产与另一种资产的偏差程度,并大致了解两种资产的均衡状态,当它们之间的矛盾被消除时(当资产朝彼此靠拢时),它们肯定会回归。一般来说,在配对交易中使用定量分析是一个非常有趣的话题;我们在以后的文章中肯定会触及这一点。 


对冲基金如何使用定量分析

使用定量分析的第一次尝试是爱德华·索普(Edward O. Thorp)的实践,他于 1970 年代学会了分析股票与该股票的认股权证之间的价差,以及计算资产相对于其认股权证的高估或低估程度。索普的电脑在当时占据了整个房间,且还依据打孔卡运行。爱德华·索普(Edward O. Thorp)是第一个将计算机定量分析应用于金融市场的人。这是当时的突破,得到了全世界的认可。索普创建了世界上第一个“量化”对冲基金。 

如您所知,我们想到的股票市场定量分析的第一个例子是它在配对交易、或一篮子交易中的应用。我们肯定会考虑这些选项,但今天的定量分析算法则会基于其它原则。

主要市场参与者应如何运用定量分析? 

统计套利令他们能够检测不同市场、或不同时间点的金融产品价格差值。这令基金能够辨别并利用跨各种不同相关市场的可盈利交易机会。此外,定量模型帮助对冲基金基于统计数据预测未来的市场走势,这有助于他们做出明智的交易决策。

风险管理是定量分析的另一个极其重要的应用。对冲基金使用模型来评估和管理其投资组合中的风险。他们根据风险优化资产结构,从而尽量减少潜在损失。这方面有不同的例子,例如根据马科维茨(Markowitz)投资组合理论(基于风险,如此投资组合的偏差不超过潜在盈利)的投资组合优化,并依据 VaR 系统进行风险管理。后者是一个独特的模型,允许我们计算回撤,我们不会超过 99% 的机会。

当然,真实的市场有时很难用数学来描述,所以也有负面的例子。LTCM 对冲基金在 1998 年计算得出其持仓不会带来巨额亏损,并基于定量分析,采用套利策略入场,标的是长期和短期美国债券之间的利差。俄罗斯违约了,亚洲出现危机,结果就是,通过蝴蝶效应,导致了美国政府债券市场的恐慌。LTCM 基金使用的模型表明,利差异常高额,价格肯定会向相反方向“回滚”,并且基金的持仓肯定会以盈利了结。

结果,该基金应用均摊法,极其激进地获得大量杠杆,用资产负担债务,最终爆仓,尽管公司员工中曾有诺贝尔奖获得者谈到这种结果的不可能性。当一个名为 VaR 的定量分析模型几乎摧毁了整个美国市场时,情况就是如此。美联储主席艾伦·格林斯潘(Alan Greenspan)不得不紧急召集美国最大银行的负责人,以买断该基金的保证金持仓,否则,将如此庞大的资产池“抛售到市场”将导致美国股市立即重洗,并导致比大萧条更严重的恐慌。

因此,在应用任何指标的定量分析和平均时,记住正态概率分布的尾部是很重要的。在金融市场的情况下,钟形概率曲线具有“胖尾”,反映了重大的偏差,这些偏差也被称为“黑天鹅”。一方面,它们在统计学上极端不可能,另一方面,这些事件的规模和威力可能会摧毁投资者的投资组合,以及对冲基金投资组合,摧毁保证金持仓,破坏市场,并在每个新周期中改变它们。我们在 1998 年、2008 年、2020 年和 2022 年都曾看到了这一点。甚至,我们将来会多次看到这一点。

定量分析为对冲基金提供了相当多的功能,并在日常工作中持续加以运用。但重点是要记住,尚没有如此函数能够计算出数百万人的决定、他们的恐慌、和对某些事件的反应。记住正态分布的尾部也很重要,当使用激进的交易手段时,这可能会毁灭本钱。 


算法基础:计算走势波浪

我们的思路其基础首先由交易员 Artem Zvezdin 表述,他计算价格走势波浪的规模,以便了解资产相对于自身的高估或低估程度。例如,我们计算过去 500-5000 根柱线的看涨和看跌波浪,以便了解价格在每个小周期中移动的幅度。每个价格走势周期都反映了某人的持仓、某人的资金、以及买卖决策。每一个新的周期,都是市场的新生和死亡。我们采用的价格走势分析思路没有回滚,从上到下。这是一组单独的参与者,他们的行为大致相同,如此这般我们假设周期的长度总是大致相同。我们将使用之字折线(ZigZag)指标计算平均价格走势,该指标包含在标准 MetaTrader 5 终端发行包之中。

我们看一下我在本文中创建的智能系统。首先,看一下 EA 的头部。这里的设置非常简单。对于交易,我们使用标准的交易库。对于手数设置,您可以指定手数按固定手数交易,也可以指定基于余额值计算手数。如果您指示的平仓利润大于 0,则 EA 将根据总利润平仓。止损和止盈是根据 ATR 值计算的,即取决于金融产品的当前波动性。EA 计算依据的之字折线设置是通常标准的;我们不会详述它们。另外,请注意,我们的 EA 模板是多币种的,能够处理各种资产。我们需要这个来降低整体风险,通过在智能系统的未来版本中交易一篮子相关资产来降低总体风险。当前 0.90 版本仅适用于一个品种。

//+------------------------------------------------------------------+
//|                                          QuantAnalysisSample.mq5 |
//|                                                   Copyright 2023 |
//|                                                Evgeniy Koshtenko |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2023, Evgeniy Koshtenko"
#property link        "https://www.mql5.com"
#property version     "0.90"
#property strict

#include <Trade\Trade.mqh>
#include <Graphics\Graphic.mqh>
#include <Math\Stat\Normal.mqh>
#include <Math\Stat\Math.mqh>
CTrade trade;
//--- Inputs
input double Lots       = 0.1;      // lot
input double Risk       = 0.1;     // risk
input double Profit     = 0;     // profit
input int StopLoss      = 0;        // ATR stop loss
input int TakeProfit    = 0;        // ATR take profit
input string Symbol1    = "EURUSD";
input int    Magic      = 777;    // magic number
//--- Indicator inputs
input uint   InpDepth       =  120;   // ZigZag Depth
input uint   InpDeviation   =  50;    // ZigZag Deviation
input uint   InpBackstep    =  30;    // ZigZag Backstep
input uchar  InpPivotPoint  =  1;    // ZigZag pivot point
datetime t=0;
double last=0;
double countMovements;
double currentMovement;
// Global variable for storing the indicator descriptor
int zigzagHandle;

现在我们看看 EA 的其余函数。初始化和逆初始化的函数通常简单易懂。我们设置了 EA 的魔幻数字,这是一个独有的标识符,允许将 EA 的订单与其它订单区分开来。同时,我们在一个额外的自编函数中设置了句柄,因为如果我们直接通过 OnInit 加载多币种句柄,EA 会抛出错误。这就是为什么我们要使用这个相当简单易行的解决方案。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert initialization function custom                            |
//+------------------------------------------------------------------+
int OnIniti(string symb)
  {// Loading the ZigZag indicator
   zigzagHandle = iCustom(symb, _Period, "ZigZag", InpDepth, InpDeviation, InpBackstep, InpPivotPoint);
   if (zigzagHandle == INVALID_HANDLE)
     {
      Print("Error loading the ZigZag indicator: ", GetLastError());
      return(INIT_FAILED);
     }
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Comment("");
  }

我们看一下智能系统的其它函数。接下来,我们有用于计算所有仓位总盈利的函数,以及用于所有订单完全平仓的函数:

//+------------------------------------------------------------------+
//|  Position Profit                                                 |
//+------------------------------------------------------------------+
double AllProfit(int type=-1)
  {
   double p=0;

    for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetInteger(POSITION_MAGIC)==Magic)
           {
            if(PositionGetInteger(POSITION_TYPE)==type || type==-1)
               p+=PositionGetDouble(POSITION_PROFIT);
           }
        }
     }

   return(p);
  }
//+------------------------------------------------------------------+
//|   CloseAll                                                       |
//+------------------------------------------------------------------+
void CloseAll(int type=-1)
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetInteger(POSITION_MAGIC)==Magic)
           {
            if(PositionGetInteger(POSITION_TYPE)==type || type==-1)
               trade.PositionClose(PositionGetTicket(i));
           }
        }
     }
  }

接下来,我们有计算手数的函数,和计算持仓数量的函数:

//+------------------------------------------------------------------+
//|     CountTrades                                                  |
//+------------------------------------------------------------------+
int CountTrades(string symb)
  {
   int count=0;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            count++;
           }
        }
     }
   return(count);
  }
//+------------------------------------------------------------------+
//|     Lot                                                          |
//+------------------------------------------------------------------+  
double Lot()
  {
   double lot=Lots;

   if(Risk>0)
      lot=AccountInfoDouble(ACCOUNT_BALANCE)*Risk/100000;

   return(NormalizeDouble(lot,2));
  }

我们还有计算最后成交价格,以便买入和卖出的函数(我们稍后会用到),和一个用于判定持仓方向的函数。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double FindLastBuyPrice(string symb)
  {
   double pr=0;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==0)
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            pr=PositionGetDouble(POSITION_PRICE_OPEN);
            break;
           }
        }
     }
   return(pr);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double FindLastSellPrice(string symb)
  {
   double pr=0;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==1)
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            pr=PositionGetDouble(POSITION_PRICE_OPEN);
            break;
           }
        }
     }
   return(pr);
  }
//+------------------------------------------------------------------+
//|  PositionType                                                    |
//+------------------------------------------------------------------+
int PositionType(string symb)
  {
   int type=8;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            type=(int)PositionGetInteger(POSITION_TYPE);
            break;
           }
        }
     }
   return(type);
  }

当然,我们最重要的函数是计算均摊和当前走势的函数。为方便起见,它们不是以点数计算的,而是以价格单位的变动量计算的。这很简单:我们调用自定义的初始化,复制缓冲区,在 for 循环当中,我们计算自之字折线顶部到最后一个极值的价格走势规模。该函数以价格变动和平均变动为单位输出当前变动。 

//+------------------------------------------------------------------+
//|     CalculateAverageMovement                                     |
//+------------------------------------------------------------------+ 
void CalculateAverageMovement(string symb, double &averageMovement, double &currentMovement) {
    const int lookback = 500; // Number of bars for analysis
    double sumMovements = 0.0;
    int countMovements = 0;
    double lastExtremePrice = 0.0;
    double zigzagArray[500]; // Array to store ZigZag values
    OnIniti(symb);
    // Copy ZigZag values to array
    if (CopyBuffer(zigzagHandle, 0, 0, lookback, zigzagArray) <= 0) {
        Print("Error copying indicator data");
        averageMovement = -1;
        currentMovement = -1;
        return;
    }

    // Copy ZigZag values to array
    if (CopyBuffer(zigzagHandle, 0, 0, lookback, zigzagArray) <= 0) {
        Print("Error copying indicator data");
        averageMovement = -1;
        currentMovement = -1;
        return;
    }

    for (int i = 0; i < lookback; i++) {
        if (zigzagArray[i] != 0 && zigzagArray[i] != lastExtremePrice) {
            if (lastExtremePrice != 0) {
                // Determine the movement direction
                double movement = zigzagArray[i] - lastExtremePrice;
                sumMovements += movement;
                countMovements++;
            }
            lastExtremePrice = zigzagArray[i];
        }
    }

    // Calculate the current movement
    double lastMovement = iClose(symb, _Period, 0) - lastExtremePrice;
    currentMovement = lastMovement;

    // Calculate the average movement
    averageMovement = countMovements > 0 ? sumMovements / countMovements : 0.0;

    // Print the result
    Print("Average movement: ", averageMovement);
    Print("Current movement: ", currentMovement);

    // Release resources
    IndicatorRelease(zigzagHandle);
}

另一个关键函数是多币种交易的函数,该函数基于显示当前价格变动超过其平均值的信号。止盈和止损是根据 ATR 设置的。此外,ATR 还用于网格步长(均摊)。交易在新柱线时开立。这对我们很重要。然后,在 OnTick 中调用该函数,并处理一个或多个品种。我还没有能够在多个交易品种上成功运行 EA,正如我已经说过的,我只用到启动 EA 的那个品种。该品种应在 EA 设置中指定。 

//+------------------------------------------------------------------+
//| Expert Trade unction                                             |
//+------------------------------------------------------------------+
void Trade(string symb)
  {
   double averageMovement = 0;
   double currentMovement = 0;
   double pr=0,sl=0,tp=0,hi=0,lo=0;
// Call function for calculation
   CalculateAverageMovement(symb, averageMovement, currentMovement);

// Use results
   double Ask = SymbolInfoDouble(symb, SYMBOL_ASK);
   double Bid = SymbolInfoDouble(symb, SYMBOL_BID);
   int dg=(int)SymbolInfoInteger(symb,SYMBOL_DIGITS);
   double pp=SymbolInfoDouble(symb,SYMBOL_POINT);
  
   double atr = iATR(symb, PERIOD_CURRENT, 3);
     
// Here define your logic for buying and selling
   bool sell  = currentMovement > -averageMovement; // Buy condition
   bool buy = -currentMovement > averageMovement; // Sell condition
  
   if(AllProfit()>Profit && Profit>0)
      CloseAll();

   if(t!=iTime(symb,PERIOD_CURRENT,0))
     {
      if(buy && CountTrades(symb)<1)
        {
         if(StopLoss>0)
            sl=NormalizeDouble(Bid-(atr*StopLoss)*Point(),_Digits);
         if(TakeProfit>0)
            tp=NormalizeDouble(Bid+(atr*TakeProfit)*Point(),_Digits);
         pr=NormalizeDouble(Bid,dg);
         trade.Buy(Lot(),symb,pr,sl,tp,"");
         last=pr;
        }
      if(sell && CountTrades(symb)<1)
        {
         if(StopLoss>0)
            sl=NormalizeDouble(Ask+(atr*StopLoss)*Point(),_Digits);
         if(TakeProfit>0)
            tp=NormalizeDouble(Ask-(atr*TakeProfit)*Point(),_Digits);
         pr=NormalizeDouble(Ask,dg);
         trade.Sell(Lot(),symb,Ask,sl,tp,"");
         last=pr;
        }
      if(CountTrades(symb)>0)
        {
         if(PositionType(symb)==0 && (FindLastBuyPrice(symb)-Ask)/pp>=atr*30)
           {
            if(StopLoss>0)
               sl=NormalizeDouble(Bid-(atr*StopLoss)*Point(),_Digits);
            if(TakeProfit>0)
               tp=NormalizeDouble(Bid+(atr*TakeProfit)*Point(),_Digits);
            trade.Buy(Lot(),symb,Ask,sl,tp);
           }
         if(PositionType(symb)==1 && (Bid-FindLastSellPrice(symb))/pp>=atr*30)
           {
            if(StopLoss>0)
               sl=NormalizeDouble(Ask+(atr*StopLoss)*Point(),_Digits);
            if(TakeProfit>0)
               tp=NormalizeDouble(Ask-(atr*TakeProfit)*Point(),_Digits);
            trade.Sell(Lot(),symb,Bid,sl,tp);
           }
        }
      t=iTime(symb,0,0);
     }
  }


测试模型

此刻到了最有趣的部分了:我们将在真实市场上测试我们的模型。请注意,基于循环的计算非常耗费处理器,因此仅在开盘价上运行 EA 更有意义。我们依据 2020 年 1 月 1 日至 2023 年 12 月 6 日的 EURUSD 开盘价、H1 时间帧进行一次测试:


单一测试是有利可图的,但回撤很大。没有人愿意在交易时承担额外的风险。记住,我们也曾基于盈利平仓。我们可以在净额结算账户上运行测试

为了基于盈利平仓运行测试,将盈利设置为高于 0 时平仓。我们尝试测试。也许我们会得到一个稳定的测试。依据同一资产以开盘价运行 EA。我们的账户类型是对冲。这就是我们所看到的:


由于均摊法,EA 被证明是极其冒风险的。我们尝试在净额结算账户上运行相同的测试。


我们再次出现了大幅回撤;盈利比之风险完全不值得。我们尝试修改代码。这一次,我们将实现依据信号平仓(当看涨信号变为看跌信号时,之前的仓位将被平仓)。我们使用以下代码添加按盈利了结:

if (CloseSig)
   {
      if (buy)
         CloseAll(1);
      if (sell)
         CloseAll(0);
   }

并添加以下设置:

input bool CloseSig     = 1;        // close by signal

重复测试。结果又不好了:


泛泛而言,测试不能被称为理想。回撤是巨大的,净额结算和对冲账户都有大量的回撤。甚至,基于信号平仓不会产生任何积极的结果,并且通常是无利可图的。这相当令人沮丧。


结束语

我们已经看了一个在 MQL5 中创建基本和简单的定量分析算法的简单示例。我们计算了价格走势波浪,将它们与平均值进行比较,并根据这些数据做出了买入或卖出的决定。不幸的是,这导致了一个亏损的算法,尽管这个思路的基础非常好。在以后的文章中,我们将继续探索定量分析。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/13835

附加的文件 |
在 MQL5 中创建做市商算法 在 MQL5 中创建做市商算法
做市商是如何运作的?让我们探讨一下这个问题,创建一个初级的做市商算法。
掌握 MQL5:从入门到精通(第二部分)基本数据类型和变量的使用 掌握 MQL5:从入门到精通(第二部分)基本数据类型和变量的使用
这是初学者系列的延续。本文将介绍如何创建常量和变量、写入日期、颜色和其他有用的数据。我们将学习如何创建枚举,如一周中的天数或线条样式(实线、虚线等)。变量和表达式是编程的基础。它们肯定存在于99%以上的程序中,因此理解它们至关重要。因此,如果你是编程新手,这篇文章会对你非常有用。所需的编程知识水平:非常基础,在我上一篇文章(见开头的链接)的范围内。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
种群优化算法:模拟各向同性退火(SIA)算法。第 II 部分 种群优化算法:模拟各向同性退火(SIA)算法。第 II 部分
第一部分专注于众所周知、且流行的算法 — 模拟退火。我们已经通盘研究了它的利弊。本文的第二部分专注于算法的彻底变换,将其转变为一种新的优化算法 — 模拟各向同性退火(SIA)。