Sistema di trading di arbitraggio ad alta frequenza in Python utilizzando MetaTrader 5
Introduzione
Mercato dei cambi. Strategie algoritmiche. Python e MetaTrader 5. Tutto questo è nato quando ho iniziato a lavorare su un sistema di trading di arbitraggio. L'idea era semplice - creare un sistema ad alta frequenza per individuare gli squilibri di prezzo. A cosa ha portato tutto questo alla fine?
In questo periodo ho utilizzato più spesso le API MetaTrader 5. Ho deciso di calcolare tassi sintetici dei cross. Ho deciso di non limitarmi a dieci o cento. Il numero ha superato il migliaio.
La gestione del rischio era un compito separato. Architettura del sistema, algoritmi, processo decisionale - qui analizzeremo tutto. Mostrerò i risultati del backtesting e del trading live. E naturalmente condividerò idee per il futuro. Chissà, forse qualcuno di voi vuole sviluppare ulteriormente questo argomento? Spero che il mio lavoro sia richiesto. Vorrei credere che contribuirà allo sviluppo del trading algoritmico. Forse qualcuno lo prenderà come base per creare qualcosa di ancora più efficace nel mondo dell'arbitraggio ad alta frequenza. Dopo tutto, questa è l'essenza della scienza - progredire sulla base dell'esperienza dei predecessori. Andiamo subito al punto.
Introduzione al trading di arbitraggio sul Forex
Cerchiamo di capire cosa sia realmente.
Si può fare un'analogia con il cambio di valuta. Diciamo che potete acquistare USD in cambio di EUR in un posto, venderli immediatamente in GBP in un altro, e poi scambiare nuovamente GBP in EUR e ottenere un profitto. Questo è l'arbitraggio nella sua forma più semplice.
In realtà, è un po' più complicato. Il Forex è un mercato enorme e decentralizzato. Qui ci sono un gran numero di banche, broker e fondi. E ognuno ha i propri tassi di cambio. Il più delle volte non corrispondono. È qui che abbiamo un'opportunità di arbitraggio. Ma non pensate che si tratti di soldi facili. In genere queste discrepanze di prezzo durano solo pochi secondi. O addirittura millisecondi. È quasi impossibile arrivare in tempo. Ciò richiede computer potenti e algoritmi veloci.
Esistono anche diversi tipi di arbitraggio. Un esempio semplice è quello di trarre profitto dalla differenza dei tassi in luoghi diversi. Un caso complesso è quello dell'utilizzo dei tassi dei cross. Ad esempio, calcoliamo quanto costerà una sterlina in USD e in EUR e lo confrontiamo con il tasso di cambio diretto GBP/EUR.
L'elenco non finisce qui. Esiste anche l'arbitraggio temporale. In questo caso si trae profitto dalla differenza dei prezzi in momenti diversi. Comprato ora, venduto in un minuto. Naturalmente, il processo sembra semplice. Ma il problema principale è che non sappiamo dove andrà il prezzo tra un minuto. Questi sono i rischi principali. Il mercato potrebbe invertirsi più velocemente di quanto tu possa attivare l'ordine desiderato. Oppure il vostro broker potrebbe ritardare l'esecuzione degli ordini. In generale, le difficoltà e i rischi sono molti. Nonostante tutte le difficoltà, l'arbitraggio Forex è un sistema piuttosto popolare. Ci sono risorse finanziarie importanti in gioco e un numero sufficiente di trader specializzati solo in questo tipo di trading.
Ora, dopo una breve introduzione, passiamo alla nostra strategia.Panoramica delle tecnologie utilizzate: Python e MetaTrader 5
Quindi, Python e MetaTrader 5.
Python è un linguaggio di programmazione versatile e di facile comprensione. Non per niente è preferito sia dagli sviluppatori alle prime armi che da quelli più esperti. E si presta al meglio per l'analisi dei dati.
Dall’altra parte, MetaTrader 5. Si tratta di una piattaforma familiare a tutti i trader Forex. È affidabile e non complicata. Ed è anche abbastanza funzionale - quotazioni in tempo reale, robot di trading e analisi tecnica. Tutto in un'unica applicazione. Per ottenere risultati positivi, dobbiamo combinare tutto questo.
Python prende i dati da MetaTrader 5, li gestisce utilizzando le sue librerie e poi invia i comandi a MetaTrader 5 per eseguire le operazioni. Naturalmente, ci sono delle difficoltà. Ma insieme queste applicazioni sono molto efficienti.
È disponibile una libreria speciale degli sviluppatori per lavorare con MetaTrader 5 da Python. Per attivarla, è sufficiente installarla. Dopo aver fatto questo, siamo in grado di ricevere quotazioni, inviare ordini e gestire posizioni. Tutto è come nel terminale stesso, solo che ora vengono utilizzate anche le funzionalità di Python.
Quali caratteristiche e capacità sono ora disponibili? Ora ce ne sono parecchie. Ad esempio, siamo in grado di automatizzare il trading e di condurre analisi complesse dei dati storici. Possiamo anche creare la nostra piattaforma di trading. Questo è già un compito per utenti avanzati, ma è anche possibile.
Impostazione dell'ambiente: installazione delle librerie necessarie e connessione a MetaTrader 5
Inizieremo il nostro flusso di lavoro con Python. Se non lo avete ancora, visitate il sito python.org. È inoltre necessario impostare il consenso ADD TO PATCH.
Il nostro prossimo passo sono le librerie. Ne avremo bisogno alcune. La principale è MetaTrader 5. L'installazione non richiede competenze particolari.
Aprire la riga di comando e digitare:
pip install MetaTrader5 pandas numpy
Premete Invio e andate a bere un caffè. O il tè. O quello che preferite.
È tutto pronto? Ora è il momento di collegarsi a MetaTrader 5.
La prima cosa da fare è installare MetaTrader 5 stessa. Scaricatela dal vostro broker. Assicuratevi di ricordare il percorso del terminale. In genere si presenta così: "C:\ProgramFiles\MetaTrader 5\terminal64.exe".
Ora aprite Python e digitate:
import MetaTrader5 as mt5 if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"): print("Alas! Failed to connect :(") mt5.shutdown() else: print("Hooray! Connection successful!")
Se tutto si avvia, passare alla parte successiva.
Struttura del codice: funzioni principali e loro scopo
Cominciamo con le "importazioni". Qui abbiamo importazioni, come ad esempio: MetaTrader5, pandas, datetime, pytz... Poi ci sono le funzioni.
- La prima funzione è remove_duplicate_indices. Assicura che non ci siano duplicati nei nostri dati.
- Segue get_mt5_data. Accede alle funzioni di MetaTrader 5 ed estrae i dati richiesti per le ultime 24 ore.
- get_currency_data - funzione molto interessante. Richiama get_mt5_data per un gruppo di coppie di valute. AUDUSD, EURUSD, GBPJPY e molte altre coppie.
- La prossima è calculate_synthetic_prices. Questa funzione è una vero e proprio traguardo. Produce centinaia di prezzi sintetici mentre gestisce le coppie di valute.
- analyze_arbitrage cerca opportunità di arbitraggio confrontando i prezzi reali con quelli sintetici. Tutti i risultati vengono salvati in un file CSV.
- open_test_limit_order - un'altra potente unità di codice. Quando viene individuata un'opportunità di arbitraggio, questa funzione apre un ordine di prova. Ma non più di 10 operazioni aperte contemporaneamente.
Infine, la funzione "main". Gestisce l'intero processo chiamando le funzioni nel giusto ordine.
Tutto si conclude con un ciclo infinito. Esegue l'intero ciclo ogni 5 minuti, ma solo durante l'orario di lavoro. Questa è la struttura che abbiamo. È semplice, ma efficiente.
Ottenere dati da MetaTrader 5: funzione get_mt5_data
Il primo compito è quello di ricevere i dati dal terminale.
if not mt5.initialize(path=terminal_path): print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}") return None timezone = pytz.timezone("Etc/UTC") utc_from = datetime.now(timezone) - timedelta(days=1)
Notare che utilizziamo l'UTC. Perché nel mondo del Forex non c'è spazio per la confusione del fuso orario.
Ora la cosa più importante è ottenere i tick:
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL) I dati sono stati ricevuti? Benissimo! Ora dobbiamo gestirli. Per farlo, utilizziamo pandas:
ticks_frame = pd.DataFrame(ticks) ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')
Voilà! Ora abbiamo il nostro DataFrame con i dati. È già pronto per l'analisi.
Ma cosa succede se qualcosa va storto? Non preoccupatevi! La nostra funzione copre anche questo aspetto:
if ticks is None: print(f"Failed to fetch data for {symbol}") return None
Segnalerà semplicemente un problema e restituirà None.
Gestione di più coppie di valute: funzione get_currency_data
Ci immergiamo ulteriormente nel sistema - la funzione get_currency_data. Diamo un'occhiata al codice:
def get_currency_data(): # Define currency pairs and the amount of data symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"] count = 1000 # number of data points for each currency pair data = {} for symbol in symbols: df = get_mt5_data(symbol, count, terminal_path) if df is not None: data[symbol] = df[['time', 'bid', 'ask']].set_index('time') return data
Tutto inizia con la definizione delle coppie di valute. La lista comprende AUDUSD, EURUSD, GBPJPY e altri strumenti a noi ben noti.
Ora passiamo alla fase successiva. La funzione crea un dizionario "data" vuoto. In un secondo momento, inoltre, verrà riempito con i dati necessari.
Ora la nostra funzione inizia il suo lavoro. Scorrerà l'elenco delle coppie di valute. Per ogni coppia, chiama get_mt5_data. Se get_mt5_data restituisce dati (e non None), la nostra funzione prende solo i più importanti: time, bid e ask.
Ed ecco, infine, il gran finale. La funzione restituisce un dizionario pieno di dati.
Ora arriviamo a get_currency_data. È piccolo, potente, semplice ma efficace.
Calcolo di 2000 prezzi sintetici: Strategia e implementazione
Ci immergiamo nelle basi del nostro sistema - la funzione calculate_synthetic_prices. Ci permette di ottenere i nostri dati sintetici.
Diamo un'occhiata al codice:
def calculate_synthetic_prices(data):
synthetic_prices = {}
# Remove duplicate indices from all DataFrames in the data dictionary
for key in data:
data[key] = remove_duplicate_indices(data[key])
# Calculate synthetic prices for all pairs using multiple methods
pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]
method_count = 1
for pair1, pair2 in pairs:
print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['ask']
method_count += 1
print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['bid']
method_count += 1
return pd.DataFrame(synthetic_prices)
Analisi delle opportunità di arbitraggio: funzione analyze_arbitrage
Per prima cosa, creiamo un dizionario vuoto synthetic_prices. Poi, lo riempiremo di dati. Quindi esamineremo tutti i dati e rimuoveremo gli indici duplicati per evitare errori in futuro.
Il passo successivo è la lista "pairs". Queste sono le nostre coppie di valute che utilizzeremo per la sintesi. Poi inizia un altro processo. Eseguiamo un ciclo attraverso tutte le coppie. Per ogni coppia, calcoliamo il prezzo sintetico in due modi:
- Dividere il bid della prima coppia per l’ask della seconda.
- Dividere il bid della prima coppia per il bid della seconda.
Ogni volta, aumentiamo il nostro method_count. Di conseguenza, otteniamo 2000 coppie sintetiche!
Ecco come funziona la funzione calculate_synthetic_prices. Non si limita a calcolare i prezzi, ma crea nuove opportunità. Questa caratteristica offre grandi risultati sotto forma di opportunità di arbitraggio!
Visualizzazione dei risultati: Salvataggio dei dati in CSV
Vediamo la funzione analyze_arbitrage. Non si limita ad analizzare i dati, ma cerca ciò che gli serve in un flusso di numeri. Diamo un'occhiata:
def analyze_arbitrage(data, synthetic_prices, method_count): # Calculate spreads for each pair spreads = {} for pair in data.keys(): for i in range(1, method_count + 1): synthetic_pair = f'{pair}_{i}' if synthetic_pair in synthetic_prices.columns: print(f"Analyzing arbitrage opportunity for {synthetic_pair}") spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair] # Identify arbitrage opportunities arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008 print("Arbitrage opportunities:") print(arbitrage_opportunities) # Save the full table of arbitrage opportunities to a CSV file arbitrage_opportunities.to_csv('arbitrage_opportunities.csv') return arbitrage_opportunities
Per prima cosa, la nostra funzione crea un dizionario 'spreads' vuoto. Poi, lo riempiremo di dati.
Passiamo alla fase successiva. La funzione analizza tutte le coppie di valute e i loro analoghi sintetici. Per ogni coppia, calcola lo spread - la differenza tra il prezzo bid reale e il prezzo sintetico.
spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]
Questa stringa svolge un ruolo piuttosto importante. Trova la differenza tra il prezzo reale e quello sintetico. Se questa differenza è positiva, abbiamo un'opportunità di arbitraggio.
Per ottenere risultati più seri, utilizziamo il numero 0,00008:
arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008 Questa stringa elimina tutte le possibilità inferiori a 8 punti. In questo modo otterremo opportunità con una maggiore probabilità di profitto.
Ecco il passo successivo:
arbitrage_opportunities.to_csv('arbitrage_opportunities.csv') Ora tutti i dati sono salvati in un file CSV. Ora possiamo studiarli, analizzarli, tracciare grafici e in generale, svolgere un lavoro produttivo. Tutto questo è reso possibile grazie alla seguente funzione - analyze_arbitrage. Non si limita ad analizzare, ma cerca, trova e salva opportunità di arbitraggio.
Apertura di ordini di prova: funzione open_test_limit_order
Consideriamo poi la funzione open_test_limit_order. Aprirà i nostri ordini per noi.
Diamo un'occhiata:
def open_test_limit_order(symbol, order_type, price, volume, take_profit, stop_loss, terminal_path): if not mt5.initialize(path=terminal_path): print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}") return None symbol_info = mt5.symbol_info(symbol) positions_total = mt5.positions_total() if symbol_info is None: print(f"Instrument not found: {symbol}") return None if positions_total >= MAX_OPEN_TRADES: print("MAX POSITIONS TOTAL!") return None # Check if symbol_info is None before accessing its attributes if symbol_info is not None: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "deviation": 30, "magic": 123456, "comment": "Stochastic Stupi Sustem", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, "tp": price + take_profit * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price - take_profit * symbol_info.point, "sl": price - stop_loss * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price + stop_loss * symbol_info.point, } result = mt5.order_send(request) if result is not None and result.retcode == mt5.TRADE_RETCODE_DONE: print(f"Test limit order placed for {symbol}") return result.order else: print(f"Error: Test limit order not placed for {symbol}, retcode={result.retcode if result is not None else 'None'}") return None else: print(f"Error: Symbol info not found for {symbol}") return None
La prima cosa che la nostra funzione fa è cercare di connettersi al terminale MetaTrader 5. Poi controlla se lo strumento che vogliamo negoziare esiste.
Il seguente codice:
if positions_total >= MAX_OPEN_TRADES: print("MAX POSITIONS TOTAL!") return None
Questo controllo assicura che non vengano aperte troppe posizioni.
Ora il passo successivo consiste nel generare una richiesta di apertura di un ordine. Ci sono molti parametri qui. Tipo di ordine, volume, prezzo, deviazione, numero magico, commento... Se tutto va bene, la funzione ce lo comunica. In caso contrario, viene visualizzato il messaggio.
Ecco come funziona la funzione open_test_limit_order. Questo è il nostro collegamento con il mercato. In un certo senso, svolge le funzioni di un broker.
Restrizioni temporanee al trading: lavorare in determinati orari
Parliamo ora degli orari di negoziazione.
if current_time >= datetime.strptime("23:30", "%H:%M").time() or current_time <= datetime.strptime("05:00", "%H:%M").time(): print("Current time is between 23:30 and 05:00. Skipping execution.") time.sleep(300) # Wait for 5 minutes before checking again continue
Cosa sta succedendo qui? Il nostro sistema controlla l'ora. Se l'orologio indica un orario compreso tra le 23:30 e le 5:00, vede che non si tratta di un orario di trading e passa in modalità standby per 5 minuti. Poi si attiva, controlla di nuovo l'ora e, se è ancora presto, passa di nuovo in modalità standby.
Perché ne abbiamo bisogno? I motivi sono molteplici. In primo luogo, la liquidità. Di notte di solito ce n'è meno. In secondo luogo, gli spread. Di notte si espandono. Terzo, le notizie. Le più importanti di solito escono durante l'orario di lavoro.
Ciclo di esecuzione e gestione degli errori
Diamo un'occhiata alla funzione "main". È come il capitano di una nave, ma al posto del timone c'è una tastiera. Che cosa fa? Molto semplice:
- Raccolta dei dati
- Calcolo dei prezzi sintetici
- Ricerca opportunità di arbitraggio
- Apertura ordini
C'è anche una piccola gestione degli errori.
def main():
data = get_currency_data()
synthetic_prices = calculate_synthetic_prices(data)
method_count = 2000 # Define the method_count variable here
arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices, method_count)
# Trade based on arbitrage opportunities
for symbol in arbitrage_opportunities.columns:
if arbitrage_opportunities[symbol].any():
direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
symbol = symbol.split('_')[0] # Remove the index from the symbol
symbol_info = mt5.symbol_info_tick(symbol)
if symbol_info is not None:
price = symbol_info.bid if direction == "BUY" else symbol_info.ask
take_profit = 450
stop_loss = 200
order = open_test_limit_order(symbol, mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL, price, 0.50, take_profit, stop_loss, terminal_path)
else:
print(f"Error: Symbol info tick not found for {symbol}")
Scalabilità del sistema: Aggiunta di nuove coppie di valute e metodi
Volete aggiungere una nuova coppia di valute? È sufficiente inserirla in questa lista:
symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"]
Il sistema ora conosce la nuova coppia. . E i nuovi metodi di calcolo?
def calculate_synthetic_prices(data):
# ... existing code ...
# Add a new method
synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['ask'] / data[pair2]['bid']
method_count += 1
Test e backtesting del sistema di arbitraggio
Parliamo di backtesting. Questo è un punto molto importante per qualsiasi sistema di trading. Il nostro sistema di arbitraggio non fa eccezione.
Cosa abbiamo fatto? Abbiamo analizzato la nostra strategia attraverso i dati storici. Perché? Per capire quanto sia efficiente. Il nostro codice inizia con get_historical_data. Questa funzione recupera i dati storici da MetaTrader 5. Senza questi dati, non saremo in grado di lavorare in modo produttivo.
Poi viene calculate_synthetic_prices. Qui calcoliamo i tassi di cambio sintetici. Si tratta di una parte fondamentale della nostra strategia di arbitraggio. Analyze_arbitrage è il nostro rilevatore di opportunità. Confronta i prezzi reali con quelli sintetici e trova la differenza, in modo da ottenere un potenziale profitto. simulate_trade è quasi un processo di trading. Tuttavia, si verifica in modalità test. Si tratta di un processo molto importante: è meglio sbagliare nella simulazione che perdere denaro reale.
Infine, backtest_arbitrage_system mette tutto insieme ed esegue la nostra strategia attraverso i dati storici. Giorno dopo giorno, operazione dopo operazione.
import MetaTrader5 as mt5 import pandas as pd import numpy as np import matplotlib.pyplot as plt from datetime import datetime, timedelta import pytz # Path to MetaTrader 5 terminal terminal_path = "C:/Program Files/ForexBroker - MetaTrader 5/Arima/terminal64.exe" def remove_duplicate_indices(df): """Removes duplicate indices, keeping only the first row with a unique index.""" return df[~df.index.duplicated(keep='first')] def get_historical_data(start_date, end_date, terminal_path): if not mt5.initialize(path=terminal_path): print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}") return None symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"] historical_data = {} for symbol in symbols: timeframe = mt5.TIMEFRAME_M1 rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) if rates is not None and len(rates) > 0: df = pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') df.set_index('time', inplace=True) df = df[['open', 'high', 'low', 'close']] df['bid'] = df['close'] # Simplification: use 'close' as 'bid' df['ask'] = df['close'] + 0.000001 # Simplification: add spread historical_data[symbol] = df mt5.shutdown() return historical_data def calculate_synthetic_prices(data): synthetic_prices = {} pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'), ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'), ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'), ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'), ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'), ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'), ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'), ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'), ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'), ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'), ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')] for pair1, pair2 in pairs: if pair1 in data and pair2 in data: synthetic_prices[f'{pair1}_{pair2}_1'] = data[pair1]['bid'] / data[pair2]['ask'] synthetic_prices[f'{pair1}_{pair2}_2'] = data[pair1]['bid'] / data[pair2]['bid'] return pd.DataFrame(synthetic_prices) def analyze_arbitrage(data, synthetic_prices): spreads = {} for pair in data.keys(): for synth_pair in synthetic_prices.columns: if pair in synth_pair: spreads[synth_pair] = data[pair]['bid'] - synthetic_prices[synth_pair] arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008 return arbitrage_opportunities def simulate_trade(data, direction, entry_price, take_profit, stop_loss): for i, row in data.iterrows(): current_price = row['bid'] if direction == "BUY" else row['ask'] if direction == "BUY": if current_price >= entry_price + take_profit: return {'profit': take_profit * 800, 'duration': i} elif current_price <= entry_price - stop_loss: return {'profit': -stop_loss * 400, 'duration': i} else: # SELL if current_price <= entry_price - take_profit: return {'profit': take_profit * 800, 'duration': i} elif current_price >= entry_price + stop_loss: return {'profit': -stop_loss * 400, 'duration': i} # If the loop completes without hitting TP or SL, close at the last price last_price = data['bid'].iloc[-1] if direction == "BUY" else data['ask'].iloc[-1] profit = (last_price - entry_price) * 100000 if direction == "BUY" else (entry_price - last_price) * 100000 return {'profit': profit, 'duration': len(data)} def backtest_arbitrage_system(historical_data, start_date, end_date): equity_curve = [10000] # Starting with $10,000 trades = [] dates = pd.date_range(start=start_date, end=end_date, freq='D') for current_date in dates: print(f"Backtesting for date: {current_date.date()}") # Get data for the current day data = {symbol: df[df.index.date == current_date.date()] for symbol, df in historical_data.items()} # Skip if no data for the current day if all(df.empty for df in data.values()): continue synthetic_prices = calculate_synthetic_prices(data) arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices) # Simulate trades based on arbitrage opportunities for symbol in arbitrage_opportunities.columns: if arbitrage_opportunities[symbol].any(): direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL" base_symbol = symbol.split('_')[0] if base_symbol in data and not data[base_symbol].empty: price = data[base_symbol]['bid'].iloc[-1] if direction == "BUY" else data[base_symbol]['ask'].iloc[-1] take_profit = 800 * 0.00001 # Convert to price stop_loss = 400 * 0.00001 # Convert to price # Simulate trade trade_result = simulate_trade(data[base_symbol], direction, price, take_profit, stop_loss) trades.append(trade_result) # Update equity curve equity_curve.append(equity_curve[-1] + trade_result['profit']) return equity_curve, trades def main(): start_date = datetime(2024, 1, 1, tzinfo=pytz.UTC) end_date = datetime(2024, 8, 31, tzinfo=pytz.UTC) # Backtest for January-August 2024 print("Fetching historical data...") historical_data = get_historical_data(start_date, end_date, terminal_path) if historical_data is None: print("Failed to fetch historical data. Exiting.") return print("Starting backtest...") equity_curve, trades = backtest_arbitrage_system(historical_data, start_date, end_date) total_profit = sum(trade['profit'] for trade in trades) win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0 print(f"Backtest completed. Results:") print(f"Total Profit: ${total_profit:.2f}") print(f"Win Rate: {win_rate:.2%}") print(f"Final Equity: ${equity_curve[-1]:.2f}") # Plot equity curve plt.figure(figsize=(15, 10)) plt.plot(equity_curve) plt.title('Equity Curve: Backtest Results') plt.xlabel('Trade Number') plt.ylabel('Account Balance ($)') plt.savefig('equity_curve.png') plt.close() print("Equity curve saved as 'equity_curve.png'.") if __name__ == "__main__": main()
Perché è importante? Perché i backtest dimostrano l'efficienza del nostro sistema. È profittevole o prosciuga il vostro deposito? Qual'è il drawdown? Qual è la percentuale di operazioni vincenti? Tutto questo lo apprendiamo dal backtest.
Naturalmente, i risultati passati non garantiscono quelli futuri. Il mercato cambia. Ma senza un backtest non otterremo alcun risultato. Conoscendo il risultato, sappiamo più o meno cosa aspettarci. Un altro punto importante - il backtest aiuta a ottimizzare il sistema. Cambiamo i parametri e guardiamo il risultato ancora e ancora. Quindi, passo dopo passo, rendiamo il nostro sistema migliore.
Ecco il risultato del backtest del nostro sistema:

