English Русский 中文 Español Deutsch 日本語 Português Türkçe
preview
L’utilizzo delle regole di associazione nell'analisi dei dati Forex

L’utilizzo delle regole di associazione nell'analisi dei dati Forex

MetaTrader 5Integrazione |
28 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introduzione al concetto di regole di associazione

Il trading algoritmico moderno richiede nuovi approcci analitici. Il mercato è in continua evoluzione e i metodi classici di analisi tecnica non sono più in grado di individuare le complesse relazioni di mercato.

Lavoro con i dati da molto tempo e ho notato che molte idee di successo provengono da settori affini. Oggi vorrei condividere la mia esperienza nell'utilizzo delle regole di associazione nel trading. Questo metodo ha dato prova della propria efficacia nell'analisi dei dati nel settore della vendita al dettaglio, consentendoci di individuare correlazioni tra acquisti, transazioni, variazioni dei prezzi e l'andamento futuro della domanda e dell'offerta. E se lo applicassimo al mercato dei cambi?

L'idea di base è semplice: cerchiamo pattern stabili di comportamento dei prezzi, indicatori e loro combinazioni. Ad esempio, con quale frequenza un rialzo dell'EUR/USD segue un calo dell'USD/JPY? O quali sono le condizioni che più spesso precedono movimenti di prezzo significativi?

In questo articolo, illustrerò l'intero processo di creazione di un sistema di trading basato su questa idea. Faremo quanto segue:

  1. Raccoglieremo i dati storici in MQL5
  2. Li analizzeremo in Python
  3. Individueremo pattern significativi
  4. Li trasformeremo in segnali di trading

Perché proprio questa combinazione di tecnologie? MQL5 è ottimo per lavorare con i dati di borsa e per l'automazione del trading. A sua volta, Python fornisce potenti strumenti di analisi. In base alla mia esperienza, posso affermare che tale combinazione è molto efficace per lo sviluppo di sistemi di trading.

Ci saranno molte cose interessanti nel codice, in particolare nell'ambito dell'applicazione delle regole di associazione al Forex.


Raccolta e preparazione dei dati storici del Forex

Per noi è estremamente importante raccogliere e preparare tutti i dati necessari. Prendiamo come riferimento i dati H1 delle principali coppie di valute degli ultimi due anni (a partire dal 2022).

Creeremo uno script MQL5 che raccoglierà ed esporterà i dati necessari in formato CSV:

//+------------------------------------------------------------------+
//|                                                      Dataset.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"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   string pairs[] = {"EURUSD", "GBPUSD", "USDJPY", "USDCHF"};
   datetime startTime = D'2022.01.01 00:00';
   datetime endTime = D'2024.01.01 00:00';
   
   for(int i=0; i<ArraySize(pairs); i++)
   {
      string filename = pairs[i] + "_H1.csv";
      int fileHandle = FileOpen(filename, FILE_WRITE|FILE_CSV);
      
      if(fileHandle != INVALID_HANDLE)
      {
         // Set headers
         FileWrite(fileHandle, "DateTime", "Open", "High", "Low", "Close", "Volume");
         
         MqlRates rates[];
         ArraySetAsSeries(rates, true);
         
         int copied = CopyRates(pairs[i], PERIOD_H1, startTime, endTime, rates);
         
         for(int j=copied-1; j>=0; j--)
         {
            FileWrite(fileHandle,
                     TimeToString(rates[j].time),
                     DoubleToString(rates[j].open, 5),
                     DoubleToString(rates[j].high, 5),
                     DoubleToString(rates[j].low, 5),
                     DoubleToString(rates[j].close, 5),
                     IntegerToString(rates[j].tick_volume)
                    );
         }
         FileClose(fileHandle);
      }
   }
}
//+------------------------------------------------------------------+


Elaborazione dei dati in Python

Dopo aver creato un dataset, è importante gestire i dati correttamente. 

A questo scopo, ho creato la classe speciale ForexDataProcessor, che si occupa di tutto il lavoro sporco. Analizziamo i suoi componenti principali.

Inizieremo caricando i dati. La nostra funzione utilizza dati orari per le principali coppie di valute: EURUSD, GBPUSD, USDJPY e USDCHF. I dati devono essere in formato CSV e includere le principali caratteristiche di prezzo.

import pandas as pd
import numpy as np
from datetime import datetime
import os
import warnings
warnings.filterwarnings('ignore')

