English Русский Deutsch 日本語 Türkçe
preview
Python + MetaTrader 5: Framework Rapido per la Ricerca su Dati, Feature e Prototipi

Python + MetaTrader 5: Framework Rapido per la Ricerca su Dati, Feature e Prototipi

MetaTrader 5Integrazione |
19 2
MetaQuotes
MetaQuotes

Introduzione

Python è diventato uno degli strumenti più pratici per lavorare con i dati. Offre una vasta gamma di librerie che ci consentono di eseguire rapidamente analisi statistiche, testare ipotesi e presentare visivamente i risultati senza sprecare tempo e risorse. Questo è importante per la risoluzione di problemi relativi ai mercati finanziari: in questo contesto, non solo la velocità di elaborazione dei dati è un valore fondamentale, ma anche la capacità di passare rapidamente dall'analisi alle conclusioni pratiche.

MetaTrader 5 offre l'integrazione diretta con Python, il che amplia notevolmente le possibilità di lavoro pratico con i dati di mercato. Un ricercatore o uno sviluppatore può utilizzare il consueto set di strumenti Python per studiare i dati sui prezzi, costruire modelli statistici e testare ipotesi pratiche senza interrompere la connessione alla piattaforma di trading. Questo approccio rende il flusso di lavoro più flessibile e supporta un ciclo unificato: dai dati all'ipotesi, dall'ipotesi al modello e dal modello all'applicazione pratica.

MetaTrader 5 + Python

In questo articolo mostreremo:

  • Come Python viene integrato con MetaTrader 5 ;
  • come utilizzarlo per analizzare dati finanziari e testare ipotesi;
  • Come costruire e addestrare un piccolo modello e poi trasferire il risultato dell'addestramento a un Expert Advisor (EA) utilizzando ONNX.

Questo ci permetterà di passare da un esperimento di ricerca all'implementazione pratica in un sistema di trading.

1. Installazione e collegamento

Prima di procedere all'analisi dei dati, è necessario preparare correttamente l'ambiente di lavoro per l'utilizzo congiunto di Python e MetaTrader 5. Il compito è semplice, ma richiede precisione. Una corretta configurazione iniziale vi eviterà decine di piccoli problemi in seguito.

Innanzitutto, installiamo il terminale MetaTrader 5 scaricando la distribuzione dal sito ufficiale.

Successivamente avremo bisogno della versione corrente di Python. Al momento della stesura di questo articolo, la versione è la 3.14.3. Durante l'installazione, assicuratevi di abilitare l'opzione che aggiunge Python alla variabile d'ambiente PATH. Ciò consentirà di lavorare con l'interprete direttamente dalla riga di comando senza inutili impostazioni manuali.

Installazione di Python

Il punto chiave è isolare l'ambiente. L'esperienza dimostra che i progetti che lavorano con dati e modelli si sovraccaricano rapidamente di dipendenze. Per garantire ordine e riproducibilità dei risultati, per ogni progetto viene creato un ambiente virtuale separato. In Python, questo problema può essere risolto con lo strumento integrato venv.

Il flusso di lavoro è il seguente.

  1. Aprire l'ambiente di esecuzione dei comandi. Il modo più semplice è premere Win+R. Nella finestra che appare, inserite il comando cmd e premete Invio. Questo apre il prompt dei comandi di Windows. Se volete, potete usare PowerShell - il principio è lo stesso.
  2. Spostatevi nella directory del progetto in cui verrà creato l'ambiente. Questo si fa con il comando standard:
  3. cd /path/to/your/project
  4. Create un ambiente virtuale.
  5. python -m venv integration

    E attivatelo.

    integration\Scripts\activate

    D'ora in poi, tutti i pacchetti installati saranno isolati all'interno del progetto corrente.

  6. Installate il modulo Python per interagire con MetaTrader 5.
  7. pip install MetaTrader5
  8. Per un'analisi completa dei dati finanziari, è opportuno implementare immediatamente uno stack di librerie di base, ma quasi completo e pronto all'uso. Copre le attività chiave di gestione dei dati, costruzione di modelli e analisi tecnica.

Innanzitutto, installa NumPy — il fondamento del calcolo numerico. Questa è la base su cui si fonda l'intero stack successivo.

Successivamente colleghiamo Pandas - è lo strumento principale per lavorare con dati tabellari e serie temporali, senza il quale l'analisi dei prezzi diventerebbe un tormento.

Per la visualizzazione, utilizziamo una connessione tra Matplotlib e Seaborn. La prima opzione offre il pieno controllo sui grafici, mentre la seconda velocizza la creazione di visualizzazioni statisticamente significative. Lavorando in sinergia, ci permettono di leggere il mercato, non soltanto di quantificarlo.

Per le attività di machine learning, aggiungiamo Scikit-Learn - uno strumento collaudato per la creazione e la validazione dei modelli. È ideale per prototipi iniziali e strategie di base.

Per l'analisi di mercato applicata, colleghiamo TA — libreria di indicatori tecnici. Questo è un metodo pratico per arricchire rapidamente i dati con segnali senza dover implementare manualmente le formule classiche.

L'installazione delle librerie viene eseguita con un singolo comando.

pip install numpy pandas matplotlib seaborn scikit-learn ta

Vale la pena aggiungere la libreria pytz. A prima vista sembra un elemento ausiliario, ma in pratica è fondamentale per lavorare con i fusi orari.

pip install pytz

I dati finanziari sono strettamente legati al fattore tempo. Le borse valori operano in fusi orari diversi.

Per impostazione predefinita, Python si basa sull'ora locale del sistema quando crea l'oggetto datetime. Questo comportamento è comodo per le attività quotidiane, ma in un contesto finanziario diventa fonte di errori sistemici. MetaTrader 5 memorizza gli orari dei tick e delle barre nel formato UTC - senza spostamento e senza riferimento al fuso orario locale.

Questo crea un classico disallineamento. Il modello opera in un determinato riferimento temporale, mentre i dati sono in un altro. In pratica, ciò comporta effetti spiacevoli.

Pertanto, qui la regola è rigida. Tutte le operazioni relative al tempo devono essere eseguite in UTC. Gli oggetti datetime devono essere creati esplicitamente nel fuso orario UTC, mentre tutti i valori locali devono essere uniformati a un unico standard. Questo allinea i dati e il modello nello stesso sistema di coordinate temporali.

È buona norma utilizzare pytz per la gestione esplicita dei fusi orari. Ciò elimina le trasformazioni implicite e rende prevedibile il comportamento del sistema.

I dati ottenuti da MetaTrader 5 sono già in formato UTC. Non dovrebbero essere corretti. Devono essere interpretati correttamente e risultare coerenti con la logica del modello. Nei problemi finanziari, il tempo non è solo un indicatore, ma un asse di coordinate. Qualsiasi errore in esso compromette l'intera geometria dell'analisi.

Questo set può sembrare limitato, ma in pratica copre fino all'80% delle attività tipiche. Questo è un approccio classico: meno ridondanza, più efficienza.

Se dobbiamo uscire dall'ambiente corrente, utilizziamo il comando standard:

deactivate

In questa fase, l'infrastruttura è completamente pronta. Il terminale, l'interprete e le librerie necessarie sono installati. L'ambiente è isolato e riproducibile. Questa è la base su cui è comodo e sicuro costruire ulteriori analisi, testare ipotesi e passare gradualmente allo sviluppo di modelli di trading.


2. Caricamento dei dati

Dopo aver predisposto l'infrastruttura, passiamo alla prima fase pratica - la scrittura del programma. Non ci sono limitazioni rigide: qualsiasi editor di testo familiare andrà bene. Tuttavia, nel contesto dell'integrazione, è logico utilizzare l'editor integrato in MetaTrader 5, chiamato MetaEditor, che supporta già il lavoro con Python e consente di mantenere l'intero processo all'interno di un unico framework.

Per avviare gli script Python direttamente da MetaEditor o dal terminale, è sufficiente specificare una sola volta il percorso dell'interprete nelle impostazioni della piattaforma. Si tratta di un'operazione di base, ma c'è un dettaglio importante da sottolineare. Se si utilizza un ambiente virtuale creato in precedenza, il percorso deve puntare specificamente al suo interprete, non a un'installazione globale di Python.

Questo approccio mantiene l'isolamento del progetto e garantisce che tutte le dipendenze rimangano sotto controllo. Altrimenti, si rischia di incorrere in bug difficili da individuare, per cui lo stesso codice si comporta in modo diverso a seconda dell'ambiente di esecuzione. In ambito finanziario, questo è un lusso inaccettabile.

Nella prima fase, stabiliamo una connessione di base: PythonMetaTrader 5 → dati storici. Il compito è semplice nella sua formulazione ma fondamentale nella sua essenza: connettersi al terminale tramite uno script e ottenere i dati di prezzo per uno strumento e un timeframe specificati. È da questo momento che inizia qualsiasi analisi significativa.

È logico suddividere la struttura dello script in blocchi. Innanzitutto, colleghiamo le librerie necessarie - questo costituisce il set degli strumenti di lavoro.

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import pytz
import seaborn as sns
import matplotlib.pyplot as plt
import ta

Successivamente, la connessione al terminale viene inizializzata tramite il modulo MetaTrader5. Questo è il punto di accesso al sistema. Se la connessione non viene stabilita, non ha senso continuare. Pertanto, il controllo dello stato viene eseguito immediatamente e senza compromessi.

# Display data on the MetaTrader 5 package
print("MetaTrader5 package author: ", mt5.__author__)
print("MetaTrader5 package version: ", mt5.__version__)

# Connection to MetaTrader 5 terminal
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

Il passo successivo consiste nell'impostare l'intervallo di tempo. Qui utilizziamo pytz per indicare esplicitamente il fuso orario UTC. Non si tratta di un dettaglio tecnico, bensì di un requisito obbligatorio. Il terminale salva i dati sui prezzi in UTC e qualsiasi discrepanza da parte di Python comporta una distorsione dei dati. L'errore è silenzioso, ma le conseguenze sono sistemiche.

# Set time zone to UTC
timezone = pytz.timezone("Etc/UTC")
# Create 'datetime' objects in UTC time zone to avoid the implementation of a local time zone offset
utc_from = datetime(2020, 1, 1, tzinfo=timezone)
utc_to = datetime.now(timezone)  # Set to the current date and time

Successivamente, si richiedono i dati storici. L'esempio utilizza barre orarie per EURUSD. In questo caso, vale la pena prestare attenzione al nome dello strumento. Deve corrispondere esattamente all'ortografia presente nel terminale, inclusi suffissi e prefissi.

# Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

Il risultato è un array di strutture con prezzi e timestamp - dati di mercato grezzi, senza filtri né interpretazioni. Questo è esattamente il tipo di dati necessari nella prima fase.

Dopodiché, la connessione al terminale viene chiusa correttamente. Questa è disciplina di esecuzione: una chiusura controllata previene guasti nascosti agli avvii successivi e rende prevedibile il comportamento del sistema.

# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()

Segue una verifica di base del risultato. Se vengono ricevuti dati, i primi record vengono visualizzati per una rapida convalida. Altrimenti, lo script viene interrotto.

# Check if data was retrieved
if rates is None or len(rates) == 0:
    print("No data retrieved. Please check the symbol or date range.")
    quit()
# Print the first 10 raw records for a quick data sanity check
print("Display obtained data 'as is'")
for rate in rates[:10]:
    print(rate)

Inoltre, viene effettuata una visualizzazione: viene creato un grafico dei prezzi di chiusura e dei volumi. In questo caso, è consigliabile posizionare i volumi su un asse separato. Imposteremo il valore massimo lungo l'asse del volume con un margine cinque volte maggiore del massimo osservato. Questo metodo sembra semplice, ma funziona alla perfezione. L'istogramma viene compresso nella parte inferiore del grafico e smette di competere con il prezzo per attirare l'attenzione.

Di conseguenza, la linea del prezzo di chiusura rimane chiara e leggibile, e i volumi restano informativi senza appesantire la presentazione visiva. Si tratta di un classico equilibrio tra completezza dei dati e comprensibilità. Un grafico non dovrebbe solo contenere informazioni, ma anche consentirne una rapida interpretazione senza stress inutile.

# Create a DataFrame from the retrieved tick data
rates_frame = pd.DataFrame(rates)
# Convert the timestamp column from seconds since epoch to datetime
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

# Use datetime as the DataFrame index for time series plotting and analysis
rates_frame.set_index('time', inplace=True)

# Plot closing price and tick volume
fig, ax1 = plt.subplots(figsize=(12, 6))

# Close price on primary y-axis
ax1.set_xlabel('Date')
ax1.set_ylabel('Close Price', color='tab:blue')
ax1.plot(rates_frame.index, rates_frame['close'], color='tab:blue', label='Close Price')
ax1.tick_params(axis='y', labelcolor='tab:blue')

# Tick volume on secondary y-axis
ax2 = ax1.twinx()  
ax2.set_ylabel('Tick Volume', color='tab:green')
max_tick = rates_frame['tick_volume'].max()
ax2.set_ylim(0, max_tick * 5)
ax2.plot(rates_frame.index, rates_frame['tick_volume'], color='tab:green', label='Tick Volume')
ax2.tick_params(axis='y', labelcolor='tab:green')

# Show the plot
plt.title('Close Price and Tick Volume Over Time')
fig.tight_layout()
plt.show()
fig.savefig('close_price.png')

Dinamica del prezzo di chiusura

Da un punto di vista pratico, la procedura sembra elementare. Ma in realtà, questa è una fase fondamentale del controllo della qualità dei dati. Comprendete chiaramente cosa viene inserito nel modello: formato, timestamp, valori. Questo tipo di audit iniziale è un classico esempio di approccio ingegneristico. Consente di risparmiare tempo, stress e cosa particolarmente importante nei sistemi di trading, denaro.


3. Verifica delle ipotesi e selezione delle feature

Ora che disponiamo di dati storici sui prezzi, passiamo all'analisi iniziale. Partiamo da un'ipotesi estremamente semplice, quasi da manuale : Il mercato tende a proseguire il movimento dell'ultima barra. Verifichiamo se si riscontra inerzia nelle dinamiche a breve termine.

Il toolkit Python consente di testare un'ipotesi di questo tipo in poche righe di codice. La logica è la seguente. Prendiamo una serie di prezzi di chiusura e passiamo ad analizzare le differenze di prezzo tra una barra e l'altra. Ottenere informazioni sulla dinamica dei prezzi è un elemento fondamentale per l'analisi.

# Correlation analysis between adjacent bar moves
close = rates_frame['close'].to_numpy(dtype=float)
# last and next price move differences
diff = close[1:] - close[:-1]

Successivamente formiamo due serie temporali: l’ultima e le successive variazioni. Tecnicamente, ciò si realizza spostando l'array di un elemento. Il risultato è costituito da coppie di valori in cui ogni osservazione risponde a una semplice domanda: se il mercato si muoveva al rialzo (o al ribasso), cosa accade nella barra successiva?

diff = np.column_stack((diff[:-1], diff[1:]))
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])

Successivamente, viene calcolato il coefficiente di correlazione di Pearson tra le due serie. Una correlazione positiva indica una forte inerzia, mentre una negativa indica un pullback predominante.

Per maggiore chiarezza, il risultato viene visualizzato utilizzando Seaborn - viene creata una mappa di calore della correlazione. Questo è un modo rapido per visualizzare la struttura della dipendenza senza addentrarsi nei dettagli numerici.

correlation_matrix = data_matrix.corr('pearson')
plt.subplots(figsize=(3, 2))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Bar to Bar') 
plt.savefig('bar_to_bar.png')
plt.show()

Il punto cruciale non è la strategia in sé - bensì la sua intrinseca primitività. Il valore risiede altrove. Dimostriamo un ciclo di verifica delle ipotesi di base. Abbiamo formulato un'ipotesi, trasformato i dati, eseguito un calcolo e visualizzato il risultato. Questo approccio disciplina l'analisi e ci impedisce di trarre conclusioni a colpo d'occhio.

Correlazione tra l'ultima candela e la successiva

Ma valutiamo comunque i risultati ottenuti. La correlazione osservata è pari a -0.018 - un valore prossimo allo zero, ma con segno negativo.

Ciò significa che non esiste una relazione chiara tra le barre adiacenti. Inoltre, il debole segno negativo indica un sottile effetto mean reversion. Dopo un movimento in una direzione, è probabile che la barra successiva si muova nella direzione opposta. Tuttavia, l'entità dell'effetto è così piccola che, da un punto di vista pratico, rasenta il rumore statistico.

L'ipotesi di continuità del movimento non è confermata. Sul timeframe H1, il comportamento del mercato è più vicino ad un processo casuale che ad un sistema inerziale. Si tratta di un'osservazione importante. Elimina immediatamente un'intera categoria di strategie ingenue e stabilisce un punto di partenza più sobrio per ulteriori analisi.

Una candela è una scala troppo piccola. Tali dati sono dominati dal rumore piuttosto che dalla struttura. Pertanto, compiamo il passo logico successivo: ampliamo le osservazioni e verifichiamo non le singole variazioni, bensì i movimenti medi.

Anziché considerare una singola barra, calcoliamo la variazione media del prezzo su un intervallo storico da 1 a 23 barre. Questo attenua le fluttuazioni casuali e ci permette di isolare la componente più stabile del movimento. Analogamente, formuliamo la variazione media futura del prezzo sull'orizzonte temporale da 1 a 9 barre. Passiamo quindi dalle osservazioni puntuali ai segnali aggregati.

L'implementazione è nettamente suddivisa in due blocchi. Il primo calcola le medie mobili delle variazioni passate.

# Add rolling mean features for the previous and future moves
for period in range(2, 24, 1):
    data_matrix[f'last_mean_{period:02d}'] = data_matrix['last'].rolling(window=period).mean()

La seconda - i cambiamenti futuri, con uno scostamento obbligatorio per impedire la fuga di informazioni dal futuro al passato. Questo è un punto cruciale: senza, l'analisi perde di significato.

for period in range(2, 10, 1):
    data_matrix[f'next_mean_{period}'] = data_matrix['next'].rolling(window=period).mean().shift(-(period-1))

Dopo il calcolo, le righe con valori mancanti vengono rimosse - una conseguenza inevitabile delle operazioni di rolling window.

# Remove rows with missing values created by rolling calculations
data_matrix.dropna(inplace=True)

Successivamente, viene costruita la matrice di correlazione di Pearson e da essa viene ricavata la sottomatrice necessaria: la dipendenza del futuro dal passato. Ecco la risposta alla domanda principale: il movimento medio ha potere predittivo?

correlation_matrix = data_matrix.corr('pearson')
# Match columns that begin with "next"
reg = r'^next.*$'
selected_cols = correlation_matrix.filter(regex=reg).columns
remaining_rows = correlation_matrix.index.difference(selected_cols)
correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]

In questo caso, la visualizzazione mediante Seaborn sotto forma di mappa di calore risulta particolarmente appropriata. Ciò consente di valutare rapidamente la struttura delle dipendenze sull'intera griglia dei parametri.

plt.figure(figsize=(12, 7))
plt.subplots_adjust(left=0.15, right=1, bottom=0.16, top=0.95)
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Means Last to Next Bars') 
plt.savefig('mean_to_bar.png')
plt.show()

Dal punto di vista metodologico, si tratta già di un esperimento più maturo. Ci stiamo allontanando dal semplice test barra per barra e spostandoci all'analisi degli effetti aggregati. Se il mercato presenta pattern deboli, è a questo livello che iniziano ad emergere.

Correlazione tra la variazione media del prezzo in un periodo e il movimento futuro

Il risultato ottenuto appare più interessante, ma la conclusione generale rimane riservata. Continuiamo a osservare valori di correlazione negativi. Ora però sono strutturati. Nella regione centrale della matrice (finestre di 8-14 barre nello storico e 5-8 barre nell'orizzonte temporale), l'effetto si intensifica e raggiunge i valori di −0.02…−0.03. Si tratta di un segnale di mean reversion debole ma costante.

La logica è piuttosto chiara. Se il mercato si muove in una direzione da un po' di tempo, allora c'è una maggiore probabilità che si verifichi una correzione parziale nelle barre successive. Tuttavia, l'effetto non è lineare:

  • in finestre di breve durata - affoga nel rumore;
  • su lunghezze eccessive si sfoca e perde intensità;
  • il massimo si manifesta nei range medi.

L'angolo inferiore destro della matrice merita una menzione a parte. Lì la correlazione tende a zero e in alcuni punti diventa addirittura positiva. Questo è un classico segnale di perdita di potere predittivo dovuta a un'eccessiva attenuazione: il segnale scompare insieme alla variabilità dei dati.

La visualizzazione tramite Seaborn evidenzia piuttosto bene il profilo dalla dipendenza in questo caso. L'immagine si rivela piuttosto significativa.

La conclusione è semplice: il mercato non mostra una forte inerzia, ma evidenzia un debole effetto di mean reversion. Questo non fornisce una strategia preconfezionata, ma ne imposta la direzione.

Poiché i valori di correlazione ottenuti sono bassi, tale segnale non può essere direttamente scalato per il trading in un contesto lineare. Ma è qui che inizia la parte più interessante del lavoro. Se una relazione semplice è debole, possiamo provare a rafforzarla combinando diverse feature - ovvero passando a un modello.

Il compito principale consiste nel definire uno spazio delle feature informativo. Una singola serie di prezzi non è sufficiente. Pertanto, il passo successivo consiste nella ricerca di ulteriori feature in grado di cogliere la struttura nascosta del mercato. Ha senso utilizzare gli indicatori tecnici classici come base di partenza. Si tratta di uno strumento collaudato che, nonostante la sua semplicità, fornisce spesso segnali utili.

In seguito, applichiamo lo stesso approccio rigoroso - la verifica tramite correlazione. Valutiamo la relazione tra le variazioni future dei prezzi e i valori di diversi indicatori. In questo caso, i parametri degli indicatori variano all'interno del ciclo. Questo ci permette di coprire immediatamente un'ampia gamma di configurazioni e di vedere dove il segnale è più forte.

Da un punto di vista pratico, il processo assomiglia all'enumerazione dei parametri seguita dal filtraggio. Ma in sostanza, si tratta della formazione e della selezione di uno spazio delle feature. In questo contesto, la correlazione funge da strumento diagnostico. Misuriamo quali trasformazioni dei dati sono associate a movimenti futuri. Si tratta di un livello di pensiero radicalmente diverso: prima si comprende la struttura, poi si estrae il profitto.

Nel codice, questo approccio è implementato in modo piuttosto sistematico. Da un lato, si utilizzano semplici derivati del prezzo - medie mobili, la loro prima e seconda derivata e le deviazioni dal valore corrente. Si tratta di un tentativo di cogliere le dinamiche locali e l'accelerazione del mercato.

# Recreate the base matrix for indicator engineering
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
# Add 11-period previous move averages and derived momentum features
data_matrix[f'last_mean_11'] = data_matrix['last'].rolling(window=11).mean()
data_matrix[f'Dlast_mean_11'] = data_matrix[f'last_mean_11'].diff()
data_matrix[f'DDlast_mean_11'] = data_matrix[f'Dlast_mean_11'].diff()
# Feature representing the gap between the rolling mean and the current move
data_matrix[f'last_last_11'] = data_matrix[f'last_mean_11'] - data_matrix['last']
data_matrix[f'Dlast_last_11'] = data_matrix[f'last_last_11'].diff()
data_matrix[f'DDlast_last_11'] = data_matrix[f'Dlast_last_11'].diff()
# Add short-term future sum targets for the next bars
for period in range(2, 10, 1):
    data_matrix[f'next_{period}'] = data_matrix['next'].rolling(window=period).sum().shift(-(period-1))

D'altro canto, vengono aggiunti gli indicatori classici della libreria TA: SMA, RSI, MACD, inoltre, in diverse parametrizzazioni contemporaneamente. Questa copertura ci impedisce di indovinare il periodo corretto, permettendoci invece di osservare il comportamento sull'intero intervallo.

# Build additional technical indicators using the close price series
close = pd.DataFrame(close[:-1], columns=['close'])
indicator_cols = {}
for period in [4, 8, 12, 24, 36, 48]:
    sma = ta.trend.sma_indicator(close['close'], window=period, fillna=True)
    dsma = sma.diff()
    ddsma = dsma.diff()
    rsi = ta.momentum.rsi(close['close'], window=period, fillna=True)
    drsi = rsi.diff()
    ddrsi = drsi.diff()
    macd = ta.trend.MACD(
        close['close'],
        window_slow=2 * period,
        window_fast=period,
        window_sign=period * 3 // 4,
        fillna=True,
    )
    macd_main = macd.macd()
    dmacd = macd_main.diff()
    ddmacd = dmacd.diff()
    macd_diff = macd.macd_diff()
    dmacd_diff = macd_diff.diff()
    ddmacd_diff = dmacd_diff.diff()
    macd_signal = macd.macd_signal()
    dmacd_signal = macd_signal.diff()
    ddmacd_signal = dmacd_signal.diff()
    macd_sig_main = macd_signal - macd_main
    dmacd_sig_main = macd_sig_main.diff()
    ddmacd_sig_main = dmacd_sig_main.diff()

    indicator_cols[f'SMA_{period:02d}'] = sma
    indicator_cols[f'DSMA_{period:02d}'] = dsma
    indicator_cols[f'DDSMA_{period:02d}'] = ddsma
    indicator_cols[f'RSI_{period:02d}'] = rsi
    indicator_cols[f'DRSI_{period:02d}'] = drsi
    indicator_cols[f'DDRSI_{period:02d}'] = ddrsi
    indicator_cols[f'MACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_main
    indicator_cols[f'DMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd
    indicator_cols[f'DDMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd
    indicator_cols[f'MACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_diff
    indicator_cols[f'DMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_diff
    indicator_cols[f'DDMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_diff
    indicator_cols[f'MACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_signal
    indicator_cols[f'DMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_signal
    indicator_cols[f'DDMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_signal
    indicator_cols[f'MACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_sig_main
    indicator_cols[f'DMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_sig_main
    indicator_cols[f'DDMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_sig_main

# Append all indicator columns to the feature matrix in one operation
# This avoids repeated DataFrame assignment and keeps the DataFrame compact
data_matrix = pd.concat([data_matrix, pd.DataFrame(indicator_cols)], axis=1)
# Remove any rows with NaN values created by indicator calculations
data_matrix.dropna(inplace=True)

L'aggiunta di derivate di secondo ordine è particolarmente indicativa (diff e diff da diff). Si tratta già di un tentativo di cogliere il cambiamento nel segnale stesso - una transizione verso l'analisi dell'accelerazione e della decelerazione del movimento del mercato. Nelle serie finanziarie, tali effetti si rivelano spesso più informativi dei livelli stessi.

Successivamente, viene applicato il filtro. Dall'intera matrice di correlazione, rimangono solo le feature la cui relazione massima con la variabile target supera la soglia specificata (in questo caso, 0.02). Questo è un punto importante. Eliminiamo consapevolmente le dipendenze deboli e instabili, lasciando solo quelle che rimangono all'interno dei dati almeno nella misura minima possibile.

correlation_matrix = data_matrix.corr('pearson')
selected_cols = correlation_matrix.filter(regex=reg).columns
remaining_rows = correlation_matrix.index.difference(selected_cols)
correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]
# Delete rows with low correlations
correlation_matrix = correlation_matrix[correlation_matrix.abs().max(axis=1) >= 0.02]

La visualizzazione tramite Seaborn completa il processo.

plt.figure(figsize=(12, 7))
plt.subplots_adjust(left=0.2, right=1, bottom=0.05, top=0.95)
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Indicators to Next Bars') 
plt.savefig('trend_to_bar.png')
plt.show()

La mappa di calore non appare più come un rumore caotico, ma si trasforma in una mappa di segnali. È chiaro quali gruppi di indicatori iniziano a reagire ai movimenti futuri, a quali parametri questa reazione aumenta e dove scompare completamente.

Correlazione degli indicatori con i futuri movimenti dei prezzi

È a questo punto che l'insieme delle ipotesi comincia ad assumere una struttura. Stiamo passando dalla selezione casuale degli indicatori alla formazione consapevole delle feature. Questo non è ancora un modello, ma piuttosto la sua struttura di base. Il compito successivo è quello di assemblare questi segnali deboli e disparati in un unico sistema capace di estrarre una dipendenza stabile, laddove, individualmente, è quasi invisibile.


4. Costruzione e addestramento del modello

Nella fase precedente, abbiamo ottenuto una mappa di correlazione e selezionato le feature con i valori massimi di correlazione diretta e inversa. Questo è un punto fondamentale: la correlazione negativa è lo stesso segnale, solo con segno opposto. In termini di modello, questa non è una limitazione, bensì un'informazione aggiuntiva.

Ora passiamo all'addestramento. L'ecosistema Scikit-Learn offre una vasta gamma di algoritmi - dai modelli lineari agli ensemble. L'articolo "Modelli di regressione della libreria Scikit-learn e la loro esportazione in ONNX" fornisce un confronto di 55 modelli di regressione. Ma per risolvere un problema pratico, ha senso concentrarsi su algoritmi robusti e collaudati. In questo caso, utilizzeremo RandomForestRegressor.

Il forest classico rappresenta un compromesso tra semplicità ed espressività. Funziona bene con dipendenze non lineari, è robusto al rumore e non richiede una normalizzazione aggressiva dei dati. Questo è esattamente ciò che serve nella prima fase della modellazione.

Il passo fondamentale successivo è la selezione degli iperparametri. Utilizziamo l'enumerazione diretta tramite due parametri: numero di alberi (n_estimators) e profondità dell'albero (max_depth). Si tratta di una scelta ragionevole: il primo parametro è responsabile della forza dell'ensamble, mentre il secondo si concentra sul grado di profondità di un singolo albero nei dati e sul controllo dell'overfitting.

A questo scopo, creiamo un nuovo script. L'algoritmo per la connessione a MetaTrader 5 e il caricamento dei dati storici sui prezzi rimane invariato, pertanto ometteremo la sua descrizione.

Dopo aver ricevuto i dati storici, inizia la formazione dello spazio delle feature. Viene costruita una matrice di base: differenze di prezzo, le loro versioni smussate e le feature derivate.

macd_settings = [(8,16,6),(12,24,9),(36,72,27),(48,96,36)]
features = []

# Build the base feature matrix from close price changes
close = pd.DataFrame(rates_frame['close'][:-1].to_numpy(dtype=float), columns=['close'])
diff = rates_frame['close'].diff().to_numpy(dtype=float)
# Pair consecutive differences into 'last' and 'next' columns
diff = np.column_stack((diff[:-1], diff[1:]))
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
features.append('last') # Add to features list for later use
# Add a 11-period rolling mean of the previous bar move
data_matrix['last_11'] = data_matrix['last'].rolling(window=11).mean()
features.append('last_11') # Add to features list for later use 
# Add the difference between the rolling mean and current bar move
data_matrix['last_last_11'] = data_matrix['last_11'] - data_matrix['last']
features.append('last_last_11') # Add to features list for later use

Successivamente, vengono aggiunti gli indicatori tecnici: SMA e una serie di configurazioni MACD.

# Add a 12-period simple moving average as a technical feature
data_matrix['SMA_12'] = ta.trend.sma_indicator(close['close'], window=12, fillna=True)
features.append('SMA_12') # Add to features list for later use

# Add MACD-based technical indicators for the selected parameter sets
for fast, slow, sign in macd_settings:
    macd = ta.trend.MACD(
        close['close'],
        window_slow=slow,
        window_fast=fast,
        window_sign=sign,
        fillna=True,
    )
    macd_main = macd.macd()
    dmacd = macd_main.diff()
    macd_signal = macd.macd_signal()
    dmacd_signal = macd_signal.diff()
    macd_sig_main = macd_signal - macd_main

    sufix = f"{fast:02d},{slow:02d},{sign:02d}"
    data_matrix[f'MACD_MAIN_{sufix}'] = macd_main
    features.append(f'MACD_MAIN_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_MAIN_{sufix}'] = dmacd
    features.append(f'DMACD_MAIN_{sufix}') # Add to features list for later use     
    data_matrix[f'MACD_SIGNAL_{sufix}'] = macd_signal
    features.append(f'MACD_SIGNAL_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_SIGNAL_{sufix}'] = dmacd_signal
    features.append(f'DMACD_SIGNAL_{sufix}') # Add to features list for later use
    data_matrix[f'MACD_Sig_Main_{sufix}'] = macd_sig_main
    features.append(f'MACD_Sig_Main_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_Sig_Main_{sufix}'] = macd_sig_main.diff()
    features.append(f'DMACD_Sig_Main_{sufix}') # Add to features list for later use

Tutte le caratteristiche vengono accumulate in sequenza in un'unica struttura dati, mentre i loro nomi sono definiti in un elenco di feature separato. Ciò semplifica il lavoro successivo ed elimina la perdita accidentale di variabili.

Allo stesso passaggio, viene formata la variabile target - il movimento totale del prezzo su un dato orizzonte temporale (next_9).

# Add a 9-period future return target for the next bars
data_matrix['next_9'] = data_matrix['next'].rolling(window=9).sum().shift(-8)

Il problema viene quindi formalizzato come una regressione: sulla base degli indicatori attuali, prevedere le future variazioni di prezzo.

Il passo successivo è la pulizia dei dati. Dopo aver applicato le finestre scorrevoli e la derivazione, inevitabilmente compaiono valori mancanti. Vengono rimossi per garantire un corretto addestramento del modello.

data_matrix.dropna(inplace=True)

Successivamente, i dati vengono suddivisi in base al tempo: il primo 90% viene utilizzato per l'addestramento, il restante 10% per il test. Questo è un punto di fondamentale importanza. A differenza dei classici problemi di apprendimento automatico, in questo caso non è consentito il data shuffling. Rispettiamo rigorosamente la cronologia, simulando un processo reale: prima il passato, poi il futuro.

# ===== 1) Data preparation =====
# Copy the raw feature matrix (preserves original data for later reference)
df = data_matrix.copy()

# Keep only features that are actually present in the DataFrame
features = [c for c in features if c in data_matrix.columns]

df = df[features + ["next_9"]]
X = df[features]
y = df["next_9"]

# ===== 2) Time-based split =====
split_idx = int(len(X) * 0.9)

X_train = X.iloc[:split_idx]
X_test = X.iloc[split_idx:]
y_train = y.iloc[:split_idx]
y_test = y.iloc[split_idx:]

Dopodiché, viene avviato un ciclo di enumerazione degli iperparametri del modello RandomForestRegressor. Per ogni combinazione di parametri, viene eseguito un ciclo completo:

  • il modello viene addestrato su un campione di addestramento;
  • # ===== 3) Model =====
    results = []
    for est in range(60, 111, 5):
        for dep in range(2, 14, 1):
            print(f"\n=== Estimators: {est}, Max Depth: {dep} ===")
            model = RandomForestRegressor(
                n_estimators = est,
                max_depth = dep,
                max_leaf_nodes = None,
                min_samples_split = 6,
                min_samples_leaf = 3,
                bootstrap = True,
                random_state = 42,
                n_jobs = -1
                )
    
            model.fit(X_train, y_train)
  • elabora le previsioni per le parti di addestramento e di test, i risultati vengono portati a una forma stabile (gestendo NaN e infiniti);
  •         # ===== 4) Evaluation =====
            pred_train=np.nan_to_num(model.predict(X_train), nan=0.0, posinf=0.0, neginf=0.0)
            pred_test = np.nan_to_num(model.predict(X_test), nan=0.0, posinf=0.0, neginf=0.0)
  • Vengono calcolate le metriche della qualità.
  •         pt_corr = np.corrcoef(pred_test, y_test)[0, 1]
            results.append((est, dep, pt_corr))
            print("Train R2:", round(r2_score(y_train, pred_train), 6))
            print("Test  R2:", round(r2_score(y_test, pred_test), 6))
            print("Test MAE:", round(mean_absolute_error(y_test, pred_test), 8))
            print("Pred/Target corr:", round(pt_corr, 6))
    

La metrica chiave è la correlazione tra la previsione e il valore effettivo sul campione di prova. È proprio questo che riflette la capacità del modello di catturare la direzione del movimento. e MAE vengono inoltre calcolati per controllare la qualità complessiva dell'approssimazione e il livello di errore.

Tutti i risultati vengono memorizzati in una tabella, in cui ogni riga corrisponde a una specifica combinazione di iperparametri. Successivamente, questa tabella viene convertita in una forma matriciale, che ci permette di visualizzare la dipendenza della qualità del modello dai parametri.

# --- results -> DataFrame ---
df_results = pd.DataFrame(
    results,
    columns=["Estimators", "Max Depth", "Test Correlation"]
)

# --- pivot table ---
heatmap_data = df_results.pivot(
    index="Estimators",
    columns="Max Depth",
    values="Test Correlation"
).sort_index()

Il passaggio finale consiste nella visualizzazione tramite Seaborn. La mappa di calore mostra chiaramente in quale area di parametri si ottiene il risultato migliore. Questo ci permette non solo di selezionare la configurazione ottimale, ma anche di valutare la stabilità del modello - ovvero quanto la qualità cambia con piccole variazioni dei parametri.

# --- heatmap ---
plt.figure(figsize=(14, 10))
sns.heatmap(
    heatmap_data,
    annot=True,
    fmt=".4f",
    cmap="coolwarm",
    linewidths=0.5,
    cbar_kws={"label": "Test Correlation"}
)

plt.title("Heatmap of Test Correlation by n_estimators and max_leaf_nodes")
plt.xlabel("Max Nodes")
plt.ylabel("Estimators")
plt.tight_layout()
plt.show()

Trovare il rapporto ottimale tra il numero di alberi e la profondità massima

Emerge qui un punto importante: il modello non crea un segnale dal nulla. Aggrega soltanto le deboli dipendenze individuate in precedenza. Se non ci fosse nemmeno un accenno di struttura nella fase di analisi, nessun modello salverebbe la situazione. Ma se è presente un segnale, anche se debole, l'ensamble è in grado di amplificarlo e renderlo utilizzabile in modo pratico.

Nel grafico della prima iterazione di selezione degli iperparametri, la regione con max_depth = 10 è chiaramente visibile. Questo valore dimostra l'equilibrio più stabile tra la capacità del modello di cogliere le dipendenze e controllare l'overfitting. In effetti, è qui che si raggiunge la modalità operativa quando il modello non è più primitivo, ma non inizia ancora ad adattarsi al rumore.

Poi la logica si sviluppa in modo naturale. Dopo aver definito max_depth, passiamo alla seconda fase - la configurazione della struttura dell’albero tramite max_leaf_nodes. Parallelamente, restringiamo l'intervallo in base al numero di alberi (n_estimators), lasciando solo l'area in cui sono stati precedentemente osservati risultati stabili. Ciò ci consente di aumentare la risoluzione della ricerca: il passo di ricerca si riduce e l'attenzione si concentra sull'area dei parametri realmente significativi.

Questo approccio ricorda la procedura classica di ottimizzazione locale. Innanzitutto, viene determinata un'area approssimativa del massimo, quindi si procede con una messa a punto più precisa al suo interno. Di conseguenza, evitiamo di sprecare risorse di calcolo su configurazioni palesemente deboli e raggiungiamo rapidamente una combinazione stabile di parametri.

Le modifiche al codice sono mirate: viene specificato l'intervallo di ricerca e viene modificato il secondo parametro. L'architettura dello script rimane invariata.

Trovare il rapporto ottimale tra la dimensione della foresta e il numero di nodi

Dopo la selezione, definiamo gli iperparametri ottimali e procediamo all'addestramento finale del modello. Dal punto di vista strutturale, non cambia nulla: i cicli di iterazione vengono rimossi dallo script e i valori dei parametri vengono specificati direttamente durante l'inizializzazione di RandomForestRegressor. Tutte le altre logiche - la preparazione dei dati, la suddivisione dei campioni, l'addestramento e la convalida di base, rimangono invariate.

Successivamente, emerge un punto più sottile ma fondamentale. Il modello non è ugualmente affidabile in tutte le sue previsioni. In alcuni casi fornisce un segnale forte, in altri i valori sono vicini allo zero e rientrano nella zona di incertezza. Se tutte le previsioni vengono trattate allo stesso modo, la strategia inevitabilmente inizia a fare trading sul rumore.

Ciò porta a un'ipotesi naturale: ignorare i segnali deboli e operare solo laddove il modello dimostri sufficiente affidabilità o il movimento previsto copra i costi. Si tratta già di una transizione dal modello funzionale al modello di filtro di trading.

Il codice implementa questa idea in modo ordinato e senza inutili complicazioni. Sulla base del campione di addestramento, vengono calcolati i valori di soglia delle previsioni assolute.

# ===== 5) Simple PnL prototype =====
# Calculate strategy metrics for a vector of thresholds without an explicit loop
percentiles = np.arange(10, 100, 5)
thresholds = np.percentile(np.abs(pred_train), percentiles)

Questo è un punto importante: le soglie vengono determinate durante l'addestramento e applicate al test, il che mantiene la validità dell'esperimento.

Successivamente, viene creata una matrice di previsioni e relative soglie. Per ogni livello viene calcolata una maschera - che definisce quali segnali passano il filtro. La posizione è determinata dal segno della previsione, tenendo conto della correlazione generale, il che consente di rendere la direzione del trading coerente con la natura del modello.

# Build a matrix where each column repeats the test predictions
pred_matrix = np.tile(pred_test[:, None], (1, thresholds.size))
threshold_matrix = thresholds[None, :]

# Generate a mask per threshold and compute sign positions
mask = np.abs(pred_matrix) >= threshold_matrix
position = np.sign(pred_matrix) * np.sign(pt_corr) * mask.astype(float)

Dopodiché, la redditività si forma come prodotto della posizione e del movimento effettivo. I costi fissi (spread / commissioni) vengono sottratti e si costruisce la curva cumulativa del capitale.

# Broadcast y_check to match the threshold matrix shape
y_check_matrix = np.tile(y_check.values[:, None], (1, thresholds.size))
# Subtracting swap cost from the target to get a more realistic PnL estimate
strategy_ret = position * y_check_matrix - np.abs(position)*(0.00021)

# Compute equity curves for each threshold column
equity = np.cumsum(strategy_ret, axis=0)

I risultati sono aggregati in una tabella:

  • final_equity — redditività finale;
  • mean_return — risultato medio dei trade;
  • win_rate — percentuale di entrate redditizie.
# Aggregate results into a DataFrame
results = pd.DataFrame({
    'percentile': percentiles,
    'threshold': thresholds,
    'final_equity': equity[-1, :],
    'mean_return': np.sum(strategy_ret, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9),
    'win_rate': np.sum(strategy_ret > 0, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9)
})

print(results.to_string(index=False, float_format='%.8f'))

Da un punto di vista pratico, non si tratta più solo di una valutazione di un modello, ma dell'inizio di un sistema di trading. Non solo verifichiamo l'accuratezza delle previsioni, ma valutiamo anche immediatamente come queste vengono monetizzate a diversi livelli di ordinamento.


5. Trasformazione in ONNX

Abbiamo addestrato il modello. Il passo logico successivo è quello di escludere la persona dal processo decisionale. Il trading manuale basato su segnali di modelli è quasi sempre inferiore al trading automatizzato: manca la continuità, si perde la velocità di reazione e si aggiunge il fattore psicologico. Nella pratica, questo si traduce in distorsioni sistemiche - mancate opportunità di ingresso, uscite premature, sfiducia nel proprio modello.

La piattaforma MetaTrader 5 offre due percorsi di automazione. Il primo metodo consiste nell'avviare lo script Python con l'esecuzione diretta delle operazioni di trading. Il secondo metodo consiste nel convertire il modello nel formato ONNX per il successivo utilizzo nell'EA MQL5. In pratica, la seconda opzione appare più matura.

Il formato ONNX risolve diversi problemi contemporaneamente. Il modello è fissato in una forma compatta e indipendente. Può essere facilmente trasferito tra computer - tutto ciò che serve è il terminale stesso. Nel tester di strategia diventa possibile eseguire test completi. E non si registra alcuna perdita di prestazioni: il terminale supporta l'accelerazione hardware, incluso il funzionamento con GPU (CUDA), aspetto particolarmente importante quando si utilizzano modelli ensemble.

La conversione è piuttosto semplice. Innanzitutto, viene descritto l'input del modello - ovvero la dimensione dello spazio delle feature.

# Number of features used for model input
n_features = X_train.shape[1]

# Describe the model input shape for ONNX conversion
initial_type = [("float_input", FloatTensorType([None, n_features]))]

Successivamente, il modello addestrato con Scikit-Learn viene trasformato in ONNX tramite l'apposito convertitore e salvato su disco.

# Convert the trained sklearn model to ONNX format
onnx_model = convert_sklearn(model, initial_types=initial_type)

# Save the ONNX model to disk
with open(onnx_model_path, "wb") as f:
    f.write(onnx_model.SerializeToString())

A questa fase segue la fase di validazione obbligatoria. Il modello viene caricato tramite ONNX Runtime e gli stessi dati vengono utilizzati per calcolare i valori previsti.

# Load the ONNX model for inference
sess = rt.InferenceSession(onnx_model_path)

input_name = sess.get_inputs()[0].name

# ONNX runtime expects float32 input arrays
X_test_np = X_test.astype(np.float32).values

onnx_preds = sess.run(None, {
    input_name: X_test_np
})[0].ravel()

Vengono quindi confrontati con i risultati originali del modello Sklearn.

# Compare ONNX predictions with sklearn predictions
sk_preds = model.predict(X_test)

print("Correlation:", np.corrcoef(sk_preds, onnx_preds)[0, 1])
print("Max diff:", np.max(np.abs(sk_preds - onnx_preds)))

Ci sono due criteri fondamentali:

  • la correlazione tra le previsioni dovrebbe tendere a 1;
  • La discrepanza massima deve essere trascurabile.

Se queste condizioni vengono soddisfatte, possiamo presumere che il trasferimento sia andato a buon fine e che il modello sia pronto per l'integrazione nel sistema di trading.

Da un punto di vista pratico, questa rappresenta la transizione definitiva dall'ambiente di ricerca Python all'utilizzo pratico. Il modello cessa di essere un esperimento e diventa parte dell'infrastruttura - autonomo, riproducibile e adatto alla sperimentazione e al trading reale.


6. Test nel tester di strategia

Una volta completato il lavoro sul lato Python, la logica viene trasferita all'ambiente di runtime MetaTrader 5. In questo caso, il modello cessa di essere uno strumento di ricerca e diventa parte di un algoritmo di trading. È importante che la struttura del codice segua il flusso operativo già noto: inizializzazione → preparazione dei dati → previsione → decisione di trading.

L'inizializzazione viene eseguita nel metodo OnInit. In questa fase, il modello ONNX dalla risorsa viene caricato e l'ambiente di runtime viene creato tramite OnnxCreateFromBuffer.

int OnInit()
  {
//---
   if(!Symb.Name("EURUSD_i"))
      return INIT_FAILED;
   Symb.Refresh();
//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      return INIT_FAILED;
//--- load models
   onnx = OnnxCreateFromBuffer(model, ONNX_DEFAULT);
   if(onnx == INVALID_HANDLE)
     {
      Print("OnnxCreateFromBuffer error ", GetLastError());
      return INIT_FAILED;
     }
   const ulong input_state[] = {1, Inputs.Size()};
   if(!OnnxSetInputShape(onnx, 0, input_state))
     {
      Print("OnnxSetInputShape error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }
   const ulong output_forecast[] = {1, Forecast.Size()};
   if(!OnnxSetOutputShape(onnx, 0, output_forecast))
     {
      Print("OnnxSetOutputShape error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }

In seguito, vengono impostate esplicitamente le forme di input e output - questo è fondamentale, poiché il modello si aspetta un numero di feature rigorosamente fisso. Un errore in questa fase comporterà un'inferenza errata.

L'indicatore SMA e il MACD, impostati con gli stessi parametri utilizzati durante l'addestramento, vengono inizializzati in parallelo.

//--- Indicators
   if(!ciSMA.Create(Symb.Name(), TimeFrame, 12, 0, MODE_SMA, PRICE_CLOSE))
     {
      Print("SMA create error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }
   ciSMA.BufferResize(2);
   for(uint i = 0; i < ciMACD.Size(); i++)
     {
      if(!ciMACD[i].Create(Symb.Name(), TimeFrame, int(macd_set[i, 0]),
                int(macd_set[i, 1]), int(macd_set[i, 2]), PRICE_CLOSE))
        {
         PrintFormat("MACD %d create error %d", i, GetLastError());
         OnnxRelease(onnx);
         return INIT_FAILED;
        }
      ciMACD[i].BufferResize(4);
     }
//---
   return(INIT_SUCCEEDED);
  }

Questo è un punto fondamentale: le feature in MQL5 devono essere identiche a quelle utilizzate per addestrare il modello. Qualsiasi discrepanza compromette la capacità predittiva.

La logica principale è concentrata nel metodo OnTick, ma con il filtro IsNewBar per l'evento di apertura della nuova barra. Ciò impedisce che il modello venga ricalcolato a ogni tick e sincronizza i calcoli con il timeframe.

void OnTick()
  {
//---
   if(!IsNewBar())
      return;

Segue poi la sezione dedicata alla contabilizzazione delle posizioni correnti - una semplice aggregazione di volumi e profitti per direzione. Ciò è necessario per controllare le operazioni già aperte.

   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   int total = PositionsTotal();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
     }

Il vettore dei dati iniziali di input viene formato successivamente. In sostanza, il Feature Engineering di Python viene riprodotto manualmente qui:

  • feature di base;
  • //--- prepare input data
       ciSMA.Refresh();
       for(uint i = 0; i < ciMACD.Size(); i++)
          ciMACD[i].Refresh();
       if(!Rates.CopyRates(Symb.Name(), TimeFrame, COPY_RATES_CLOSE, 1, 12))
         {
          Print("CopyRates error ", GetLastError());
          return;
         }
       Inputs[0] = float(Rates[11] - Rates[10]);
       Inputs[1] = float(Rates[11] - Rates[0]) / 11;
       Inputs[2] = float(Inputs[1] - Inputs[0]);
    
  • SMA ;
  •    Inputs[3] = float(ciSMA.Main(1));
  • Blocchi MACD e loro derivati.
  •    for(uint i = 0; i < ciMACD.Size(); i++)
         {
          Inputs[4 + i * 6] = float(ciMACD[i].Main(1));
          Inputs[5 + i * 6] = float(Inputs[4 + i * 6] - ciMACD[i].Main(2));
          Inputs[6 + i * 6] = float(ciMACD[i].Signal(1));
          Inputs[7 + i * 6] = float(Inputs[6 + i * 6] - ciMACD[i].Signal(2));
          Inputs[8 + i * 6] = Inputs[6 + i * 6] - Inputs[4 + i * 6];
          Inputs[9 + i * 6] = Inputs[7 + i * 6] - Inputs[5 + i * 6];
         }
    

Si prega di notare l'indicizzazione: ogni feature occupa una posizione ben definita. Si tratta di un contratto tra modello ed esecuzione. Se l'ordine viene violato, il modello inizia a funzionare con input distorti.

Dopo aver preparato le feature, viene richiamato OnnxRun.

//--- run the inference
   if(!OnnxRun(onnx, ONNX_LOGLEVEL_INFO, Inputs, Forecast))
     {
      Print("OnnxRun error ", GetLastError());
      return;
     }

L’output è una previsione - l'andamento previsto del prezzo. Ora viene la parte pratica - l'interpretazione dei segnali.

Il codice utilizza una logica semplice ma efficace:

  • Viene introdotto un valore di soglia per bloccare i segnali deboli. Lo ricaviamo dai risultati dell’addestramento.
  • Viene presa in considerazione la direzione della correlazione, consentendo di invertire il modello se necessario.
  • Se la previsione supera la soglia, viene aperta una posizione. Se il segnale scompare, la posizione viene chiusa.
   Symb.Refresh();
   Symb.RefreshRates();
   double min_lot = Symb.LotsMin();
   double step_lot = Symb.LotsStep();
   double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point();
//--- buy control
   if(Forecast[0]*direction >= threshold)
     {
      double buy_lot = min_lot;
      if(buy_value <= 0)
         Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), 0, 0);
     }
   else
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }
//--- sell control
   if(Forecast[0]*direction <= -threshold)
     {
      double sell_lot = min_lot;
      if(sell_value <= 0)
         Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), 0, 0);
     }
   else
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
  }

Pertanto, il modello viene utilizzato come filtro di movimento direzionale. Questa è una distinzione importante: non facciamo trading per ogni valore previsto, ma solo con quelli che superano la soglia di forza del segnale.

L'EA risultante viene quindi sottoposto a una verifica fondamentale - un test nel tester di strategie di MetaTrader 5 su dati storici relativi al primo trimestre del 2026. Non si tratta più di una valutazione astratta del modello, bensì di uno scenario di implementazione vicino alla realtà.

Questo formato di test è di fondamentale importanza. Mentre nella fase Python abbiamo valutato il modello tramite metriche, qui viene testato l'intero sistema - dalla generazione delle feature alla logica di apertura e chiusura delle posizioni. Di fatto, la strategia viene sottoposta al suo primo test in condizioni che riproducono il più fedelmente possibile quelle reali.

La fase di test completa il ciclo di sviluppo: dall'ipotesi e dall'analisi dei dati alla creazione di un modello, fino all'automazione e alla verifica su dati storici. È a questo punto che diventa chiaro se le deboli correlazioni statistiche siano state trasformate con successo in uno strumento di trading pratico.

Tuttavia, l'integrazione del modello ONNX in un EA non è l'unico caso d'uso. Per gli appassionati di trading manuale, MetaTrader 5 offre la possibilità di incorporare un modello direttamente in un indicatore personalizzato. In questo caso, il modello non prende decisioni per il trader, ma funge da strumento analitico, generando segnali che l'utente interpreta autonomamente.

Dal punto di vista ingegneristico, non ci sono praticamente differenze. Le meccaniche di connessione del modello ONNX, la preparazione dei dati iniziali e la chiamata di inferenza sono completamente identiche all'implementazione nell'EA. Cambia solo il punto di applicazione: invece di aprire automaticamente le posizioni, il risultato del modello viene visualizzato su un grafico o utilizzato come filtro aggiuntivo nel processo decisionale.

Indicatore con RandomForest

Questo approccio ha i suoi vantaggi. Consente una combinazione flessibile dei segnali del modello con l'analisi classica e riduce i requisiti di affidabilità dell'algoritmo - il modello diventa un assistente, non l'unica fonte decisionale.


Conclusioni

L'integrazione di Python e MetaTrader 5 crea un framework completo e collaudato in ambito ingegneristico per lo sviluppo di soluzioni di trading - dall'ideazione all'implementazione pratica. In questo articolo, abbiamo percorso questo cammino in modo sequenziale: dall'acquisizione e analisi dei dati, passando per la verifica delle ipotesi e la costruzione del modello, fino alla sua implementazione e al test in un ambiente di esecuzione reale.

Il vantaggio principale di questo approccio risiede nella separazione dei ruoli. Python si occupa della parte di ricerca: elaborazione dei dati, generazione delle feature, analisi statistica e addestramento del modello. MetaTrader 5, a sua volta, fornisce l'esecuzione: accesso ai dati di mercato, test delle strategie e infrastruttura di trading. Si tratta di una classica combinazione di flusso laboratorio - produzione, in cui ogni ambiente viene utilizzato per lo scopo previsto.

L'utilizzo del formato ONNX offre un valore aggiunto. Il modello diventa portatile, indipendente dall'ambiente di sviluppo e pronto per essere eseguito su qualsiasi dispositivo dotato di terminale. Ciò semplifica la scalabilità, velocizza i test e riduce i rischi associati all'incompatibilità tra gli ambienti.

Programmi utilizzati nell'articolo

# Nome Tipo Descrizione
1 Experts\Integration\Integration.mq5 Expert Advisor EA per testare il modello nel terminale
2 Indicators\Integration\Integration.mq5 Indicatore Indicatore per la visualizzazione di segnali su un grafico
3 Scripts\Integration\load_data.py Script Script di caricamento dati
4 Scripts\Integration\look_model_param_rf.py Script Script di enumerazione degli iperparametri
5 Scripts\Integration\create_model_rf.py Script Script per addestrare il modello ed esportarlo in ONNX

Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/22020

File allegati |
MQL5.zip (13.99 KB)
Ultimi commenti | Vai alla discussione (2)
Denis Kirichenko
Denis Kirichenko | 17 apr 2026 a 11:17

Nel file di script load_data.py, contenuto nell'archivio, sono presenti le seguenti righe:

#  Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD_i", mt5.TIMEFRAME_H1, utc_from, utc_to)

mentre nell'articolo stesso:

#  Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

È una cosa da poco, ma non me ne sono accorto subito durante il test...

Poi ho dovuto rinunciare alla versione 3.14.3 di Python. Lavoro con Python in VS. Lì il debug è possibile solo con la versione 3.11.
Gloria Diana
Gloria Diana | 4 mag 2026 a 16:55
Apprezziamo questa risorsa
Arriva il Nuovo MetaTrader 5 e MQL5 Arriva il Nuovo MetaTrader 5 e MQL5
Questa è solo una panoramica di MetaTrader 5. Non posso descrivere tutte le nuove funzionalità del sistema per un periodo di tempo così breve: i test sono iniziati il 09.09.2009. Questa è una data simbolica e sono sicuro che sarà un numero fortunato. Sono passati alcuni giorni da quando ho ricevuto la versione beta del terminale MetaTrader 5 e MQL5. Non sono riuscito a provare tutte le sue funzionalità, ma sono già sorpreso.
Trading algoritmico senza routine: Analisi rapida dei trade in MetaTrader 5 con SQLite Trading algoritmico senza routine: Analisi rapida dei trade in MetaTrader 5 con SQLite
L'articolo presenta un set minimo di funzionalità per la gestione di un registro di trading in MQL5 utilizzando SQLite: una struttura di tabelle per transazioni, segnali, eventi, indici, istruzioni preparate e gestione delle operazioni, nonché query SQL analitiche standard. Vengono illustrate l'integrazione con la dashboard delle statistiche in MetaTrader 5 e il lavoro con il database tramite MetaEditor. Questo approccio consente di automatizzare il registro, accelerare i calcoli ed eseguire analisi senza complicare il codice dell'EA.
Utilizza i canali MQL5.community e le chat di gruppo Utilizza i canali MQL5.community e le chat di gruppo
Il sito web MQL5.com riunisce trader di tutto il mondo. Gli utenti pubblicano articoli, condividono codici gratuiti, vendono prodotti nel Market, offrono servizi da freelance e copiano segnali di trading. Puoi comunicare con loro sul Forum, nelle chat dei trader e nei canali MetaTrader.
Ottimizzazione del portafoglio nel Forex: Integrazione tra VaR e teoria di Markowitz Ottimizzazione del portafoglio nel Forex: Integrazione tra VaR e teoria di Markowitz
Come funziona il trading di portafoglio sul Forex? Come si possono sintetizzare la teoria del portafoglio di Markowitz per l'ottimizzazione delle proporzioni del portafoglio e il modello VaR per l'ottimizzazione del rischio di portafoglio? Creiamo un codice in base alla teoria del portafoglio, che da un lato ci consentirà di ottenere un basso rischio e, dall'altro, una redditività accettabile a lungo termine.