Ecco un test del sistema in MetaTrader 5:

Ed ecco il codice dell'EA MQL5 per il sistema:
//+------------------------------------------------------------------+ //| TrissBotDemo.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" // Input parameters input int MAX_OPEN_TRADES = 10; input double VOLUME = 0.50; input int TAKE_PROFIT = 450; input int STOP_LOSS = 200; input double MIN_SPREAD = 0.00008; // Global variables string symbols[] = {"AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"}; int symbolsTotal; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { symbolsTotal = ArraySize(symbols); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Cleanup code here } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(!IsTradeAllowed()) return; datetime currentTime = TimeGMT(); if(currentTime >= StringToTime("23:30:00") || currentTime <= StringToTime("05:00:00")) { Print("Current time is between 23:30 and 05:00. Skipping execution."); return; } AnalyzeAndTrade(); } //+------------------------------------------------------------------+ //| Analyze arbitrage opportunities and trade | //+------------------------------------------------------------------+ void AnalyzeAndTrade() { double synthetic_prices[]; ArrayResize(synthetic_prices, symbolsTotal); for(int i = 0; i < symbolsTotal; i++) { synthetic_prices[i] = CalculateSyntheticPrice(symbols[i]); double currentPrice = SymbolInfoDouble(symbols[i], SYMBOL_BID); if(MathAbs(currentPrice - synthetic_prices[i]) > MIN_SPREAD) { if(currentPrice > synthetic_prices[i]) { OpenOrder(symbols[i], ORDER_TYPE_SELL); } else { OpenOrder(symbols[i], ORDER_TYPE_BUY); } } } } //+------------------------------------------------------------------+ //| Calculate synthetic price for a symbol | //+------------------------------------------------------------------+ double CalculateSyntheticPrice(string symbol) { // This is a simplified version. You need to implement the logic // to calculate synthetic prices based on your specific method return SymbolInfoDouble(symbol, SYMBOL_ASK); } //+------------------------------------------------------------------+ //| Open a new order | //+------------------------------------------------------------------+ void OpenOrder(string symbol, ENUM_ORDER_TYPE orderType) { if(PositionsTotal() >= MAX_OPEN_TRADES) { Print("MAX POSITIONS TOTAL!"); return; } double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tp = (orderType == ORDER_TYPE_BUY) ? price + TAKE_PROFIT * point : price - TAKE_PROFIT * point; double sl = (orderType == ORDER_TYPE_BUY) ? price - STOP_LOSS * point : price + STOP_LOSS * point; MqlTradeRequest request = {}; MqlTradeResult result = {}; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = VOLUME; request.type = orderType; request.price = price; request.deviation = 30; request.magic = 123456; request.comment = "ArbitrageAdvisor"; request.type_time = ORDER_TIME_GTC; request.type_filling = ORDER_FILLING_IOC; request.tp = tp; request.sl = sl; if(!OrderSend(request, result)) { Print("OrderSend error ", GetLastError()); return; } if(result.retcode == TRADE_RETCODE_DONE) { Print("Order placed successfully"); } else { Print("Order failed with retcode ", result.retcode); } } //+------------------------------------------------------------------+ //| Check if trading is allowed | //+------------------------------------------------------------------+ bool IsTradeAllowed() { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Print("Trade is not allowed in the terminal"); return false; } if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Print("Trade is not allowed in the Expert Advisor"); return false; } return true; }
Possibili miglioramenti e legittimità del sistema per i broker, o come non colpire un fornitore di liquidità con ordini limite
Il nostro sistema presenta altre potenziali difficoltà. I broker e i fornitori di liquidità spesso non vedono di buon occhio tali sistemi. Perché? Perché stiamo essenzialmente sottraendo al mercato la liquidità necessaria. Hanno persino inventato un termine speciale per questo fenomeno - Toxic Order Flow (Flusso di Ordini Tossici).
Questo è un problema reale. Con i nostri ordini di mercato risucchiamo letteralmente la liquidità dal sistema. Tutti ne hanno bisogno: sia i grandi operatori che i piccoli trader. Naturalmente, ciò comporta delle conseguenze.
Cosa fare in questa situazione? Esiste un compromesso - gli ordini limite.
Ma questo non risolve tutti i problemi: l'etichetta Toxic Order Flow viene apposta non tanto per l'assorbimento dell'attuale liquidità dal mercato, quanto per gli elevati carichi di servizio di un tale flusso di ordini. Non ho ancora risolto questo problema. Ad esempio, spendere 100 dollari per servire un enorme flusso di transazioni di arbitraggio, ricevendo una commissione di 50 dollari, non è redditizio. Quindi, forse, la chiave è un elevato turnover e un'elevata dimensione dei lotti, oltre a un'elevata velocità di turnover. In questo caso i broker potrebbero anche essere disposti a pagare degli abbuoni.
Ora ci concentriamo sul codice. Come possiamo migliorarlo? In primo luogo, possiamo aggiungere una funzione per la gestione degli ordini limite. Anche qui c'è molto lavoro da fare - dobbiamo pensare alla logica dell'attesa e dell'annullamento degli ordini non eseguiti.
L'apprendimento automatico potrebbe essere un'idea interessante per migliorare il sistema. Suggerisco che potrebbe essere possibile addestrare il nostro sistema a prevedere quali opportunità di arbitraggio hanno maggiori probabilità di funzionare.
Conclusioni
Riassumiamo. Abbiamo creato un sistema che cerca opportunità di arbitraggio. Ricordate che il sistema non risolve tutti i vostri problemi finanziari.
Abbiamo risolto il problema del backtest. Funziona con dati basati sul tempo e, ancora meglio, ci permette di vedere come il nostro sistema avrebbe funzionato in passato. Ma ricordate - i risultati passati non garantiscono quelli futuri. Il mercato è un meccanismo complesso che cambia continuamente.
Ma sapete qual è la cosa più importante? Non è un codice. Non sono gli algoritmi. Ma voi. Il vostro desiderio di imparare, sperimentare, sbagliare e riprovare. È davvero impagabile.
Quindi non fermatevi qui. Questo sistema è solo l'inizio del vostro viaggio nel mondo del trading algoritmico. Utilizzatela come punto di partenza per nuove idee e nuove strategie. Proprio come nella vita, la cosa principale nel trading è l'equilibrio. L'equilibrio tra rischio e cautela, avidità e razionalità, complessità e semplicità.
In bocca al lupo per questo entusiasmante viaggio e che i vostri algoritmi siano sempre un passo avanti al mercato!
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/15964
Avvertimento: Tutti i diritti su questi materiali sono riservati a MetaQuotes Ltd. La copia o la ristampa di questi materiali in tutto o in parte sono proibite.
Questo articolo è stato scritto da un utente del sito e riflette le sue opinioni personali. MetaQuotes Ltd non è responsabile dell'accuratezza delle informazioni presentate, né di eventuali conseguenze derivanti dall'utilizzo delle soluzioni, strategie o raccomandazioni descritte.
Arriva il Nuovo MetaTrader 5 e MQL5
I metodi di William Gann (Parte III): L'astrologia funziona?
Utilizza i canali MQL5.community e le chat di gruppo
I metodi di William Gann (parte II): Creazione dell'indicatore Quadrato di Gann
- 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
Si prega di spiegare di cosa si tratta:
Ecco le coppie:
pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'), ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'), ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'), ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'), ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'), ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'), ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'), ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'), ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'), ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'), ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]Qual è il Bid della prima coppia? La prima coppia è:
Qual è il Bid della prima coppia? La prima coppia è:
AUDUSD è anche una coppia. AUD a USD.
Per favore, spiegate di cosa si tratta:
Ecco le coppie:
Qual è il Bid della prima coppia? La prima coppia è:
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)Tutti installati. Questo è ciò che viene visualizzato in ticks:
array([b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'',
...
b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'',
b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'',
b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b''],
dtype='|V0')
E qui otteniamo già un'uscita in tempo:
Anche il codice dell'esempio https://www.mql5.com/it/docs/python_metatrader5/mt5copyticksfrom_py non funziona .
Comunque, com'è python? Come prepararlo? Non è chiaro...