class ForexDataProcessor:
    def __init__(self):
        self.pairs = ["EURUSD", "GBPUSD", "USDJPY", "USDCHF"]
        self.data = {}
        self.processed_data = {}
    
    def load_data(self):
        """Load data for all currency pairs"""
        success = True
        for pair in self.pairs:
            filename = f"{pair}_H1.csv"
            try:
                df = pd.read_csv(filename, 
                               encoding='utf-16',
                               sep='\t',
                               names=['DateTime', 'Open', 'High', 'Low', 'Close', 'Volume'])
                
                # Remove lines with duplicate headers
                df = df[df['DateTime'] != 'DateTime']
                
                # Convert data types
                df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y.%m.%d %H:%M')
                for col in ['Open', 'High', 'Low', 'Close']:
                    df[col] = pd.to_numeric(df[col], errors='coerce')
                df['Volume'] = pd.to_numeric(df['Volume'], errors='coerce')
                
                # Remove NaN strings
                df = df.dropna()
                
                df.set_index('DateTime', inplace=True)
                self.data[pair] = df
                print(f"Loaded {pair} data successfully. Shape: {df.shape}")
            except Exception as e:
                print(f"Error loading {pair} data: {str(e)}")
                success = False
        return success

    def safe_qcut(self, series, q, labels):
        """Safe quantization with error handling"""
        try:
            if series.nunique() <= q:
                # If there are fewer unique values than quantiles, use regular categorization
                return pd.qcut(series, q=q, labels=labels, duplicates='drop')
            return pd.qcut(series, q=q, labels=labels)
        except Exception as e:
            print(f"Warning: Error in qcut - {str(e)}. Using manual categorization.")
            # Manual categorization as a backup option
            percentiles = np.percentile(series, [20, 40, 60, 80])
            return pd.cut(series, 
                         bins=[-np.inf] + list(percentiles) + [np.inf], 
                         labels=labels)

    def calculate_indicators(self, df):
        """Calculate technical indicators for a single dataframe"""
        result = df.copy()
        
        # Basic calculations
        result['Returns'] = result['Close'].pct_change()
        result['Log_Returns'] = np.log(result['Close']/result['Close'].shift(1))
        result['Range'] = result['High'] - result['Low']
        result['Range_Pct'] = result['Range'] / result['Open'] * 100
        
        # SMA calculations
        for period in [5, 10, 20, 50, 200]:
            result[f'SMA_{period}'] = result['Close'].rolling(window=period).mean()
        
        # EMA calculations
        for period in [5, 10, 20, 50]:
            result[f'EMA_{period}'] = result['Close'].ewm(span=period, adjust=False).mean()
        
        # Volatility
        result['Volatility'] = result['Returns'].rolling(window=20).std() * np.sqrt(20)
        
        # RSI
        delta = result['Close'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
        rs = gain / loss
        result['RSI'] = 100 - (100 / (1 + rs))
        
        # MACD
        exp1 = result['Close'].ewm(span=12, adjust=False).mean()
        exp2 = result['Close'].ewm(span=26, adjust=False).mean()
        result['MACD'] = exp1 - exp2
        result['MACD_Signal'] = result['MACD'].ewm(span=9, adjust=False).mean()
        result['MACD_Hist'] = result['MACD'] - result['MACD_Signal']
        
        # Bollinger Bands
        result['BB_Middle'] = result['Close'].rolling(window=20).mean()
        result['BB_Upper'] = result['BB_Middle'] + (result['Close'].rolling(window=20).std() * 2)
        result['BB_Lower'] = result['BB_Middle'] - (result['Close'].rolling(window=20).std() * 2)
        result['BB_Width'] = (result['BB_Upper'] - result['BB_Lower']) / result['BB_Middle']
        
        # Discretization for association rules
        # SMA-based trend
        result['Trend'] = 'Sideways'
        result.loc[result['Close'] > result['SMA_50'], 'Trend'] = 'Uptrend'
        result.loc[result['Close'] < result['SMA_50'], 'Trend'] = 'Downtrend'
        
        # RSI zones
        result['RSI_Zone'] = pd.cut(result['RSI'].fillna(50), 
                                   bins=[-np.inf, 30, 45, 55, 70, np.inf],
                                   labels=['Oversold', 'Weak', 'Neutral', 'Strong', 'Overbought'])
        
        # Secure quantization for other parameters
        labels = ['Very_Low', 'Low', 'Medium', 'High', 'Very_High']
        
        result['Volatility_Zone'] = self.safe_qcut(
            result['Volatility'].fillna(result['Volatility'].mean()), 
            5, labels)
        
        result['Price_Zone'] = self.safe_qcut(
            result['Close'], 
            5, labels)
        
        result['Volume_Zone'] = self.safe_qcut(
            result['Volume'], 
            5, labels)
        
        # Candle patterns
        result['Body'] = result['Close'] - result['Open']
        result['Upper_Shadow'] = result['High'] - result[['Open', 'Close']].max(axis=1)
        result['Lower_Shadow'] = result[['Open', 'Close']].min(axis=1) - result['Low']
        result['Body_Pct'] = result['Body'] / result['Open'] * 100
        
        body_mean = abs(result['Body_Pct']).mean()
        result['Candle_Pattern'] = 'Normal'
        result.loc[abs(result['Body_Pct']) < body_mean * 0.1, 'Candle_Pattern'] = 'Doji'
        result.loc[result['Body_Pct'] > body_mean * 2, 'Candle_Pattern'] = 'Long_Bullish'
        result.loc[result['Body_Pct'] < -body_mean * 2, 'Candle_Pattern'] = 'Long_Bearish'
        
        return result

    def process_all_pairs(self):
        """Process all currency pairs and create combined dataset"""
        if not self.load_data():
            return None

        # Handling each pair
        for pair in self.pairs:
            if not self.data[pair].empty:
                print(f"Processing {pair}...")
                self.processed_data[pair] = self.calculate_indicators(self.data[pair])
                # Add a pair prefix to the column names
                self.processed_data[pair].columns = [f"{pair}_{col}" for col in self.processed_data[pair].columns]
            else:
                print(f"Skipping {pair} - no data")

        # Find the common time range for non-empty data
        common_dates = None
        for pair in self.pairs:
            if pair in self.processed_data and not self.processed_data[pair].empty:
                if common_dates is None:
                    common_dates = set(self.processed_data[pair].index)
                else:
                    common_dates &= set(self.processed_data[pair].index)

        if not common_dates:
            print("No common dates found")
            return None

        # Align all pairs by common dates
        aligned_data = {}
        for pair in self.pairs:
            if pair in self.processed_data and not self.processed_data[pair].empty:
                aligned_data[pair] = self.processed_data[pair].loc[sorted(common_dates)]

        # Combine all pairs
        combined_df = pd.concat([aligned_data[pair] for pair in aligned_data], axis=1)
        
        return combined_df

    def save_data(self, data, suffix='combined'):
        """Save processed data to CSV"""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"forex_data_{suffix}_{timestamp}.csv"
        
        try:
            data.to_csv(filename, sep='\t', encoding='utf-16')
            print(f"Saved processed data to: {filename}")
            return True
        except Exception as e:
            print(f"Error saving data: {str(e)}")
            return False

if __name__ == "__main__":
    processor = ForexDataProcessor()
    
    # Handling all pairs
    combined_data = processor.process_all_pairs()
    
    if combined_data is not None:
        # Save the combined dataset
        processor.save_data(combined_data)
        
        # Display dataset info
        print("\nCombined dataset shape:", combined_data.shape)
        print("\nFeatures for association rules analysis:")
        for col in combined_data.columns:
            if any(x in col for x in ['_Zone', '_Pattern', 'Trend']):
                print(f"- {col}")
        
        # Save individual pairs
        for pair in processor.pairs:
            if pair in processor.processed_data and not processor.processed_data[pair].empty:
                processor.save_data(processor.processed_data[pair], pair)


Una volta completato il caricamento, inizia la parte più interessante: il calcolo degli indicatori tecnici. Qui mi affido ad un intero arsenale di strumenti collaudati nel tempo. Le medie mobili aiutano a identificare tendenze di durata variabile. SMA(50) spesso agisce come supporto o resistenza dinamica. L'oscillatore RSI con un periodo classico di 14 è utile per individuare le zone di ipercomprato e ipervenduto del mercato. Il MACD è indispensabile per identificare i punti di inversione e di momentum. Le Bande di Bollinger offrono un quadro chiaro dell'attuale volatilità del mercato.

# Volatility and RSI calculation example
result['Volatility'] = result['Returns'].rolling(window=20).std() * np.sqrt(20)

delta = result['Close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
result['RSI'] = 100 - (100 / (1 + rs))

La discretizzazione dei dati merita un'attenzione particolare. Tutti i valori continui devono essere suddivisi in categorie chiare. In questo ambito, è fondamentale trovare un giusto equilibrio - una suddivisione troppo netta complicherà la ricerca di pattern, mentre una suddivisione troppo approssimativa porterà alla perdita di importanti sfumature di mercato. Ad esempio, per determinare il trend, una suddivisione più semplice funziona meglio - in base alla posizione del prezzo rispetto alla media.

# Defining a trend
result['Trend'] = 'Sideways'
result.loc[result['Close'] > result['SMA_50'], 'Trend'] = 'Uptrend'
result.loc[result['Close'] < result['SMA_50'], 'Trend'] = 'Downtrend'

Anche i pattern a candela richiedono un approccio particolare. Sulla base di un'analisi statistica, distinguo le Doji in base alla dimensione minima del corpo della candela, mentre i Long_Bullish e i Long_Bearish in base a movimenti di prezzo estremi. Questa classificazione ci permette di identificare chiaramente i momenti di indecisione del mercato e i forti movimenti impulsivi.

Al termine dell'elaborazione, tutte le coppie di valute vengono combinate in un unico array di dati con una scala temporale comune. Questo passaggio è di fondamentale importanza: apre la possibilità di ricercare relazioni complesse tra diversi strumenti. Ora possiamo vedere come l'andamento di una coppia di valute influisce sulla volatilità di un'altra o come i pattern a candele giapponesi si relazionano ai volumi di scambio sull'intero mercato.


Implementazione dell'algoritmo Apriori in Python

Dopo aver preparato i dati, passiamo alla fase cruciale: l'implementazione dell'algoritmo Apriori per individuare le regole di associazione nei nostri dati finanziari. Adattiamo l'algoritmo Apriori, originariamente sviluppato per l'analisi di panieri di titoli, per farlo funzionare con serie temporali di coppie di valute. 

Nel contesto del mercato valutario, una "transazione" è un insieme di stati di vari indicatori e coppie di valute in un determinato momento. Per esempio:
  • EURUSD_Trend = Tendenza rialzista
  • GBPUSD_RSI_Zone = Ipercomprato
  • USDJPY_Volatility_Zone = Alta

L'algoritmo individua le combinazioni più ricorrenti di tali stati, sulla base delle quali vengono poi definite le regole di trading.

import pandas as pd
import numpy as np
from collections import defaultdict
from itertools import combinations
import time
import logging

# Setting up logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('apriori_forex_advanced.log'),
        logging.StreamHandler()
    ]
)

