
Analisi quantitativa in MQL5: Implementazione di un algoritmo promettente
Che cos'è l'analisi quantitativa nel mercato finanziario
Che cos'è l'analisi quantitativa nel mercato finanziario? L'analisi quantitativa è apparsa come una sorta di precursore dell'apprendimento automatico, essendo di fatto una sottosezione dell'apprendimento statistico. Ai tempi in cui i computer cominciavano a comparire, occupavano un'intera stanza e lavoravano su schede perforate, le menti progressiste cercavano di adattarli all'analisi di grandi dati e statistiche. All'epoca, l'insieme delle operazioni e delle funzioni statistiche attraverso le quali era possibile eseguire i prezzi era estremamente ridotto, le funzioni stesse erano piuttosto semplici e i modelli trovati non erano particolarmente complessi.
Questi studi erano semplici calcoli per identificare determinate relazioni nei dati, per lo più lineari.
Il metodo di analisi quantitativa più semplice e facile da apprendere nei mercati finanziari è l'analisi dello spread tra attività correlate. Ad esempio, possiamo tracciare uno spread tra due asset correlati e utilizzando l'analisi quantitativa, trovare la media, il massimo e la deviazione media di questo spread. Avendo ricevuto una descrizione quantitativa dei dati, possiamo capire di quanto un asset si sia discostato dall'altro e comprendere approssimativamente lo stato di equilibrio dei due asset, in cui entrambi torneranno sicuramente quando la discrepanza tra loro sarà eliminata (quando gli asset si muoveranno l'uno verso l'altro). In generale, l'uso dell'analisi quantitativa nel pairs trading è un argomento molto interessante; tratteremo sicuramente questo punto nei prossimi articoli.
Come viene utilizzata l'analisi quantitativa dagli hedge fund
Il primo tentativo di utilizzare l'analisi quantitativa è stato quello di Edward O. Thorp, che negli anni '70 ha imparato ad analizzare lo spread tra un'azione e un warrant su quell'azione, calcolando la sopravvalutazione o la sottovalutazione dell'asset rispetto al suo warrant. A quel tempo il computer di Thorp occupava un'intera stanza e funzionava anche con schede perforate. Edward O. Thorp è stato generalmente il primo ad applicare l'analisi quantitativa computerizzata ai mercati finanziari. Si trattava di un'innovazione all'epoca, riconosciuta da tutto il mondo. Thorp ha creato il primo hedge fund "quantitativo" al mondo.
Come avrete capito, il primo esempio di analisi quantitativa nel mercato azionario che ci viene in mente è la sua applicazione nel pairs trading o basket trading. Prenderemo sicuramente in considerazione queste opzioni, ma l'algoritmo di analisi quantitativa di oggi si baserà su altri principi.
In che altro modo i principali operatori di mercato utilizzano l'analisi quantitativa?
L'arbitraggio statistico consente di rilevare le differenze tra i prezzi degli strumenti finanziari in mercati diversi o in momenti diversi. Ciò consente ai fondi di individuare e sfruttare opportunità di trading profittevoli in una serie di mercati correlati. Inoltre, i modelli quantitativi aiutano gli hedge fund a prevedere i futuri movimenti del mercato sulla base di dati statistici, aiutandoli a prendere decisioni di trading informate.
La gestione del rischio è un'altra applicazione estremamente importante dell'analisi quantitativa. Gli hedge fund utilizzano modelli per valutare e gestire il rischio nei loro portafogli. Ottimizzano la struttura degli asset in base al rischio per ridurre al minimo le perdite potenziali. Ne esistono diversi esempi, come l'ottimizzazione del portafoglio secondo la teoria del portafoglio di Markowitz (che si basa sul rischio in modo che la deviazione del portafoglio non superi il potenziale profitto) e la gestione del rischio secondo il sistema VaR. Quest'ultimo è un modello unico che ci permette di calcolare il drawdown, che non supereremo con una probabilità del 99%.
Naturalmente, il mercato reale a volte è molto difficile da descrivere con la matematica, quindi ci sono anche esempi negativi. Nel 1998 l'hedge fund LTCM ha calcolato che le sue posizioni non avrebbero comportato grosse perdite ed è entrata con una strategia di arbitraggio mirata allo spread tra le obbligazioni statunitensi a lungo e a breve termine sulla base di un'analisi quantitativa. La Russia è andata in default, l'Asia ha avuto una crisi e di conseguenza, attraverso l'effetto farfalla, si è scatenato il panico nel mercato dei titoli di Stato statunitensi. Il fondo LTCM utilizzava modelli che suggerivano che lo spread era anormalmente alto, che il prezzo sarebbe sicuramente "tornato indietro" nella direzione opposta e che le posizioni del fondo sarebbero state sicuramente chiuse con un profitto.
Di conseguenza, il fondo ha applicato l'averaging (mediazione), ha acquistato in modo estremamente aggressivo con una grande leva finanziaria, caricando il debito con gli asset, ed è saltato in aria, nonostante i premi Nobel presenti nello staff della società abbiano parlato dell'impossibilità di un simile esito. Questo è stato il caso in cui un modello di analisi quantitativa denominato VaR ha quasi distrutto l'intero mercato statunitense. Il presidente della Fed Alan Greenspan ha dovuto chiamare d'urgenza i dirigenti delle maggiori banche statunitensi per acquistare le posizioni marginali del fondo, altrimenti la vendita "al mercato" di un così enorme insieme di asset avrebbe causato un immediato azzeramento del mercato azionario statunitense e un panico peggiore della Grande Depressione.
Pertanto, quando si applica l'analisi quantitativa e la media di qualsiasi indicatore, è importante ricordare le code della distribuzione normale delle probabilità. La curva di probabilità a campana, nel caso dei mercati finanziari, presenta delle "code grasse" che riflettono deviazioni significative, definite anche "cigni neri". Da un lato, sono statisticamente estremamente improbabili, dall'altro, la portata e la potenza di questi eventi possono distruggere i portafogli degli investitori e degli hedge fund, eliminare le posizioni marginali, distruggere i mercati e cambiarli in ogni nuovo ciclo. Lo abbiamo visto nel 1998, nel 2008, nel 2020 e nel 2022. Inoltre, lo vedremo molte volte in futuro.
L'analisi quantitativa offre molto agli hedge fund e viene costantemente utilizzata nel loro lavoro quotidiano. Ma è importante ricordare che non esistono funzioni in grado di calcolare le decisioni di milioni di persone, il loro panico e le reazioni a determinati eventi. È inoltre importante ricordare le code della distribuzione normale, che possono rovinare il deposito quando si utilizzano tattiche di trading aggressive.
Base dell'algoritmo: conteggio delle onde di movimento
La base della nostra idea è stata espressa per la prima volta dal trader Artem Zvezdin, che calcola la dimensione delle onde di movimento dei prezzi per capire quanto un asset sia sopravvalutato o sottovalutato rispetto a se stesso. Ad esempio, contiamo le onde rialziste e ribassiste delle ultime 500-5000 barre per capire quanto si è mosso il prezzo in ciascuno dei suoi piccoli cicli. Ogni ciclo di movimento dei prezzi riflette le posizioni di qualcuno, il denaro di qualcuno e le decisioni di acquisto o vendita. Ogni nuovo ciclo è una nuova nascita e morte del mercato. Utilizzeremo l'idea di analizzare i movimenti di prezzo senza rollback, dall'alto verso il basso. Si tratta di un insieme separato di partecipanti che agiscono approssimativamente allo stesso modo, quindi ipotizziamo che la durata dei cicli sarà sempre più o meno la stessa. Calcoleremo il movimento medio dei prezzi utilizzando l'indicatore ZigZag, che è incluso nel pacchetto standard del terminale MetaTrader 5.
Vediamo l'Expert Advisor che ho creato come parte di questo articolo. Per prima cosa, date un'occhiata alla parte dell’intestazione dell'EA. Le impostazioni sono piuttosto semplici. Per il trading utilizziamo la libreria standard Trade. Per le impostazioni dei lotti, è possibile specificare sia un lotto di negoziazione fisso o il calcolo del lotto in base al valore del saldo. Se si indica un profitto di chiusura maggiore di 0, l'EA chiuderà i trade in base al profitto totale. Gli stop loss e i take profit sono calcolati in base al valore ATR, ovvero dipendono dalla volatilità corrente dello strumento. Le impostazioni ZigZag per i calcoli dell'EA sono generalmente standard; non ci soffermeremo su di esse. Inoltre, notate che il nostro modello di EA è multivaluta, in grado di lavorare su una varietà di asset. Questo ci servirà per ridurre il rischio complessivo, negoziando panieri di attività correlate nelle future versioni dell'Expert Advisor. La versione attuale 0.90 funziona solo su un simbolo.
//+------------------------------------------------------------------+ //| 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;
Ora analizziamo le funzioni rimanenti dell'EA. Le funzioni di inizializzazione e deinizializzazione sono generalmente semplici e comprensibili. Impostiamo il numero magico dell'EA, un identificatore unico che permetterà all'EA di distinguere i suoi ordini dagli altri. Allo stesso tempo, impostiamo l'handle in una funzione aggiuntiva scritta in proprio, perché se carichiamo un handle multivaluta direttamente tramite OnInit, l'EA lancerà un errore. Ecco perché utilizziamo questa soluzione piuttosto semplice e facile.
//+------------------------------------------------------------------+ //| 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(""); }
Vediamo le altre funzioni dell'Expert Advisor. Poi ci sono le funzioni per calcolare il profitto totale di tutte le posizioni e una funzione per chiudere completamente tutti gli ordini:
//+------------------------------------------------------------------+ //| 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)); } } } }
Poi abbiamo la funzione di calcolo dei lotti e la funzione di calcolo del numero di posizioni aperte:
//+------------------------------------------------------------------+ //| 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)); }
Abbiamo anche funzioni per calcolare l'ultimo prezzo di negoziazione per gli acquisti e le vendite (che utilizzeremo in seguito) e una funzione per determinare la direzione della posizione.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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); }
E naturalmente la nostra funzione più importante è quella di calcolare il movimento medio e corrente. Per comodità, non sono calcolati in punti, ma in quantità di movimento di un'unità di prezzo. È semplice: chiamiamo la nostra "inizializzazione personalizzata", copiamo i buffer e nel ciclo for calcoliamo la dimensione del movimento del prezzo dalla cima dello ZigZag al suo ultimo estremo. La funzione fornisce il movimento corrente in unità di movimento del prezzo e il movimento medio.
//+------------------------------------------------------------------+ //| 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); }
Un'altra funzione tra le più importanti è quella del trading multivaluta basato su segnali che mostrano che il movimento attuale del prezzo supera il suo valore medio. Il take profit e lo stop loss vengono impostati in base all'ATR. Inoltre, l'ATR viene utilizzato per i passi della griglia (mediazione). Le operazioni vengono aperte su nuove barre. Questo è importante per noi. Questa funzione viene quindi richiamata in OnTick e agisce su uno o più simboli. Non sono ancora riuscito a far funzionare con successo l'EA su diversi simboli, come ho già detto, userò solo un simbolo su cui viene lanciato l'EA. Questo simbolo deve essere specificato nelle impostazioni dell'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); } }
Test del modello
È il momento della parte più divertente: testeremo il nostro modello sul mercato reale. Notare che i calcoli basati sui cicli sono piuttosto impegnativi per il processore, quindi è più sensato eseguire l'EA solo sui prezzi di apertura. Eseguiamo un singolo test su EURUSD, prezzi di apertura, timeframe H1, dal 1° gennaio 2020 al 6 dicembre 2023:
Un singolo test è redditizio, ma il drawdown è elevato. Nessuno vuole prendere rischi aggiuntivi quando fa trading. Ricordate anche che abbiamo una chiusura basata sul profitto. È possibile eseguire un test su un conto netting.
Per eseguire un test con la chiusura basata sul profitto, impostare la chiusura con un profitto superiore a 0. Proviamo a testare. Forse riusciremo ad ottenere un test stabile. Eseguire l'EA sullo stesso asset con i prezzi di apertura. Il nostro tipo di conto è hedging. E questo è ciò che vediamo:
L'EA si è rivelato estremamente rischioso a causa della mediazione. Proviamo a eseguire lo stesso test su un conto netting.
Anche in questo caso abbiamo un grande drawdown; il profitto non vale assolutamente il rischio. Proviamo a rivedere il codice. Questa volta implementeremo la chiusura in base a un segnale (quando un segnale rialzista si trasforma in uno ribassista, le posizioni precedenti verranno chiuse). Aggiungiamo la chiusura per profitto utilizzando il seguente codice:
if (CloseSig) { if (buy) CloseAll(1); if (sell) CloseAll(0); }
E aggiungiamo questa impostazione:
input bool CloseSig = 1; // close by signal
Ripetiamo il test. I risultati non sono ancora buoni:
I test in generale non possono essere definiti ideali. Il drawdown è enorme, sia sul conto netting che su quello hedging. Inoltre, la chiusura basata su un segnale non genera alcun risultato positivo ed è generalmente poco redditizia. Questo è abbastanza sconvolgente.
Conclusioni
Abbiamo visto un semplice esempio di creazione di un algoritmo di analisi quantitativa semplice e basilare in MQL5. Abbiamo contato le onde di movimento del prezzo, le abbiamo confrontate con i valori medi e sulla base di questi dati, abbiamo preso una decisione di acquisto o di vendita. Purtroppo il risultato è stato un algoritmo che fa perdere, anche se la base dell'idea era piuttosto buona. Nei prossimi articoli continueremo ad esplorare l'analisi quantitativa.
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/13835





- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso