
MQL5 中的定量分析:实现有前途的算法
什么是金融市场的定量分析
什么是金融市场的定量分析?定量分析的出现,是作为机器学习的一种先驱,实际上是统计学习的一个子部分。在计算机刚刚开始出现、占据整个房间、并研究穿孔卡片的时代,前进的思想正尝试令它们适应分析大数据和统计数据。当时,可以运行统计操作和函数的价格数据集非常小,函数本身非常简单,发现的形态也不是特别复杂。
这些研究就是简单地计算,以便判定数据中的某些关系,主要是线性的。
金融市场中最简单易学的定量分析方法是分析相关资产之间的价差。例如,我们可以绘制两种相关资产之间的价差,并运用定量分析找到该价差的平均值、最大值和中位数偏差。在获知数据的定量描述后,我们可以了解一种资产与另一种资产的偏差程度,并大致了解两种资产的均衡状态,当它们之间的矛盾被消除时(当资产朝彼此靠拢时),它们肯定会回归。一般来说,在配对交易中使用定量分析是一个非常有趣的话题;我们在以后的文章中肯定会触及这一点。
对冲基金如何使用定量分析
使用定量分析的第一次尝试是爱德华·索普(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 ¤tMovement) { 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


