English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
preview
Utilisation des règles d'association dans l'analyse des données Forex

Utilisation des règles d'association dans l'analyse des données Forex

MetaTrader 5Intégration |
54 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introduction au concept des règles d'association

Le trading algorithmique moderne exige de nouvelles approches d'analyse. Le marché est en constante évolution, et les méthodes classiques d'analyse technique ne permettent plus d'identifier les relations complexes qui s'y rattachent.

Je travaille avec des données depuis longtemps et j'ai constaté que de nombreuses idées fructueuses proviennent de domaines connexes. Aujourd'hui, je souhaite partager mon expérience de l'utilisation des règles d'association dans le trading. Cette méthode a fait ses preuves dans l'analyse des données de vente au détail, nous permettant de trouver des liens entre les achats, les transactions, les variations de prix et l'offre et la demande futures. Et si on l'appliquait au marché des changes ?

L'idée de base est simple : nous recherchons des schémas stables de comportement des prix, des indicateurs et leurs combinaisons. Par exemple, à quelle fréquence une hausse de l'EURUSD fait-elle suite à une baisse de l'USDJPY ? Quelles sont les conditions qui précèdent le plus souvent les mouvements importants ?

Dans cet article, je vais vous présenter le processus complet de création d'un système de trading basé sur cette idée. Nous allons :

  1. Collecter les données historiques en MQL5
  2. Les analyser en Python
  3. Identifier les tendances significatives
  4. Les transformer en signaux de trading

Pourquoi cette liste en particulier ? MQL5 est idéal pour travailler avec les données boursières et automatiser les transactions. Python fournit à son tour de puissants outils d'analyse. D'après mon expérience, je peux affirmer que cette combinaison est très efficace pour développer des systèmes de trading.

Le code contiendra de nombreuses choses intéressantes, notamment en ce qui concerne l'application des règles d'association au Forex.


Collecte et préparation des données historiques du Forex

Il est extrêmement important pour nous de collecter et de préparer toutes les données dont nous avons besoin. Prenons comme base les données H1 des principales paires de devises des deux dernières années (depuis 2022).

Nous allons maintenant créer un script MQL5 qui collectera et exportera les données nécessaires au format 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);
      }
   }
}
//+------------------------------------------------------------------+


Traitement des données en Python

Après avoir constitué un ensemble de données, il est important de les traiter correctement. 

Pour cela, j'ai créé la classe spéciale ForexDataProcessor, qui se charge de tout le travail ingrat. Examinons ses principaux composants.

Nous allons commencer par charger les données. Notre fonction utilise des données horaires pour les principales paires de devises : EURUSD, GBPUSD, USDJPY et USDCHF. Les données doivent être au format CSV et comporter les principales caractéristiques de prix.

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)


Une fois le chargement réussi, la partie la plus intéressante commence : le calcul des indicateurs techniques. Je m'appuie ici sur tout un arsenal d'outils éprouvés par le temps. Les moyennes mobiles permettent d'identifier les tendances de durée variable. Une SMA(50) agit souvent comme support ou résistance dynamique. L'oscillateur RSI avec une période classique de 14 est performant pour déterminer les zones de sur-achat et de sur-vente du marché. Le MACD est indispensable pour identifier les points de momentum et de retournement. Les Bandes de Bollinger donnent une image claire de la volatilité actuelle du marché.

# 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 discrétisation des données mérite une attention particulière. Toutes les valeurs continues doivent être réparties en catégories claires. En la matière, il est important de trouver un juste milieu : une division trop abrupte compliquera la recherche de tendances, et une division trop faible entraînera la perte de nuances importantes du marché. Par exemple, pour déterminer la tendance, une division plus simple est plus efficace : celle qui repose sur la position du prix par rapport à la moyenne :

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

