![MQL5 中的定量分析:实现有前途的算法](https://c.mql5.com/2/62/Quantitative_analysis_in_MQL5_-__implementing_a_promising_algorithm_600x314.jpg)
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
![掌握 MQL5:从入门到精通(第二部分)基本数据类型和变量的使用](https://c.mql5.com/2/64/Learning_MQL5_-_from_beginner_to_pro_xPart_IIv_LOGO.png)
![新手在交易中的10个基本错误](https://c.mql5.com/2/13/173_1.png)
![种群优化算法:模拟各向同性退火(SIA)算法。第 II 部分](https://c.mql5.com/2/62/midjourney_image_13870_45_399__3-logo.png)