class AdvancedForexApriori:
    def __init__(self, min_support=0.01, min_confidence=0.7, max_length=3):
        self.min_support = min_support
        self.min_confidence = min_confidence
        self.max_length = max_length
        
    def find_patterns(self, df):
        start_time = time.time()
        logging.info("Starting advanced pattern search...")
        
        # Group columns by type for more meaningful analysis
        column_groups = {
            'trend': [col for col in df.columns if 'Trend' in col],
            'rsi': [col for col in df.columns if 'RSI_Zone' in col],
            'volume': [col for col in df.columns if 'Volume_Zone' in col],
            'price': [col for col in df.columns if 'Price_Zone' in col],
            'pattern': [col for col in df.columns if 'Pattern' in col]
        }
        
        # Create a list of all columns for analysis
        pattern_cols = []
        for cols in column_groups.values():
            pattern_cols.extend(cols)
        
        logging.info(f"Found {len(pattern_cols)} pattern columns in {len(column_groups)} groups")
        
        # Prepare data
        pattern_df = df[pattern_cols]
        n_rows = len(pattern_df)
        
        # Find single patterns
        logging.info("Finding single patterns...")
        single_patterns = {}
        for col in pattern_cols:
            value_counts = pattern_df[col].value_counts()
            value_counts = value_counts[value_counts/n_rows >= self.min_support]
            for value, count in value_counts.items():
                pattern = f"{col}={value}"
                single_patterns[pattern] = count/n_rows
        
        # Find pair and triple patterns 
        logging.info("Finding complex patterns...")
        complex_rules = []
        
        # Generate column combinations for analysis
        column_combinations = []
        for i in range(2, self.max_length + 1):
            column_combinations.extend(combinations(pattern_cols, i))
        
        total_combinations = len(column_combinations)
        for idx, cols in enumerate(column_combinations, 1):
            if idx % 10 == 0:
                logging.info(f"Processing combination {idx}/{total_combinations}")
            
            # Create a cross-table for the selected columns
            grouped = pattern_df.groupby([*cols]).size().reset_index(name='count')
            grouped['support'] = grouped['count'] / n_rows
            
            # Sort by minimum support
            grouped = grouped[grouped['support'] >= self.min_support]
            
            for _, row in grouped.iterrows():
                # Form all possible combinations of antecedents and consequents
                items = [f"{col}={row[col]}" for col in cols]
                
                for i in range(1, len(items)):
                    for antecedent in combinations(items, i):
                        consequent = tuple(set(items) - set(antecedent))
                        
                        # Calculate the support of the antecedent
                        ant_support = self._calculate_support(pattern_df, antecedent)
                        
                        if ant_support > 0:  # Avoid division by zero
                            confidence = row['support'] / ant_support
                            
                            if confidence >= self.min_confidence:
                                # Count the lift
                                cons_support = self._calculate_support(pattern_df, consequent)
                                lift = confidence / cons_support if cons_support > 0 else 0
                                
                                # Adding additional metrics to evaluate rules
                                leverage = row['support'] - (ant_support * cons_support)
                                conviction = (1 - cons_support) / (1 - confidence) if confidence < 1 else float('inf')
                                
                                rule = {
                                    'antecedent': antecedent,
                                    'consequent': consequent,
                                    'support': row['support'],
                                    'confidence': confidence,
                                    'lift': lift,
                                    'leverage': leverage,
                                    'conviction': conviction
                                }
                                
                                # Sort the rules by additional criteria 
                                if self._is_meaningful_rule(rule):
                                    complex_rules.append(rule)
        
        # Sort the rules by complex metric
        complex_rules.sort(key=self._rule_score, reverse=True)
        
        end_time = time.time()
        logging.info(f"Pattern search completed in {end_time - start_time:.2f} seconds")
        logging.info(f"Found {len(complex_rules)} meaningful rules")
        
        return complex_rules
    
    def _calculate_support(self, df, items):
        """Calculate support for a set of elements"""
        mask = pd.Series(True, index=df.index)
        for item in items:
            col, val = item.split('=')
            mask &= (df[col] == val)
        return mask.mean()
    
    def _is_meaningful_rule(self, rule):
        """Check the rule for its relevance to trading"""
        # The rule should have the high lift and 'leverage'
        if rule['lift'] < 1.5 or rule['leverage'] < 0.01:
            return False
            
        # At least one element should be related to a trend or RSI
        has_trend_or_rsi = any('Trend' in item or 'RSI' in item 
                              for item in rule['antecedent'] + rule['consequent'])
        if not has_trend_or_rsi:
            return False
            
        return True
    
    def _rule_score(self, rule):
        """Calculate the rule complex evaluation"""
        return (rule['lift'] * 0.4 + 
                rule['confidence'] * 0.3 + 
                rule['support'] * 0.2 + 
                rule['leverage'] * 0.1)

# Load data
logging.info("Loading data...")
data = pd.read_csv('forex_data_combined_20241116_074242.csv', 
                  sep='\t', 
                  encoding='utf-16',
                  index_col='DateTime')
logging.info(f"Data loaded, shape: {data.shape}")

# Apply the algorithm
apriori = AdvancedForexApriori(min_support=0.01, min_confidence=0.7, max_length=3)
rules = apriori.find_patterns(data)

# Display results
logging.info("\nTop 10 trading rules:")
for i, rule in enumerate(rules[:10], 1):
    logging.info(f"\nRule {i}:")
    logging.info(f"IF {' AND '.join(rule['antecedent'])}")
    logging.info(f"THEN {' AND '.join(rule['consequent'])}")
    logging.info(f"Support: {rule['support']:.3f}")
    logging.info(f"Confidence: {rule['confidence']:.3f}")
    logging.info(f"Lift: {rule['lift']:.3f}")
    logging.info(f"Leverage: {rule['leverage']:.3f}")
    logging.info(f"Conviction: {rule['conviction']:.3f}")

# Save results
results_df = pd.DataFrame(rules)
results_df.to_csv('forex_rules_advanced.csv', index=False, sep='\t', encoding='utf-16')
logging.info("Results saved to forex_rules_advanced.csv")


Adattamento delle regole di associazione per l'analisi delle coppie di valute

Nel corso del mio lavoro di adattamento dell'algoritmo Apriori al mercato dei cambi, ho incontrato sfide interessanti. Sebbene questo metodo fosse stato originariamente creato per analizzare gli acquisti nei negozi fisici, il suo potenziale per il Forex mi è sembrato promettente.