Les figures de chandeliers nécessitent également une approche particulière. Sur la base d'une analyse statistique, je distingue les Doji pour les tailles de corps de bougie minimales, les Long_Bullish et les Long_Bearish pour les mouvements de prix extrêmes. Cette classification nous permet d'identifier clairement les moments d'indécision du marché et les fortes impulsions.

À la fin du traitement, toutes les paires de devises sont combinées en un seul tableau de données avec une échelle de temps commune. Cette étape est d'une importance fondamentale : elle ouvre la possibilité de rechercher des relations complexes entre différents instruments. Nous pouvons désormais observer comment la tendance d'une paire influence la volatilité d'une autre, ou comment les configurations de chandeliers japonais sont liées aux volumes d'échanges sur l'ensemble du marché.


Implémentation de l'algorithme Apriori en Python

Après avoir préparé les données, nous passons à l'étape clé : la mise en œuvre de l'algorithme Apriori pour trouver des règles d'association dans nos données financières. Nous adaptons l'algorithme Apriori, initialement développé pour l'analyse des paniers de biens et services, afin qu'il fonctionne avec des séries temporelles de paires de devises. 

Dans le contexte du marché des changes, une « transaction » désigne un ensemble d'états de divers indicateurs et paires de devises à un moment donné. Par exemple :
  • Tendance EUR/USD = Haussière
  • Zone RSI GBPUSD = Sur-achat
  • Zone de volatilité USD/JPY = Élevée

L'algorithme recherche les combinaisons fréquentes de ces états, sur la base desquelles des règles de trading sont ensuite établies.

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")


Adaptation des règles d'association pour l'analyse des paires de devises

Dans le cadre de mon travail d'adaptation de l'algorithme Apriori au marché des changes, j'ai rencontré des défis intéressants. Bien que cette méthode ait été initialement créée pour analyser les achats en magasin, son potentiel pour le Forex me semblait prometteur.

La principale difficulté résidait dans le fait que le marché des changes est radicalement différent des achats classiques en magasin. Au fil des années passées à travailler sur les marchés financiers, je me suis habitué à gérer des prix et des indicateurs en constante évolution. Mais comment appliquer un algorithme qui, d'habitude, se contente de rechercher des liens entre les bananes et le lait sur les tickets de caisse des supermarchés ? 

Mes expériences ont abouti à un système de 5 indicateurs. Je les ai tous testés minutieusement.

Le « support » s'est avéré être un indicateur très délicat. J'ai failli inclure une fois une règle aux performances excellentes dans un système de trading, mais le support n'était que de 0,02. Heureusement, je l'ai remarqué à temps – en pratique, une telle règle ne s'appliquerait qu'une fois tous les 100 ans !

La « confiance » s'est avérée plus simple. Quand on travaille sur les marchés, on apprend vite qu'une probabilité de 70% est déjà un excellent indicateur. L'essentiel est de gérer judicieusement les risques avec les 30% restants. Nous devons toujours garder la gestion des risques à l'esprit. Sans cela, vous subirez un revers, voire une perte, même si vous tenez le Graal entre vos mains.

Le « Lift » est devenu mon indicateur préféré. Après des centaines d'heures de tests, j'ai remarqué une tendance : les règles avec un lift supérieur à 1,5 fonctionnent effectivement sur le marché réel. Cette découverte a eu un impact profond sur mon approche du tri des signaux. 

L'utilisation de l'effet de levier s'est avérée amusante. Au départ, je voulais l'exclure complètement du système, le considérant comme inutile. Mais lors d'une période particulièrement volatile du marché, cela a permis d'éliminer la plupart des faux signaux.

La « conviction » a été ajoutée en dernier, après consultation des forums. Cela m'a permis de comprendre l'importance de cet indicateur pour évaluer la signification réelle des tendances observées.

Ce qui m'a le plus surpris, c'est la façon dont l'algorithme trouve des liens inattendus entre différentes paires de devises. Qui aurait cru, par exemple, que certains schémas de l'EURUSD pouvaient prédire les mouvements de l'USDJPY avec une telle précision ? En 9 ans d'expérience sur le marché, je n'avais pas remarqué beaucoup des relations que l'algorithme a découvertes. Bien que le trading de paires, le trading de paniers et l'arbitrage aient été mon domaine, je me souviens encore de l'époque où cmillion commençait tout juste à développer ses robots basés sur les mouvements mutuels des paires.

Je poursuis maintenant mes recherches, en testant de nouvelles combinaisons d'indicateurs et de périodes. Le marché est en constante évolution et chaque jour apporte son lot de nouvelles découvertes. La semaine prochaine, je prévois de publier les résultats des tests du système sur des données annuelles, ainsi que les premiers résultats en direct de l'algorithme sur un compte de démonstration réel. On y a découvert plusieurs résultats très intéressants.

Honnêtement, je ne m'attendais même pas à ce que ce projet aille aussi loin. Tout a commencé par une simple expérience d'exploration de données et de tentatives de classification rigoureuse de tous les mouvements du marché pour répondre aux besoins des algorithmes de classification, pour finalement se transformer en un système de trading à part entière. Je crois que je commence tout juste à comprendre le véritable potentiel de cette approche.


Caractéristiques de mise en œuvre pour le Forex

Revenons un peu au code lui-même. Notre code comporte plusieurs adaptations importantes de l'algorithme de traitement des données financières :

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]
}

Ce regroupement permet de trouver des combinaisons d'indicateurs plus pertinentes et réduit la complexité des calculs.

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

Nous ne sélectionnons que les règles présentant une forte signification statistique (lift > 1,5) et l'inclusion obligatoire d'indicateurs de tendance ou de RSI.

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

Le score pondéré permet de classer les règles en fonction de leur utilité potentielle pour le trading.


Visualisation des associations trouvées

Après avoir identifié les règles d'association, nous devons les visualiser et les analyser correctement. À cette fin, j'ai développé la classe spéciale ForexRulesVisualizer, qui offre plusieurs méthodes d'analyse visuelle des modèles trouvés.

Distribution des métriques de règles

La première étape de l'analyse consiste à comprendre la distribution des principales métriques des règles trouvées. Le graphique de distribution du « support », de la « confiance », du « lift » et du « levier » permet d'évaluer la qualité des règles trouvées et, si nécessaire, d'ajuster les paramètres de l'algorithme.

L'outil le plus utile était le graphique de réseau interactif, qui montre clairement les liens entre les différentes conditions de marché. Dans ce graphique, les nœuds représentent les états indicateurs (par exemple, "EURUSD_Trend=Uptrend" ou "USDJPY_RSI_Zone=Overbought"), et les bords représentent les règles trouvées, où l'épaisseur du bord est proportionnelle à la valeur 'lift'.

Carte thermique des interactions entre paires de devises


Pour analyser les relations entre les paires de devises, j'utilise une carte thermique, qui montre la force des relations entre les différents instruments. Cela permet d'identifier les paires qui s'influencent le plus souvent, ce qui est essentiel pour constituer un portefeuille de trading diversifié.


Création de signaux de trading

Une fois les règles d'association identifiées et visualisées, l'étape suivante importante consiste à les transformer en signaux de trading. À cette fin, j'ai développé la classe ForexSignalGenerator, qui analyse l'état actuel du marché et génère des signaux de trading en fonction des règles identifiées.

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())


Évaluer la force des règles

Après de longues expériences de visualisation des règles, il est temps de passer à la partie la plus difficile : la création de véritables signaux de trading. Je l'avoue, cette tâche m'a beaucoup fait transpirer. Trouver de belles configurations sur les graphiques est une chose, les transformer en un système de trading fonctionnel en est une autre.

J'ai décidé de créer un module séparé, ForexSignalGenerator. Au départ, je voulais simplement générer des signaux selon les règles les plus strictes, mais je me suis vite rendu compte que tout était beaucoup plus compliqué. Le marché est en constante évolution, et une règle qui fonctionnait bien hier peut ne plus fonctionner aujourd'hui.

J'ai dû adopter une approche sérieuse pour évaluer la solidité des règles. Après plusieurs expériences infructueuses, j'ai mis au point un système d'échelle. Ce qui m'a le plus posé problème, c'est le choix des proportions – j'ai probablement essayé des dizaines de combinaisons. Au final, j'ai opté pour « lift » qui représente 40% de l'évaluation finale (il s'agit d'un indicateur vraiment clé), « confiance » - 30%, « support » - 20% et « levier » - 10%.

Il est intéressant de noter que les signaux les plus forts étaient souvent obtenus lorsque la règle comportait une composante de tendance. J'ai même ajouté un bonus spécial de 20% à la force de ces règles, et la pratique a montré que cela était justifié.

J'ai également dû travailler dur pour gérer l'analyse de la situation actuelle du marché. Dans un premier temps, j'ai simplement comparé les valeurs actuelles des indicateurs aux conditions des règles. Mais je me suis alors rendu compte que je devais prendre en compte le contexte plus large. Par exemple, j'ai ajouté la vérification de la tendance générale sur les dernières périodes, l'état de la volatilité, voire l'heure de la journée.

Actuellement, le système analyse environ 20 paramètres différents pour chaque paire de devises. Certains des motifs que j'ai découverts m'ont vraiment surpris. 

Bien sûr, le système est encore loin d'être parfait. Parfois, je me surprends à penser que je dois ajouter des facteurs fondamentaux. Mais je garde cela pour plus tard. Je souhaite tout d'abord terminer la version actuelle. 


Tri et agrégation des signaux

Lors du développement du système, j'ai rapidement réalisé que la simple définition des règles ne suffisait pas ; nous avions besoin d'un contrôle strict de la qualité des signaux. Après quelques transactions infructueuses, il est devenu évident que le tri est peut-être encore plus important que la recherche de modèles eux-mêmes.

J'ai commencé par un seuil simple correspondant à la force minimale des règles. Au début, je l'avais réglé à 0,5, mais j'obtenais sans cesse des faux positifs. Après deux semaines de tests, je l'ai augmenté à 0,7, et la situation s'est nettement améliorée. Le nombre de signaux a diminué d'environ un tiers, mais leur qualité a considérablement augmenté.

Le second niveau de tri est apparu après un incident particulièrement choquant. Il existait une règle qui fonctionnait parfaitement, j'ai ouvert une position en conséquence, mais le marché a évolué strictement dans la direction opposée. Lorsque j'ai commencé à me pencher sur la question, il s'est avéré que d'autres règles en vigueur à ce moment-là envoyaient des signaux contraires. Depuis, je vérifie la cohérence en n'ouvrant la position que si plusieurs règles pointent dans la même direction.

Gérer la volatilité s'est avéré intéressant. J'ai remarqué que pendant les périodes calmes, le système fonctionne comme sur des roulettes. Mais dès que le marché s'anime, les problèmes commencent. J'ai donc ajouté un filtre dynamique sur l’ATR. Si la volatilité dépasse le 75e percentile au cours des 20 derniers jours, nous augmentons de 20% les exigences relatives à la robustesse des règles.

Le plus difficile a été de vérifier les signaux contradictoires. Il arrive que certaines règles préconisent d'acheter, d'autres de vendre, et toutes les règles ont de bons paramètres. J'ai essayé différentes approches, mais j'ai finalement opté pour une solution simple : s'il existe des contradictions importantes dans les signaux, nous ignorons cette situation. Ce faisant, nous perdons certaines opportunités, mais nous réduisons considérablement les risques.

Le mois prochain, j'ajouterai le tri par heure. J'ai remarqué qu'à certaines heures, les règles fonctionnent nettement moins bien. Cela est particulièrement vrai en période de faible liquidité et lors de la publication d'informations importantes. Je pense que cela devrait encore augmenter le pourcentage de transactions réussies.


Résultats des tests

Après plusieurs mois de développement du système, je me suis trouvé confronté à une question cruciale : comment évaluer correctement la force de chaque règle trouvée ? Tout paraissait simple sur le papier, mais le marché réel a rapidement mis en évidence toutes les faiblesses de l'approche initiale.

À l'issue de longues expériences, j'ai mis au point un système de pondération pour différents facteurs. J'ai fait de « Lift » la composante principale (40% d'influence) - la pratique a montré qu'il s'agit d'un indicateur vraiment crucial. La « confiance » représente 30% – après tout, la fiabilité de la règle compte aussi beaucoup. Les « support » et « effet de levier » ont vu leur poids réduit ; ils agissent davantage comme des filtres.

Le tri des signaux s'est avéré être une toute autre histoire. Au début, j'ai essayé d'appliquer toutes les règles à la lettre, mais j'ai vite compris mon erreur. J'ai donc dû mettre en place un système de tri à plusieurs niveaux. Tout d'abord, nous éliminons les règles faibles en fonction du seuil de force minimal. Ensuite, nous vérifions si le signal est confirmé par plusieurs règles – les règles isolées sont généralement moins fiables.

La prise en compte de la volatilité s'est avérée particulièrement importante. Durant les périodes calmes, le système fonctionnait parfaitement, mais dès que la volatilité augmentait, le nombre de faux signaux grimpait en flèche. J'ai dû ajouter des filtres dynamiques qui deviennent plus stricts à mesure que la volatilité augmente.

Les tests du système ont duré près de 3 mois. Je l'ai exécuté sur un historique de 2 ans pour 4 paires majeures. Les résultats ont été tout à fait inattendus. Par exemple, la paire USDJPY a affiché les meilleures performances : 65% de transactions rentables avec un RR de 1,6. Mais la paire GBPUSD a été décevante - seulement 58% avec un RR de 1,4.

Il est intéressant de noter que les règles avec un « lift » supérieur à 2,0 et une « confiance » supérieure à 0,8 ont systématiquement donné les meilleurs résultats pour toutes les paires. Apparemment, ces niveaux constituent réellement des seuils de signification naturelle sur le marché des changes.


Améliorations supplémentaires

Actuellement, je vois plusieurs pistes d'amélioration du système. Premièrement, les paramètres des règles doivent être rendus plus dynamiques : le marché évolue et le système doit s'adapter. Deuxièmement, on constate un manque évident de prise en compte de la macroéconomie et du contexte de l'actualité. Oui, cela compliquera le système, mais les gains potentiels en valent la peine.

L'application de filtres adaptatifs semble particulièrement intéressante. Les différentes phases du marché nécessitent clairement des paramètres système différents. Elle est implémentée de manière rudimentaire pour le moment, mais je vois déjà plusieurs façons de l'améliorer.

La semaine prochaine, je prévois de commencer à tester une nouvelle version avec une optimisation dynamique des tailles de position. Les premiers résultats basés sur les données historiques sont prometteurs, mais le marché réel, comme toujours, procédera à ses propres ajustements.


Conclusion

L'utilisation de règles d'association dans le trading algorithmique ouvre des perspectives intéressantes pour la découverte de schémas de marché non évidents. La clé du succès réside ici dans une préparation adéquate des données, une sélection rigoureuse des règles et un système de génération de signaux bien conçu.

Il est important de rappeler que tout système de trading nécessite une surveillance constante et une adaptation aux conditions changeantes du marché. Les règles associatives sont un outil d'analyse puissant, mais elles doivent être utilisées conjointement avec d'autres méthodes d'analyse techniques et fondamentales.

Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/16061

Fichiers joints |
Dataset.mq5 (4.29 KB)
Derniers commentaires | Aller à la discussion (2)
Aleksey Vyazmikin
Aleksey Vyazmikin | 22 nov. 2024 à 18:27

Apparemment, on suppose que le lecteur doit déjà avoir une certaine connaissance de cette méthode, et si ce n'est pas le cas ?

Je ne comprends pas les mesures mentionnées, en particulier :

Lift est devenu mon indicateur préféré. Après des centaines d'heures de tests, j'ai remarqué une tendance : les règles dont le lift est supérieur à 1,5 fonctionnent vraiment sur le marché réel. Cette découverte a sérieusement influencé mon approche du filtrage des signaux.

Si j'ai bien compris la méthode, des signaux corrélés sont recherchés dans les segments quantiques. Mais je ne comprenais pas l'étape suivante. Quel est le segment cible ? Je suppose que les règles résultantes sont comparées à la cible et évaluées par rapport aux mesures.

Si c'est le cas, cela fait écho à ma méthode, et il est intéressant d'évaluer les performances et l'efficacité.

Cks1295
Cks1295 | 24 nov. 2024 à 05:28
Bonjour, Eugène ! S'il vous plaît écrivez-moi (je vous ai envoyé une demande d'ajout en tant qu'ami, il y a un sujet de conversation sérieux (les modèles de promoteurs et leur application pratique). Je vous remercie de votre réponse et vous prie d'agréer, Andrey, l'expression de mes salutations distinguées.
Comment Échanger des Données : Une DLL pour MQL5 en 10 minutes Comment Échanger des Données : Une DLL pour MQL5 en 10 minutes
Maintenant, peu de développeurs se rappellent de la façon d'écrire une DLL simple et des caractéristiques spéciales des différentes liaisons système. À l'aide de plusieurs exemples, je vais tenter de montrer l'ensemble du processus de création de la DLL simple en 10 minutes, ainsi que de discuter de certains détails techniques de notre implémentation de liaison. Je vais montrer étape par étape le processus de la création de DLL dans Visual Studio avec des exemples d'échange de différents types de variables (nombres, tableaux, chaînes, etc.). En outre, je vais vous expliquer comment protéger votre terminal client des plantages dans les DLL personnalisées.
L'analyse des réseaux neuronaux volumétriques comme clé des tendances futures L'analyse des réseaux neuronaux volumétriques comme clé des tendances futures
Cet article explore la possibilité d'améliorer les prévisions de prix basées sur l'analyse des volumes de transactions en intégrant les principes de l'analyse technique à l'architecture du réseau neuronal LSTM. Une attention particulière est portée à la détection et à l'interprétation des volumes anormaux, à l'utilisation du clustering et à la création de caractéristiques basées sur les volumes et leur définition dans le contexte de l'apprentissage automatique.
L'Histogramme des prix (Profile du Marché) et son implémentation  en MQL5 L'Histogramme des prix (Profile du Marché) et son implémentation en MQL5
Le Profile du Marché a été élaboré par le brillant penseur Peter Steidlmayer. Il a suggéré l’utilisation de la représentation alternative de l'information sur les mouvements de marché « horizontaux » et « verticaux » qui conduit à un ensemble de modèles complètement différent. Il a assumé qu'il existe une impulsion sous-jacente du marché ou un modèle fondamental appelé cycle d'équilibre et de déséquilibre. Dans cet article, j’examinerai l'Histogramme des Prix - un modèle simplifié de profil de marché, et décrirai son implémentation dans MQL5.
Analyse de l'impact des conditions météorologiques sur les devises des pays agricoles à l'aide de Python Analyse de l'impact des conditions météorologiques sur les devises des pays agricoles à l'aide de Python
Quel est le lien entre la météo et le Forex ? La théorie économique classique a longtemps ignoré l'influence de facteurs tels que les conditions météorologiques sur le comportement du marché. Mais tout a changé. Essayons de trouver des liens entre les conditions météorologiques et la position des devises agricoles sur le marché.