English Русский 中文 Español Deutsch 日本語 Português
preview
Sistema di trading di arbitraggio ad alta frequenza in Python utilizzando MetaTrader 5

Sistema di trading di arbitraggio ad alta frequenza in Python utilizzando MetaTrader 5

MetaTrader 5Trading |
33 7
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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:

  1. Dividere il bid della prima coppia per l’ask della seconda.
  2. 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:

  1. Raccolta dei dati
  2. Calcolo dei prezzi sintetici 
  3. Ricerca opportunità di arbitraggio 
  4. 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

Ultimi commenti | Vai alla discussione (7)
pivomoe
pivomoe | 24 ott 2024 a 00:31

Si prega di spiegare di cosa si tratta:

А теперь следующий шаг — список pairs. Это наши валютные пары, которые мы будем использовать для синтеза. Дальше начинается еще один процесс. Мы запускаем цикл по всем парам. Для каждой пары мы рассчитываем синтетическую цену двумя способами:

Делим bid первой пары на ask второй.
Делим bid первой пары на bid второй.
И каждый раз мы увеличиваем наш method_count. В итоге у нас получается не 1000, не 1500, а целых 2000 синтетических цен!

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 è:

('AUDUSD', 'USDCHF')
Andrey Khatimlianskii
Andrey Khatimlianskii | 28 ott 2024 a 16:09
pivomoe #:

Qual è il Bid della prima coppia? La prima coppia è:

AUDUSD è anche una coppia. AUD a USD.

Roman Shiredchenko
Roman Shiredchenko | 28 ott 2024 a 20:12
pivomoe #:

Per favore, spiegate di cosa si tratta:

Ecco le coppie:

Qual è il Bid della prima coppia? La prima coppia è:

È così che si costruisce il sintetico. Non attraverso la differenza, ma la divisione. E non semplice, ma... leggere.....
leonerd
leonerd | 21 nov 2024 a 10:59
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:

ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')
leonerd
leonerd | 22 nov 2024 a 09:11

Anche il codice dell'esempio https://www.mql5.com/it/docs/python_metatrader5/mt5copyticksfrom_py non funziona .

>>>  timezone = pytz.timezone("Etc/UTC")
>>>  utc_from = datetime(2020, 1, 10, tzinfo=timezone)
>>>  ticks = mt5.copy_ticks_from("EURUSD", utc_from, 100000, mt5.COPY_TICKS_ALL)
>>>
>>> print("Zecche ricevute:",len(ticks))
Получено тиков: 100000
>>> print("Prendiamo le zecche risultanti così come sono".)
Выведем полученные тики как есть
>>>  count = 0
>>> for tick in ticks:
...     count+=1
...     print(tick)
...     if count >= 100:
...         break
...
b''
b''
b''
b''

Comunque, com'è python? Come prepararlo? Non è chiaro...

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.
I metodi di William Gann (Parte III): L'astrologia funziona? I metodi di William Gann (Parte III): L'astrologia funziona?
Le posizioni di pianeti e stelle influenzano i mercati finanziari? Armiamoci di statistiche e big data e intraprendiamo un viaggio emozionante nel mondo in cui stelle e grafici azionari si intersecano.
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.
I metodi di William Gann (parte II): Creazione dell'indicatore Quadrato di Gann I metodi di William Gann (parte II): Creazione dell'indicatore Quadrato di Gann
Creeremo un indicatore basato sul Quadrato del 9 di Gann, costruito squadrando tempo e prezzo. Prepareremo il codice e testeremo l'indicatore nella piattaforma su differenti intervalli di tempo.