La difficoltà principale risiedeva nel fatto che il mercato Forex è radicalmente diverso dallo shopping tradizionale in un negozio. Nel corso degli anni trascorsi a lavorare nei mercati finanziari, mi sono abituato a gestire prezzi e indicatori in continua evoluzione. Ma come si applica un algoritmo che di solito cerca solo correlazioni tra banane e latte sugli scontrini del supermercato? 

A seguito dei miei esperimenti, è nato un sistema composto da cinque parametri. Li ho testati tutti a fondo.

“Support" si è rivelato un parametro di misurazione molto complesso. Una volta stavo quasi per includere una regola con ottime prestazioni in un sistema di trading, ma “Support” era solo di 0,02. Per fortuna me ne sono accorto in tempo - in pratica, una regola del genere si attiverebbe solo una volta ogni cento anni!

“Confidence" si è rivelata più semplice. Quando lavori nel mercato, impari presto che anche una probabilità del 70% è un ottimo indicatore. La cosa fondamentale è gestire i rischi con saggezza con il restante 30%. Dobbiamo sempre tenere presente la gestione del rischio. Senza di essa, subirete un drawdown o addirittura un tracollo anche se avete il Santo Graal tra le mani.

“Lift” è diventato il mio indicatore preferito. Dopo centinaia di ore di test, ho notato uno schema: le regole con un “lift” superiore a 1,5 funzionano effettivamente sul mercato reale. Questa scoperta ha avuto un profondo impatto sul mio approccio alla classificazione dei segnali. 

Avere a che fare con "Leverage" si è rivelato divertente. Inizialmente volevo escluderlo completamente dal sistema, considerandolo inutile. Ma durante un periodo di particolare volatilità del mercato, ha contribuito a distinguere la maggior parte dei falsi segnali.

"Conviction" è stata aggiunta per ultima, dopo aver consultato i forum. Mi ha aiutato a capire quanto sia importante questo indicatore per valutare il reale significato dei pattern individuati.

La cosa che mi ha sorpreso di più è stata la capacità dell'algoritmo di individuare correlazioni inaspettate tra diverse coppie di valute. Ad esempio, chi avrebbe mai pensato che certi pattern nella coppia EUR/USD potessero prevedere i movimenti di USD/JPY con tale precisione? Nei 9 anni di lavoro nel mercato, non avevo notato molte delle relazioni che l'algoritmo ha individuato. Sebbene il pair trading, il basket trading e l'arbitraggio fossero un tempo il mio campo d'azione, ricordo ancora i tempi in cui cmillion stava iniziando a sviluppare i suoi robot basati sui movimenti reciproci delle coppie di valute.

Ora proseguo la mia ricerca, testando nuove combinazioni di indicatori e periodi di tempo. Il mercato è in continua evoluzione e ogni giorno porta nuove scoperte. La prossima settimana ho intenzione di pubblicare i risultati dei test del sistema sui dati annuali, nonché i primi risultati in tempo reale dell'algoritmo su un conto demo di trading. Ci sono diverse scoperte molto interessanti.

A dire il vero, non mi aspettavo nemmeno che questo progetto arrivasse a questo punto. Tutto è iniziato come un semplice esperimento di data mining e tentativi di classificare in modo rigoroso tutti i movimenti di mercato per le esigenze degli algoritmi di classificazione, e alla fine si è trasformato in un vero e proprio sistema di trading. Penso di aver appena iniziato a comprendere il vero potenziale di questo approccio.


Caratteristiche dell’implementazione per il Forex

Torniamo un attimo al codice stesso. Il nostro codice presenta diversi importanti adattamenti dell'algoritmo per la gestione dei dati finanziari:

column_groups = {
    'trend': [col for col in df.columns if 'Trend' in col],
    'rsi': [col for col in df.columns if 'RSI_Zone' in col],
    'volume': [col for col in df.columns if 'Volume_Zone' in col],
    'price': [col for col in df.columns if 'Price_Zone' in col],
    'pattern': [col for col in df.columns if 'Pattern' in col]
}

Questo raggruppamento aiuta a trovare combinazioni di indicatori più significative e riduce la complessità computazionale.

def _is_meaningful_rule(self, rule):
    if rule['lift'] < 1.5 or rule['leverage'] < 0.01:
        return False
    has_trend_or_rsi = any('Trend' in item or 'RSI' in item 
                          for item in rule['antecedent'] + rule['consequent'])
    if not has_trend_or_rsi:
        return False
    return True

Selezioniamo solo le regole con una forte significatività statistica (lift > 1,5) e l'inclusione obbligatoria di indicatori di trend o RSI.

def _rule_score(self, rule):
    return (rule['lift'] * 0.4 + 
            rule['confidence'] * 0.3 + 
            rule['support'] * 0.2 + 
            rule['leverage'] * 0.1)

Il punteggio ponderato aiuta a classificare le regole in base alla loro potenziale utilità per il trading.


Visualizzazione delle associazioni trovate

Dopo aver individuato le regole di associazione, dobbiamo visualizzarle e analizzarle correttamente. A questo scopo, ho sviluppato la classe speciale ForexRulesVisualizer, che offre diversi metodi di analisi visiva dei modelli individuati.

Distribuzione delle metriche delle regole

Il primo passo dell'analisi consiste nel comprendere la distribuzione delle principali metriche delle regole individuate. Il grafico di distribuzione di 'support', 'confidence', 'lift' e 'leverage' aiuta a valutare la qualità delle regole trovate e se necessario, a regolare i parametri dell'algoritmo.

Uno strumento particolarmente utile è risultato essere il grafico di rete interattivo, che mostra chiaramente le connessioni tra le diverse condizioni di mercato. In questo grafico, i nodi sono gli stati degli indicatori (ad esempio "EURUSD_Trend=Uptrend" o "USDJPY_RSI_Zone=Overbought"), e i bordi rappresentano le regole trovate, dove lo spessore del bordo è proporzionale al valore di 'lift'.

Mappa di calore delle interazioni tra coppie di valute


Per analizzare le relazioni tra le coppie di valute, utilizzo una mappa di calore, che mostra la forza delle correlazioni tra i diversi strumenti. Questo aiuta a identificare le coppie di valute che più spesso si influenzano a vicenda, aspetto fondamentale per costruire un portafoglio di trading diversificato.


Creazione di segnali di trading

Una volta individuate e visualizzate le regole di associazione, il passo successivo fondamentale è trasformarle in segnali di trading. A questo scopo, ho sviluppato la classe ForexSignalGenerator, che analizza lo stato attuale del mercato e genera segnali di trading in base alle regole individuate.

import pandas as pd
import numpy as np
from datetime import datetime
import logging

class ForexSignalGenerator:
    def __init__(self, rules_df, min_rule_strength=0.5):
        """
        Signal generator initialization
        
        Parameters:
        rules_df: DataFrame with association rules
        min_rule_strength: minimum rule strength to generate a signal
        """
        self.rules_df = rules_df
        self.min_rule_strength = min_rule_strength
        self.active_signals = {}
        
    def calculate_rule_strength(self, rule):
        """
        Comprehensive assessment of the rule strength
        Takes into account all metrics with different weights
        """
        strength = (
            rule['lift'] * 0.4 +        # Main weight on 'lift'
            rule['confidence'] * 0.3 +   # Rule confidence
            rule['support'] * 0.2 +      # Occurrence frequency
            rule['leverage'] * 0.1       # Improvement over randomness
        )
        
        # Additional bonus for having trend indicators
        if any('Trend' in item for item in rule['antecedent']):
            strength *= 1.2
            
        return strength
        
    def analyze_market_state(self, current_data):
        """
        Current market state analysis
        
        Parameters:
        current_data: DataFrame with current indicator values
        """
        signals = []
        state = self._create_market_state(current_data)
        
        # Find all the matching rules
        matching_rules = self._find_matching_rules(state)
        
        # Grouping rules by currency pairs
        for pair in ['EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF']:
            pair_rules = [r for r in matching_rules if any(pair in c for c in r['consequent'])]
            if pair_rules:
                signal = self._generate_pair_signal(pair, pair_rules)
                signals.append(signal)
        
        return signals
    
    def _create_market_state(self, data):
        """Forming the current market state"""
        state = []
        for col in data.columns:
            if any(x in col for x in ['_Zone', '_Pattern', 'Trend']):
                state.append(f"{col}={data[col].iloc[-1]}")
        return set(state)
    
    def _find_matching_rules(self, state):
        """Searching for rules that match the current state"""
        matching_rules = []
        
        for _, rule in self.rules_df.iterrows():
            # Check if all the rule conditions are met
            if all(cond in state for cond in rule['antecedent']):
                strength = self.calculate_rule_strength(rule)
                if strength >= self.min_rule_strength:
                    rule['calculated_strength'] = strength
                    matching_rules.append(rule)
        
        return matching_rules
    
    def _generate_pair_signal(self, pair, rules):
        """Generating a signal for a specific currency pair"""
        # Divide the rules by signal type
        trend_signals = defaultdict(float)
        
        for rule in rules:
            # Looking for trend-related consequents
            trend_cons = [c for c in rule['consequent'] if pair in c and 'Trend' in c]
            if trend_cons:
                for cons in trend_cons:
                    trend = cons.split('=')[1]
                    trend_signals[trend] += rule['calculated_strength']
        
        # Determine the final signal
        if trend_signals:
            strongest_trend = max(trend_signals.items(), key=lambda x: x[1])
            return {
                'pair': pair,
                'signal': strongest_trend[0],
                'strength': strongest_trend[1],
                'timestamp': datetime.now()
            }
        
        return None

# Usage example
def run_trading_system(data, rules_df):
    """
    Trading system launch
    
    Parameters:
    data: DataFrame with historical data
    rules_df: DataFrame with association rules
    """
    signal_generator = ForexSignalGenerator(rules_df)
    
    # Simulate a pass along historical data
    signals_history = []
    
    for i in range(len(data) - 1):
        current_slice = data.iloc[i:i+1]
        signals = signal_generator.analyze_market_state(current_slice)
        
        for signal in signals:
            if signal:
                signals_history.append({
                    'datetime': current_slice.index[0],
                    'pair': signal['pair'],
                    'signal': signal['signal'],
                    'strength': signal['strength']
                })
    
    return pd.DataFrame(signals_history)

# Loading historical data and rules
data = pd.read_csv('forex_data_combined_20241116_090857.csv', 
                  sep='\t', 
                  encoding='utf-16',
                  index_col='DateTime',
                  parse_dates=True)

rules_df = pd.read_csv('forex_rules_advanced.csv',
                      sep='\t',
                      encoding='utf-16')
rules_df['antecedent'] = rules_df['antecedent'].apply(eval)
rules_df['consequent'] = rules_df['consequent'].apply(eval)

# Launch the test
signals_df = run_trading_system(data, rules_df)

# Analyze the results
print("Generated signals statistics:")
print(signals_df.groupby('pair')['signal'].value_counts())


Valutare la solidità delle regole

Dopo lunghi esperimenti di visualizzazione delle regole, è giunto il momento della parte più difficile: la creazione di veri e propri segnali di trading. Lo ammetto, questo compito mi ha fatto sudare parecchio. Una cosa è individuare schemi interessanti sui grafici, tutt'altra cosa è trasformarli in un sistema di trading efficace.

Ho deciso di creare un modulo separato chiamato ForexSignalGenerator. Inizialmente, volevo semplicemente generare segnali secondo le regole più rigide, ma mi sono presto reso conto che la realtà è molto più complessa. Il mercato è in continua evoluzione e una regola che funzionava bene ieri potrebbe non funzionare più oggi.

Ho dovuto adottare un approccio serio per valutare la solidità delle regole. Dopo diversi esperimenti infruttuosi, ho sviluppato un sistema di scala. La parte più difficile è stata scegliere le proporzioni - probabilmente ho provato decine di combinazioni. Alla fine, ho deciso di assegnare il 40% del punteggio finale al fattore "lift" (un indicatore davvero fondamentale), il 30% a "confidence", il 20% a "support" e il 10% a "leverage".

Curiosamente,i segnali più forti, spesso si ottenevano quando la regola includeva una componente di tendenza. Ho persino aggiunto uno speciale bonus del 20% alla forza di tali regole e l'esperienza ha dimostrato che è giustificato.

Ho dovuto impegnarmi a fondo anche nell'analisi dell'attuale situazione di mercato. Inizialmente, ho semplicemente confrontato i valori attuali degli indicatori con le condizioni previste dalle regole. Ma poi mi sono reso conto che dovevo tenere conto del contesto più ampio. Ad esempio, ho aggiunto la verifica del trend generale degli ultimi periodi, dello stato di volatilità e persino dell'ora del giorno.

Attualmente, il sistema analizza circa 20 parametri diversi per ogni coppia di valute. Alcuni dei pattern che ho trovato mi hanno davvero sorpreso. 

Certo, il sistema è ancora ben lontano dall'essere perfetto. A volte mi sorprendo a pensare che devo aggiungere fattori fondamentali. Tuttavia, ho deciso di rimandare questa questione in un secondo momento. Innanzitutto, voglio terminare la versione attuale. 


Selezione e aggregazione dei segnali

Durante lo sviluppo del sistema, mi sono presto reso conto che trovare semplicemente delle regole non è sufficiente - abbiamo bisogno di un controllo rigoroso della qualità dei segnali. Dopo alcuni trade infruttuosi, è diventato chiaro che la selezione è forse ancora più importante dell'individuazione dei pattern stessi.

Ho iniziato con una semplice soglia di forza minima della regola. Inizialmente l'ho impostato a 0,5, ma continuavo a ricevere falsi positivi. Dopo due settimane di test, l'ho aumentato a 0,7 e la situazione è migliorata sensibilmente. Il numero dei segnali è diminuito di circa un terzo, ma la loro qualità è migliorata significativamente.

Il secondo livello di selezione è stato introdotto in seguito ad un episodio particolarmente grave. C'era una regola con ottime prestazioni, ho aperto una posizione seguendola, ma il mercato è andato nettamente nella direzione opposta. Quando ho iniziato ad approfondire la questione, è emerso che altre regole, in quel momento, davano segnali opposti. Da allora, verifico la coerenza aprendo solo se diverse regole puntano nella stessa direzione.

Gestire la volatilità si è rivelato interessante. Ho notato che durante i periodi di calma il sistema funziona come un orologio, ma non appena il mercato si anima, iniziano i problemi. Ho quindi aggiunto un filtro dinamico tramite ATR. Se la volatilità supera il 75° percentile negli ultimi 20 giorni, aumentiamo del 20% i requisiti di solidità delle regole.

La parte più difficile è stata verificare i segnali contrastanti. Capita che alcune regole dicano di comprare, altre di vendere e tutte le regole hanno dei parametri validi. Ho provato diversi approcci, ma alla fine ho optato per una soluzione semplice: se ci sono contraddizioni significative nei segnali, saltiamo questa situazione. Così facendo, perdiamo alcune opportunità, ma riduciamo significativamente i rischi.

Il mese prossimo aggiungerò la possibilità di ordinare i dati per tempo. Ho notato che in certe ore le regole funzionano decisamente peggio. Ciò è particolarmente evidente durante i periodi di bassa liquidità e in concomitanza con la pubblicazione di notizie importanti. Penso che questo dovrebbe aumentare ulteriormente la percentuale di operazioni andate a buon fine.


Risultati del test

Dopo diversi mesi di sviluppo del sistema, mi sono trovato di fronte a una domanda fondamentale: come valutare correttamente la robustezza di ciascuna regola individuata? Sulla carta sembrava tutto semplice, ma il mercato reale ha presto messo a nudo tutte le debolezze dell'approccio iniziale.

A seguito di lunghi esperimenti, sono giunto a un sistema di pesi per diversi fattori. Ho reso "Lift" la componente principale (influenza del 40%) - l'esperienza ha dimostrato che si tratta di un indicatore di importanza davvero cruciale. "Confidence" vale il 30% - dopotutto, anche la fiducia nella regola è molto importante. A "Support" e "leverage" è stato assegnato un peso minore - agiscono più come filtri.

La classificazione dei segnali si è rivelata una storia a parte. All'inizio ho provato a fare trading seguendo tutte le regole di seguito, ma mi sono subito reso conto del mio errore. Pertanto, ho dovuto introdurre un sistema di ordinamento multilivello. Innanzitutto, classifichiamo le regole deboli in base alla soglia minima di solidità. Successivamente controlliamo se il segnale è confermato da più regole - quelle singole sono generalmente meno affidabili.

Tenere conto della volatilità si è rivelato particolarmente importante. Durante i periodi di calma, il sistema ha funzionato perfettamente, ma non appena la volatilità è aumentata, il numero di falsi segnali è aumentato vertiginosamente. Ho dovuto aggiungere filtri dinamici che diventano più restrittivi all'aumentare della volatilità.

La fase di test del sistema ha richiesto quasi tre mesi. L'ho eseguito su uno storico di due anni per quattro coppie principali. I risultati sono stati del tutto inaspettati. Ad esempio, la coppia USDJPY ha mostrato la performance migliore - il 65% di operazioni profittevoli con un RR di 1,6. Ma la coppia GBPUSD è stata deludente - solo il 58% con un RR di 1,4.

È interessante notare che le regole con un valore di 'lift' superiore a 2,0 e un valore di 'confidence' superiore a 0,8 hanno costantemente mostrato i risultati migliori per tutte le coppie. A quanto pare, questi livelli rappresentano effettivamente una sorta di soglie di rilevanza naturale nel mercato Forex.


Ulteriori miglioramenti

Al momento, vedo diverse direzioni in cui migliorare il sistema. Innanzitutto, i parametri delle regole devono essere resi più dinamici - il mercato è in continua evoluzione e il sistema deve adattarsi. In secondo luogo, si nota una chiara mancanza di attenzione agli aspetti macroeconomici e al contesto dell'attualità. Sì, complicherà il sistema, ma i potenziali vantaggi ne valgono la pena.

L'applicazione di filtri adattivi sembra particolarmente interessante. Le diverse fasi del mercato richiedono chiaramente impostazioni di sistema differenti. Al momento è implementato in modo rudimentale, ma riesco già ad individuare diversi modi per migliorarlo.

La prossima settimana ho intenzione di iniziare a testare una nuova versione con ottimizzazione dinamica delle dimensioni delle posizioni. I risultati preliminari basati sui dati storici sembrano promettenti, ma il mercato reale, come sempre, apporterà i propri aggiustamenti.


Conclusioni

L'utilizzo di regole di associazione nel trading algoritmico apre interessanti opportunità per individuare pattern di mercato non ovvi. La chiave del successo in questo caso risiede in una corretta preparazione dei dati, un'attenta selezione delle regole e un sistema di generazione del segnale ben congegnato.

È importante ricordare che qualsiasi sistema di trading richiede un monitoraggio costante e un adattamento alle mutevoli condizioni di mercato. Le regole associative sono un potente strumento di analisi, ma devono essere utilizzate in combinazione con altri metodi di analisi tecnica e fondamentale.

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

File allegati |
Dataset.mq5 (4.29 KB)
Ultimi commenti | Vai alla discussione (2)
Aleksey Vyazmikin
Aleksey Vyazmikin | 22 nov 2024 a 18:27

A quanto pare, si presume che il lettore debba già avere una certa conoscenza di tale metodo, e in caso contrario?

Non capisco le metriche citate, in particolare:

Lift è diventato il mio indicatore preferito. Dopo centinaia di ore di test, ho notato uno schema: le regole con lift superiore a 1,5 funzionano davvero nel mercato reale. Questa scoperta ha influenzato seriamente il mio approccio al filtraggio dei segnali.

Se ho capito bene il metodo, i segnali correlati vengono cercati nei segmenti quantistici. Ma non ho capito il passo successivo. Qual è il target? Presumo che le regole risultanti vengano controllate rispetto all'obiettivo e valutate rispetto alle metriche.

Se è così, questo metodo riprende il mio, ed è interessante valutare le prestazioni e l'efficienza.

Cks1295
Cks1295 | 24 nov 2024 a 05:28
Ciao, Eugene! Ti prego di scrivermi (ti ho inviato la richiesta di essere aggiunto come amico, c'è un argomento serio per la conversazione (modelli proponenti e la loro applicazione pratica). Grazie per la tua risposta, con rispetto, Andrey
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.
Analisi volumetrica basata su reti neurali come chiave per i trend futuri Analisi volumetrica basata su reti neurali come chiave per i trend futuri
L'articolo esplora la possibilità di migliorare la previsione dei prezzi basata sull'analisi dei volumi di scambio, integrando i principi dell'analisi tecnica con l'architettura delle reti neurali LSTM. Particolare attenzione è dedicata all'individuazione e all'interpretazione dei volumi anomali, all'utilizzo del clustering, alla creazione di caratteristiche basate sui volumi e alla loro definizione nel contesto dell'apprendimento automatico.
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.
Analisi dell'impatto dei fattori meteorologici sulle valute dei paesi agricoli usando Python Analisi dell'impatto dei fattori meteorologici sulle valute dei paesi agricoli usando Python
Qual è la relazione tra le condizioni meteorologiche e il mercato Forex? La teoria economica classica ha a lungo ignorato l'influenza di fattori come le condizioni meteorologiche sul comportamento del mercato. Ma tutto è cambiato. Proviamo a individuare delle correlazioni tra le condizioni meteorologiche e l'andamento delle valute agricole sul mercato.