English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
preview
Création de barres 3D basées sur le temps, le prix et le volume

Création de barres 3D basées sur le temps, le prix et le volume

MetaTrader 5Intégration |
167 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introduction

Cela fait six mois que j'ai commencé ce projet. Six mois après l'idée, qui me paraissait stupide, je n'y suis pas beaucoup revenu, me contentant d'en discuter avec des traders que je connaissais bien. 

Tout a commencé par une question simple : pourquoi les traders s'obstinent-ils à analyser un marché 3D en se basant sur des graphiques 2D ? Analyse de l'évolution des prix, analyse technique, théorie des vagues – tout cela repose sur la projection du marché sur un plan. Mais que se passerait-il si nous essayions de voir la véritable structure des prix, des volumes et du temps ?

Dans mes travaux sur les systèmes algorithmiques, j'ai constamment constaté que les indicateurs traditionnels ne parviennent pas à saisir les relations essentielles entre le prix et le volume.

L'idée des barres en 3D n'est pas venue immédiatement. Dans un premier temps, une expérience de visualisation 3D de la profondeur du marché a été menée. Puis apparurent les premiers schémas de regroupements volume-prix. Et lorsque j'ai ajouté la composante temporelle et construit la première barre 3D, il est devenu évident qu'il s'agissait d'une façon fondamentalement nouvelle d'appréhender le marché.

Aujourd'hui, je souhaite partager avec vous les résultats de ce travail. Je vais vous montrer comment Python et MetaTrader 5 vous permettent de construire des barres de volume en temps réel. Je parlerai des mathématiques qui sous-tendent ces calculs et de la manière d'utiliser ces informations dans la pratique du trading.


Qu'est-ce qui différencie une barre 3D ?

Tant que nous analyserons le marché à travers le prisme de graphiques bidimensionnels, nous passerons à côté de l'essentiel : sa structure réelle. L'analyse technique traditionnelle fonctionne avec des projections prix-temps et volume-temps, mais ne montre jamais l'ensemble de l'interaction de ces composantes.

L'analyse 3D est fondamentalement différente en ce qu'elle nous permet de voir le marché dans son ensemble. Lorsque nous construisons une barre de volume, nous créons littéralement un « instantané » de l'état du marché, où chaque dimension véhicule des informations cruciales :

  • La hauteur de la barre indique l'amplitude du mouvement de prix.
  • la largeur reflète l'échelle de temps
  • la profondeur visualise la distribution du volume

Pourquoi est-ce important ? Imaginez deux mouvements de prix identiques sur un graphique. En deux dimensions, elles semblent identiques. Mais lorsque l'on ajoute la composante volume, le tableau change radicalement : un mouvement peut être soutenu par un volume massif, formant une barre profonde et stable, tandis qu'un autre s'avère n'être qu'une éclaboussure superficielle avec un soutien minimal pour les transactions réelles.

Une approche intégrée utilisant des barres 3D résout un problème classique de l'analyse technique : le décalage du signal. La structure volumétrique de la barre commence à se former dès les premières impulsions, ce qui nous permet de voir l'émergence d'un mouvement important bien avant qu'il n'apparaisse sur un graphique classique. En résumé, nous obtenons un outil d'analyse prédictive basé non pas sur des tendances historiques, mais sur la dynamique réelle des transactions actuelles.

L'analyse de données multivariées est bien plus qu'une simple visualisation esthétique ; c'est une manière fondamentalement nouvelle de comprendre la microstructure du marché. Chaque barre 3D contient des informations sur :

  • la répartition des volumes dans la fourchette de prix
  • la vitesse d'accumulation des positions
  • les déséquilibres entre acheteurs et vendeurs
  • la volatilité au niveau microéconomique
  • l’élan du mouvement (le momentum)

Tous ces éléments fonctionnent comme un seul mécanisme, vous permettant de percevoir la véritable nature des fluctuations de prix. Là où l'analyse technique classique ne voit qu'une bougie ou une barre, l'analyse 3D révèle la structure complexe de l'interaction entre l'offre et la demande.


Équations pour le calcul des principales métriques. Principes de base de la construction des barres 3D. La logique consistant à combiner différentes dimensions en un seul système

Le modèle mathématique des barres 3D est né de l'analyse de la microstructure réelle du marché. Chaque barre du système peut être représentée par une figure tridimensionnelle :

class Bar3D:
    def __init__(self):
        self.price_range = None  # Price range
        self.time_period = None  # Time interval
        self.volume_profile = {} # Volume profile by prices
        self.direction = None    # Movement direction
        self.momentum = None     # Impulse
        self.volatility = None   # Volatility
        self.spread = None       # Average spread

Le point clé est le calcul du profil volumétrique à l'intérieur de la barre. Contrairement aux barres classiques, nous analysons la répartition des volumes par niveaux de prix.

def calculate_volume_profile(self, ticks_data):
    volume_by_price = defaultdict(float)
    
    for tick in ticks_data:
        price_level = round(tick.price, 5)
        volume_by_price[price_level] += tick.volume
        
    # Normalize the profile
    total_volume = sum(volume_by_price.values())
    for price in volume_by_price:
        volume_by_price[price] /= total_volume
        
    return volume_by_price

La dynamique est calculée comme une combinaison du taux de variation du prix et du volume :

def calculate_momentum(self):
    price_velocity = (self.close - self.open) / self.time_period
    volume_intensity = self.total_volume / self.time_period
    self.momentum = price_velocity * volume_intensity * self.direction

Une attention particulière est portée à l'analyse de la volatilité intra-barre. Nous utilisons une équation modifiée pour l’ATR qui prend en compte la microstructure du mouvement :

def calculate_volatility(self, tick_data):
    tick_changes = np.diff([tick.price for tick in tick_data])
    weighted_std = np.std(tick_changes * [tick.volume for tick in tick_data[1:]])
    time_factor = np.sqrt(self.time_period)
    self.volatility = weighted_std * time_factor

La différence fondamentale avec les barres classiques réside dans le fait que toutes les mesures sont calculées en temps réel, ce qui nous permet de voir la formation de la structure de la barre :

def update_bar(self, new_tick):
    self.update_price_range(new_tick.price)
    self.update_volume_profile(new_tick)
    self.recalculate_momentum()
    self.update_volatility(new_tick)
    
    # Recalculate the volumetric center of gravity
    self.volume_poc = self.calculate_poc()

Toutes les mesures sont combinées grâce à un système de facteurs de pondération adaptés à chaque instrument :

def calculate_bar_strength(self):
    return (self.momentum_weight * self.normalized_momentum +
            self.volatility_weight * self.normalized_volatility +
            self.volume_weight * self.normalized_volume_concentration +
            self.spread_weight * self.normalized_spread_factor)

En pratique, ce modèle mathématique nous permet d'observer des aspects du marché tels que :

  • des déséquilibres dans l'accumulation de volume
  • des anomalies dans la vitesse de formation des prix
  • des zones de consolidation et de rupture
  • la véritable force d'une tendance à travers ses caractéristiques de volume

Chaque barre 3D devient non seulement un point sur le graphique, mais un indicateur à part entière de l'état du marché à un moment précis.


Analyse détaillée de l'algorithme de création de barres 3D. Caractéristiques de l'utilisation de MetaTrader 5. Spécificités du traitement des données

Après avoir débogué l'algorithme principal, je suis enfin arrivé à la partie la plus intéressante : l'implémentation de barres multidimensionnelles en temps réel. J'avoue qu'au départ, cela m'a paru une tâche ardue. MetaTrader 5 n'est pas particulièrement compatible avec les scripts externes, et la documentation ne permet parfois pas une compréhension adéquate. Mais laissez-moi vous expliquer comment j'ai finalement réussi à surmonter cela.

J'ai commencé par une structure de base pour le stockage des données. Après plusieurs itérations, la classe suivante a vu le jour :

class Bar7D:
    def __init__(self):
        self.time = None
        self.open = None
        self.high = None
        self.low = None
        self.close = None
        self.tick_volume = 0
        self.volume_profile = {}
        self.direction = 0
        self.trend_count = 0
        self.volatility = 0
        self.momentum = 0

Le plus difficile a été de trouver comment calculer correctement la taille du bloc. Après de nombreuses expérimentations, j'ai opté pour cette équation :

def calculate_brick_size(symbol_info, multiplier=45):
    spread = symbol_info.spread
    point = symbol_info.point
    min_price_brick = spread * multiplier * point
    
    # Adaptive adjustment for volatility
    atr = calculate_atr(symbol_info.name)
    if atr > min_price_brick * 2:
        min_price_brick = atr / 2
        
    return min_price_brick

J'ai également eu beaucoup de problèmes avec les volumes. Au départ, je voulais utiliser un volume_brick de taille fixe, mais je me suis vite rendu compte que cela ne fonctionnait pas. La solution s'est présentée sous la forme d'un algorithme adaptatif :

def adaptive_volume_threshold(tick_volume, history_volumes):
    median_volume = np.median(history_volumes)
    std_volume = np.std(history_volumes)
    
    if tick_volume > median_volume + 2 * std_volume:
        return median_volume + std_volume
    return max(tick_volume, median_volume / 2)

Mais je crois que j'ai un peu exagéré avec le calcul des indicateurs statistiques :

def calculate_stats(df):
    df['ma_5'] = df['close'].rolling(5).mean()
    df['ma_20'] = df['close'].rolling(20).mean()
    df['volume_ma_5'] = df['tick_volume'].rolling(5).mean()
    df['price_volatility'] = df['price_change'].rolling(10).std()
    df['volume_volatility'] = df['tick_volume'].rolling(10).std()
    df['trend_strength'] = df['trend_count'] * df['direction']
    
    # This is probably too much
    df['zscore_price'] = stats.zscore(df['close'], nan_policy='omit')
    df['zscore_volume'] = stats.zscore(df['tick_volume'], nan_policy='omit')
    return df

C'est drôle, mais le plus difficile n'était pas d'écrire le code, mais de le déboguer en conditions réelles. 

Voici le résultat final de la fonction avec normalisation dans la plage 3-9. Pourquoi 3-9 ? Gann et Tesla affirmaient tous deux qu'il y avait une sorte de magie cachée dans ces chiffres. J'ai également personnellement vu un trader sur une plateforme bien connue qui aurait créé un script de retournement de situation performant basé sur ces chiffres. Mais n'entamons pas de débats sur les théories du complot et le mysticisme. Essayez plutôt ceci :

def create_true_3d_renko(symbol, timeframe, min_spread_multiplier=45, volume_brick=500, lookback=20000):
    """
    Creates 3D Renko bars with extended analytics
    """
    rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, lookback)
    if rates is None:
        print(f"Error getting data for {symbol}")
        return None, None
        
    df = pd.DataFrame(rates)
    df['time'] = pd.to_datetime(df['time'], unit='s')
    
    if df.isnull().any().any():
        print("Missing values detected, cleaning...")
        df = df.dropna()
        if len(df) == 0:
            print("No data for analysis after cleaning")
            return None, None
    
    symbol_info = mt5.symbol_info(symbol)
    if symbol_info is None:
        print(f"Failed to get symbol info for {symbol}")
        return None, None
    
    try:
        min_price_brick = symbol_info.spread * min_spread_multiplier * symbol_info.point
        if min_price_brick <= 0:
            print("Invalid block size")
            return None, None
    except AttributeError as e:
        print(f"Error getting symbol parameters: {e}")
        return None, None
    
    # Convert time to numeric and scale everything
    scaler = MinMaxScaler(feature_range=(3, 9))
    
    # Convert datetime to numeric (seconds from start)
    df['time_numeric'] = (df['time'] - df['time'].min()).dt.total_seconds()
    
    # Scale all numeric data together
    columns_to_scale = ['time_numeric', 'open', 'high', 'low', 'close', 'tick_volume']
    df[columns_to_scale] = scaler.fit_transform(df[columns_to_scale])
    
    renko_blocks = []
    current_price = float(df.iloc[0]['close'])
    current_tick_volume = 0
    current_time = df.iloc[0]['time']
    current_time_numeric = float(df.iloc[0]['time_numeric'])
    current_spread = float(symbol_info.spread)
    current_type = 0
    prev_direction = 0
    trend_count = 0
    
    try:
        for idx, row in df.iterrows():
            if pd.isna(row['tick_volume']) or pd.isna(row['close']):
                continue
                
            current_tick_volume += float(row['tick_volume'])
            volume_bricks = int(current_tick_volume / volume_brick)
            
            price_diff = float(row['close']) - current_price
            if pd.isna(price_diff) or pd.isna(min_price_brick):
                continue
                
            price_bricks = int(price_diff / min_price_brick)
            
            if volume_bricks > 0 or abs(price_bricks) > 0:
                direction = np.sign(price_bricks) if price_bricks != 0 else 1
                
                if direction == prev_direction:
                    trend_count += 1
                else:
                    trend_count = 1
                
                renko_block = {
                    'time': current_time,
                    'time_numeric': float(row['time_numeric']),
                    'open': float(row['open']),
                    'close': float(row['close']),
                    'high': float(row['high']),
                    'low': float(row['low']),
                    'tick_volume': float(row['tick_volume']),
                    'direction': float(direction),
                    'spread': float(current_spread),
                    'type': float(current_type),
                    'trend_count': trend_count,
                    'price_change': price_diff,
                    'volume_intensity': float(row['tick_volume']) / volume_brick,
                    'price_velocity': price_diff / (volume_bricks if volume_bricks > 0 else 1)
                }
                
                if volume_bricks > 0:
                    current_tick_volume = current_tick_volume % volume_brick
                if price_bricks != 0:
                    current_price += min_price_brick * price_bricks
                    
                prev_direction = direction
                renko_blocks.append(renko_block)
                
    except Exception as e:
        print(f"Error processing data: {e}")
        if len(renko_blocks) == 0:
            return None, None
    
    if len(renko_blocks) == 0:
        print("Failed to create any blocks")
        return None, None
        
    result_df = pd.DataFrame(renko_blocks)
    
    # Scale derived metrics to same range
    derived_metrics = ['price_change', 'volume_intensity', 'price_velocity', 'spread']
    result_df[derived_metrics] = scaler.fit_transform(result_df[derived_metrics])
    
    # Add analytical metrics using scaled data
    result_df['ma_5'] = result_df['close'].rolling(5).mean()
    result_df['ma_20'] = result_df['close'].rolling(20).mean()
    result_df['volume_ma_5'] = result_df['tick_volume'].rolling(5).mean()
    result_df['price_volatility'] = result_df['price_change'].rolling(10).std()
    result_df['volume_volatility'] = result_df['tick_volume'].rolling(10).std()
    result_df['trend_strength'] = result_df['trend_count'] * result_df['direction']
    
    # Scale moving averages and volatility
    ma_columns = ['ma_5', 'ma_20', 'volume_ma_5', 'price_volatility', 'volume_volatility', 'trend_strength']
    result_df[ma_columns] = scaler.fit_transform(result_df[ma_columns])
    
    # Add statistical metrics and scale them
    result_df['zscore_price'] = stats.zscore(result_df['close'], nan_policy='omit')
    result_df['zscore_volume'] = stats.zscore(result_df['tick_volume'], nan_policy='omit')
    zscore_columns = ['zscore_price', 'zscore_volume']
    result_df[zscore_columns] = scaler.fit_transform(result_df[zscore_columns])
    
    return result_df, min_price_brick
Voici à quoi ressemble la série de barres que nous avons obtenue sur une seule échelle. Pas très statique, n'est-ce pas ?

Distributions statistiques :

 

Naturellement, une telle série ne me satisfaisait pas, car mon objectif était de créer une série plus ou moins stationnaire – une série stationnaire, temps-volume-prix. Voici ce que j'ai fait ensuite :


Introduction de la mesure de la volatilité 

Lors de la mise en œuvre de la fonction create_stationary_4d_features, j'ai emprunté une voie fondamentalement différente. Contrairement aux barres 3D originales où nous nous contentions de mettre les données à l'échelle dans la plage 3-9, je me suis ici concentré sur la création de séries véritablement stationnaires.

L'idée principale de cette fonction est de créer une représentation quadridimensionnelle du marché à travers des éléments stationnaires. Au lieu d'une simple mise à l'échelle, chaque dimension est transformée d'une manière spéciale pour atteindre la stationnarité :

  1. Dimension temporelle : J'ai appliqué ici la transformation trigonométrique, convertissant les heures en ondes sinusoïdales et cosinusoïdales. Les équations sin(2π * heure/24) et cos(2π * heure/24) créent des caractéristiques cycliques, éliminant complètement le problème de la saisonnalité quotidienne.
  2. Mesure des prix : au lieu des valeurs absolues des prix, on utilise leurs variations relatives. Dans le code, cela est implémenté en calculant le prix typique (haut + bas + clôture)/3 puis en calculant les rendements et leur accélération. Cette approche rend la série stationnaire quel que soit le niveau de prix.
  3. Mesure volumétrique : voici un point intéressant – nous ne prenons pas seulement en compte les variations de volume, mais aussi leurs incréments relatifs. C'est important car les volumes sont souvent très inégalement répartis. Dans le code, cela est implémenté par l'application successive de pct_change() et diff().
  4. Mesure de la volatilité : J'ai ici mis en œuvre une transformation en deux étapes : d'abord, calculer la volatilité courante à travers l'écart type des rendements, puis prendre en compte les variations relatives de cette volatilité. En réalité, on obtient une « volatilité de la volatilité ».

Chaque bloc de données est formé dans une fenêtre glissante de 20 périodes. Il ne s'agit pas d'un nombre aléatoire ; il est choisi comme un compromis entre la préservation de la structure locale des données et la garantie de la signification statistique des calculs.

Toutes les caractéristiques calculées sont finalement mises à l'échelle dans la plage 3-9, mais il s'agit déjà d'une transformation secondaire appliquée à des séries déjà stationnaires. Cela nous permet de maintenir la compatibilité avec l'implémentation originale des barres 3D tout en utilisant une approche fondamentalement différente du prétraitement des données.

Il est particulièrement important de préserver toutes les métriques clés de la fonction d'origine : moyennes mobiles, volatilité, scores z. Cela permet d'utiliser la nouvelle implémentation en remplacement direct de la fonction originale, tout en obtenant des données stationnaires de meilleure qualité.

Par conséquent, nous obtenons un ensemble de caractéristiques qui sont non seulement stationnaires au sens statistique, mais qui conservent également toutes les informations importantes concernant la structure du marché. Cette approche rend les données beaucoup plus adaptées à l'application de méthodes d'apprentissage automatique et d'analyse statistique, tout en conservant leur lien avec le contexte de trading d'origine.

Voici la fonction : 

def create_true_3d_renko(symbol, timeframe, min_spread_multiplier=45, volume_brick=500, lookback=20000):
    """
    Creates 4D stationary features with same interface as 3D Renko
    """
    rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, lookback)
    if rates is None:
        print(f"Error getting data for {symbol}")
        return None, None
        
    df = pd.DataFrame(rates)
    df['time'] = pd.to_datetime(df['time'], unit='s')
    
    if df.isnull().any().any():
        print("Missing values detected, cleaning...")
        df = df.dropna()
        if len(df) == 0:
            print("No data for analysis after cleaning")
            return None, None
    
    symbol_info = mt5.symbol_info(symbol)
    if symbol_info is None:
        print(f"Failed to get symbol info for {symbol}")
        return None, None
    
    try:
        min_price_brick = symbol_info.spread * min_spread_multiplier * symbol_info.point
        if min_price_brick <= 0:
            print("Invalid block size")
            return None, None
    except AttributeError as e:
        print(f"Error getting symbol parameters: {e}")
        return None, None
    
    scaler = MinMaxScaler(feature_range=(3, 9))
    df_blocks = []
    
    try:
        # Time dimension
        df['time_sin'] = np.sin(2 * np.pi * df['time'].dt.hour / 24)
        df['time_cos'] = np.cos(2 * np.pi * df['time'].dt.hour / 24)
        df['time_numeric'] = (df['time'] - df['time'].min()).dt.total_seconds()
        
        # Price dimension
        df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3
        df['price_return'] = df['typical_price'].pct_change()
        df['price_acceleration'] = df['price_return'].diff()
        
        # Volume dimension
        df['volume_change'] = df['tick_volume'].pct_change()
        df['volume_acceleration'] = df['volume_change'].diff()
        
        # Volatility dimension
        df['volatility'] = df['price_return'].rolling(20).std()
        df['volatility_change'] = df['volatility'].pct_change()
        
        for idx in range(20, len(df)):
            window = df.iloc[idx-20:idx+1]
            
            block = {
                'time': df.iloc[idx]['time'],
                'time_numeric': scaler.fit_transform([[float(df.iloc[idx]['time_numeric'])]]).item(),
                'open': float(window['price_return'].iloc[-1]),
                'high': float(window['price_acceleration'].iloc[-1]),
                'low': float(window['volume_change'].iloc[-1]),
                'close': float(window['volatility_change'].iloc[-1]),
                'tick_volume': float(window['volume_acceleration'].iloc[-1]),
                'direction': np.sign(window['price_return'].iloc[-1]),
                'spread': float(df.iloc[idx]['time_sin']),
                'type': float(df.iloc[idx]['time_cos']),
                'trend_count': len(window),
                'price_change': float(window['price_return'].mean()),
                'volume_intensity': float(window['volume_change'].mean()),
                'price_velocity': float(window['price_acceleration'].mean())
            }
            df_blocks.append(block)
                
    except Exception as e:
        print(f"Error processing data: {e}")
        if len(df_blocks) == 0:
            return None, None
    
    if len(df_blocks) == 0:
        print("Failed to create any blocks")
        return None, None
        
    result_df = pd.DataFrame(df_blocks)
    
    # Scale all features
    features_to_scale = [col for col in result_df.columns if col != 'time' and col != 'direction']
    result_df[features_to_scale] = scaler.fit_transform(result_df[features_to_scale])
    
    # Add same analytical metrics as in original function
    result_df['ma_5'] = result_df['close'].rolling(5).mean()
    result_df['ma_20'] = result_df['close'].rolling(20).mean()
    result_df['volume_ma_5'] = result_df['tick_volume'].rolling(5).mean()
    result_df['price_volatility'] = result_df['price_change'].rolling(10).std()
    result_df['volume_volatility'] = result_df['tick_volume'].rolling(10).std()
    result_df['trend_strength'] = result_df['trend_count'] * result_df['direction']
    
    # Scale moving averages and volatility
    ma_columns = ['ma_5', 'ma_20', 'volume_ma_5', 'price_volatility', 'volume_volatility', 'trend_strength']
    result_df[ma_columns] = scaler.fit_transform(result_df[ma_columns])
    
    # Add statistical metrics and scale them
    result_df['zscore_price'] = stats.zscore(result_df['close'], nan_policy='omit')
    result_df['zscore_volume'] = stats.zscore(result_df['tick_volume'], nan_policy='omit')
    zscore_columns = ['zscore_price', 'zscore_volume']
    result_df[zscore_columns] = scaler.fit_transform(result_df[zscore_columns])
    
    return result_df, min_price_brick

Voici à quoi cela ressemble en 2D :

Ensuite, essayons de créer un modèle 3D interactif pour les prix en 3D à l'aide de plotly. Un graphique bidimensionnel classique devrait être visible à proximité. Voici le code :

import plotly.graph_objects as go
from plotly.subplots import make_subplots


def create_interactive_3d(df, symbol, save_dir):
    """
    Creates interactive 3D visualization with smoothed data and original price chart
    """
    try:
        save_dir = Path(save_dir)
        
        # Smooth all series with MA(100)
        df_smooth = df.copy()
        smooth_columns = ['close', 'tick_volume', 'price_volatility', 'volume_volatility']
        
        for col in smooth_columns:
            df_smooth[f'{col}_smooth'] = df_smooth[col].rolling(window=100, min_periods=1).mean()
        
        # Create subplots: 3D view and original chart side by side
        fig = make_subplots(
            rows=1, cols=2,
            specs=[[{'type': 'scene'}, {'type': 'xy'}]],
            subplot_titles=(f'{symbol} 3D View (MA100)', f'{symbol} Original Price'),
            horizontal_spacing=0.05
        )
        
        # Add 3D scatter plot
        fig.add_trace(
            go.Scatter3d(
                x=np.arange(len(df_smooth)),
                y=df_smooth['tick_volume_smooth'],
                z=df_smooth['close_smooth'],
                mode='markers',
                marker=dict(
                    size=5,
                    color=df_smooth['price_volatility_smooth'],
                    colorscale='Viridis',
                    opacity=0.8,
                    showscale=True,
                    colorbar=dict(x=0.45)
                ),
                hovertemplate=
                "Time: %{x}<br>" +
                "Volume: %{y:.2f}<br>" +
                "Price: %{z:.5f}<br>" +
                "Volatility: %{marker.color:.5f}",
                name='3D View'
            ),
            row=1, col=1
        )
        
        # Add original price chart
        fig.add_trace(
            go.Candlestick(
                x=np.arange(len(df)),
                open=df['open'],
                high=df['high'],
                low=df['low'],
                close=df['close'],
                name='OHLC'
            ),
            row=1, col=2
        )
        
        # Add smoothed price line
        fig.add_trace(
            go.Scatter(
                x=np.arange(len(df_smooth)),
                y=df_smooth['close_smooth'],
                line=dict(color='blue', width=1),
                name='MA100'
            ),
            row=1, col=2
        )
        
        # Update 3D layout
        fig.update_scenes(
            xaxis_title='Time',
            yaxis_title='Volume',
            zaxis_title='Price',
            camera=dict(
                up=dict(x=0, y=0, z=1),
                center=dict(x=0, y=0, z=0),
                eye=dict(x=1.5, y=1.5, z=1.5)
            )
        )
        
        # Update 2D layout
        fig.update_xaxes(title_text="Time", row=1, col=2)
        fig.update_yaxes(title_text="Price", row=1, col=2)
        
        # Update overall layout
        fig.update_layout(
            width=1500,  # Double width to accommodate both plots
            height=750,
            showlegend=True,
            title_text=f"{symbol} Combined Analysis"
        )
        
        # Save interactive HTML
        fig.write_html(save_dir / f'{symbol}_combined_view.html')
        
        # Create additional plots with smoothed data (unchanged)
        fig2 = make_subplots(rows=2, cols=2, 
                            subplot_titles=('Smoothed Price', 'Smoothed Volume',
                                          'Smoothed Price Volatility', 'Smoothed Volume Volatility'))
        
        fig2.add_trace(
            go.Scatter(x=np.arange(len(df_smooth)), y=df_smooth['close_smooth'],
                      name='Price MA100'),
            row=1, col=1
        )
        
        fig2.add_trace(
            go.Scatter(x=np.arange(len(df_smooth)), y=df_smooth['tick_volume_smooth'],
                      name='Volume MA100'),
            row=1, col=2
        )
        
        fig2.add_trace(
            go.Scatter(x=np.arange(len(df_smooth)), y=df_smooth['price_volatility_smooth'],
                      name='Price Vol MA100'),
            row=2, col=1
        )
        
        fig2.add_trace(
            go.Scatter(x=np.arange(len(df_smooth)), y=df_smooth['volume_volatility_smooth'],
                      name='Volume Vol MA100'),
            row=2, col=2
        )
        
        fig2.update_layout(
            height=750,
            width=750,
            showlegend=True,
            title_text=f"{symbol} Smoothed Data Analysis"
        )
        
        fig2.write_html(save_dir / f'{symbol}_smoothed_analysis.html')
        
        print(f"Interactive visualizations saved in {save_dir}")
        
    except Exception as e:
        print(f"Error creating interactive visualization: {e}")
        raise

Voici à quoi ressemble notre nouvelle gamme de prix :



Globalement, cela semble très intéressant. Nous observons certaines séquences de regroupement des prix dans le temps, et des valeurs aberrantes dans le regroupement des prix en fonction du volume. Ainsi, se crée le sentiment (directement confirmé par l'expérience des principaux traders) que lorsque le marché est agité, lorsque d'énormes volumes sont injectés, lorsque la volatilité s'emballe, nous sommes confrontés à une explosion dangereuse qui dépasse les statistiques – les fameux risques extrêmes. Par conséquent, nous pouvons immédiatement détecter ici une telle sortie « anormale » du prix à ces coordonnées. Je dois cela à l'idée des graphiques de prix multivariés ! 

Veuillez noter:

Examen du patient (graphiques 3D)

Ensuite, je suggère de visualiser. Pas notre avenir radieux sous un palmier, mais des graphiques de prix en 3D. Décomposons les situations en 4 groupes : tendance haussière, tendance baissière, retournement de tendance haussière à tendance baissière et retournement de tendance baissière à tendance haussière. Pour ce faire, nous devrons légèrement modifier le code : nous n’aurons plus besoin des indices des barres, nous chargerons les données à des dates spécifiques. En fait, pour faire cela, il suffit d'aller dans mt5.copy_rates_range.

def create_true_3d_renko(symbol, timeframe, start_date, end_date, min_spread_multiplier=45, volume_brick=500):
    """
    Creates 4D stationary features with same interface as 3D Renko
    """
    rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
    if rates is None:
        print(f"Error getting data for {symbol}")
        return None, None
        
    df = pd.DataFrame(rates)
    df['time'] = pd.to_datetime(df['time'], unit='s')
    
    if df.isnull().any().any():
        print("Missing values detected, cleaning...")
        df = df.dropna()
        if len(df) == 0:
            print("No data for analysis after cleaning")
            return None, None
    
    symbol_info = mt5.symbol_info(symbol)
    if symbol_info is None:
        print(f"Failed to get symbol info for {symbol}")
        return None, None
    
    try:
        min_price_brick = symbol_info.spread * min_spread_multiplier * symbol_info.point
        if min_price_brick <= 0:
            print("Invalid block size")
            return None, None
    except AttributeError as e:
        print(f"Error getting symbol parameters: {e}")
        return None, None
    
    scaler = MinMaxScaler(feature_range=(3, 9))
    df_blocks = []
    
    try:
        # Time dimension
        df['time_sin'] = np.sin(2 * np.pi * df['time'].dt.hour / 24)
        df['time_cos'] = np.cos(2 * np.pi * df['time'].dt.hour / 24)
        df['time_numeric'] = (df['time'] - df['time'].min()).dt.total_seconds()
        
        # Price dimension
        df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3
        df['price_return'] = df['typical_price'].pct_change()
        df['price_acceleration'] = df['price_return'].diff()
        
        # Volume dimension
        df['volume_change'] = df['tick_volume'].pct_change()
        df['volume_acceleration'] = df['volume_change'].diff()
        
        # Volatility dimension
        df['volatility'] = df['price_return'].rolling(20).std()
        df['volatility_change'] = df['volatility'].pct_change()
        
        for idx in range(20, len(df)):
            window = df.iloc[idx-20:idx+1]
            
            block = {
                'time': df.iloc[idx]['time'],
                'time_numeric': scaler.fit_transform([[float(df.iloc[idx]['time_numeric'])]]).item(),
                'open': float(window['price_return'].iloc[-1]),
                'high': float(window['price_acceleration'].iloc[-1]),
                'low': float(window['volume_change'].iloc[-1]),
                'close': float(window['volatility_change'].iloc[-1]),
                'tick_volume': float(window['volume_acceleration'].iloc[-1]),
                'direction': np.sign(window['price_return'].iloc[-1]),
                'spread': float(df.iloc[idx]['time_sin']),
                'type': float(df.iloc[idx]['time_cos']),
                'trend_count': len(window),
                'price_change': float(window['price_return'].mean()),
                'volume_intensity': float(window['volume_change'].mean()),
                'price_velocity': float(window['price_acceleration'].mean())
            }
            df_blocks.append(block)
                
    except Exception as e:
        print(f"Error processing data: {e}")
        if len(df_blocks) == 0:
            return None, None
    
    if len(df_blocks) == 0:
        print("Failed to create any blocks")
        return None, None
        
    result_df = pd.DataFrame(df_blocks)
    
    # Scale all features
    features_to_scale = [col for col in result_df.columns if col != 'time' and col != 'direction']
    result_df[features_to_scale] = scaler.fit_transform(result_df[features_to_scale])
    
    # Add same analytical metrics as in original function
    result_df['ma_5'] = result_df['close'].rolling(5).mean()
    result_df['ma_20'] = result_df['close'].rolling(20).mean()
    result_df['volume_ma_5'] = result_df['tick_volume'].rolling(5).mean()
    result_df['price_volatility'] = result_df['price_change'].rolling(10).std()
    result_df['volume_volatility'] = result_df['tick_volume'].rolling(10).std()
    result_df['trend_strength'] = result_df['trend_count'] * result_df['direction']
    
    # Scale moving averages and volatility
    ma_columns = ['ma_5', 'ma_20', 'volume_ma_5', 'price_volatility', 'volume_volatility', 'trend_strength']
    result_df[ma_columns] = scaler.fit_transform(result_df[ma_columns])
    
    # Add statistical metrics and scale them
    result_df['zscore_price'] = stats.zscore(result_df['close'], nan_policy='omit')
    result_df['zscore_volume'] = stats.zscore(result_df['tick_volume'], nan_policy='omit')
    zscore_columns = ['zscore_price', 'zscore_volume']
    result_df[zscore_columns] = scaler.fit_transform(result_df[zscore_columns])
    
    return result_df, min_price_brick

Voici notre code modifié :

def main():
    try:
        # Initialize MT5
        if not mt5.initialize():
            print("MetaTrader5 initialization error")
            return

        # Analysis parameters
        symbols = ["EURUSD", "GBPUSD"]
        timeframes = {
            "M15": mt5.TIMEFRAME_M15
        }
        
        # 7D analysis parameters
        params = {
            "min_spread_multiplier": 45,
            "volume_brick": 500
        }

        # Date range for data fetching
        start_date = datetime(2017, 1, 1)
        end_date = datetime(2018, 2, 1)

        # Analysis for each symbol and timeframe
        for symbol in symbols:
            print(f"\nAnalyzing symbol {symbol}")
            
            # Create symbol directory
            symbol_dir = Path('charts') / symbol
            symbol_dir.mkdir(parents=True, exist_ok=True)
            
            # Get symbol info
            symbol_info = mt5.symbol_info(symbol)
            if symbol_info is None:
                print(f"Failed to get symbol info for {symbol}")
                continue

            print(f"Spread: {symbol_info.spread} points")
            print(f"Tick: {symbol_info.point}")
            
            # Analysis for each timeframe
            for tf_name, tf in timeframes.items():
                print(f"\nAnalyzing timeframe {tf_name}")
                
                # Create timeframe directory
                tf_dir = symbol_dir / tf_name
                tf_dir.mkdir(exist_ok=True)
                
                # Get and analyze data
                print("Getting data...")
                df, brick_size = create_true_3d_renko(
                    symbol=symbol,
                    timeframe=tf,
                    start_date=start_date,
                    end_date=end_date,
                    min_spread_multiplier=params["min_spread_multiplier"],
                    volume_brick=params["volume_brick"]
                )
                
                if df is not None and brick_size is not None:
                    print(f"Created {len(df)} 7D bars")
                    print(f"Block size: {brick_size}")
                    
                    # Basic statistics
                    print("\nBasic statistics:")
                    print(f"Average volume: {df['tick_volume'].mean():.2f}")
                    print(f"Average trend length: {df['trend_count'].mean():.2f}")
                    print(f"Max uptrend length: {df[df['direction'] > 0]['trend_count'].max()}")
                    print(f"Max downtrend length: {df[df['direction'] < 0]['trend_count'].max()}")
                    
                    # Create visualizations
                    print("\nCreating visualizations...")
                    create_visualizations(df, symbol, tf_dir)
                    
                    # Save data
                    csv_file = tf_dir / f"{symbol}_{tf_name}_7d_data.csv"
                    df.to_csv(csv_file)
                    print(f"Data saved to {csv_file}")
                    
                    # Results analysis
                    trend_ratio = len(df[df['direction'] > 0]) / len(df[df['direction'] < 0])
                    print(f"\nUp/Down bars ratio: {trend_ratio:.2f}")
                    
                    volume_corr = df['tick_volume'].corr(df['price_change'].abs())
                    print(f"Volume-Price change correlation: {volume_corr:.2f}")
                    
                    # Print warnings if anomalies detected
                    if df['price_volatility'].max() > df['price_volatility'].mean() * 3:
                        print("\nWARNING: High volatility periods detected!")
                        
                    if df['volume_volatility'].max() > df['volume_volatility'].mean() * 3:
                        print("WARNING: Abnormal volume spikes detected!")
                else:
                    print(f"Failed to create 3D bars for {symbol} on {tf_name}")
        
        print("\nAnalysis completed successfully!")
        
    except Exception as e:
        print(f"An error occurred: {e}")
        import traceback
        print(traceback.format_exc())
    finally:
        mt5.shutdown()

Prenons la première section de données : l’EUR/USD, du 1er janvier 2017 au 1er février 2018. En fait, une tendance haussière très puissante. Prêt à voir à quoi ça ressemble en barres 3D ?

Voici à quoi ressemble une autre visualisation :

Portons une attention particulière au début de la tendance haussière :

Et pour conclure :

Examinons maintenant la tendance à la baisse. Du 1er février 2018 au 20 mars 2020 :

Voici le début de la tendance baissière :

Et voici sa fin :

Nous constatons donc que les deux tendances (à la fois baissière et haussière) dans la représentation 3D ont commencé comme une zone de points en dessous de la densité de points 3D. La fin de cette tendance a été marquée, dans les deux cas, par une palette de couleurs jaune vif. 

Pour décrire ce phénomène et le comportement des cours de l'EUR/USD dans les tendances haussières et baissières, on peut utiliser l'équation universelle suivante :

P(t) = P_0 + \int_{t_0}^{t} A \cdot e^{k(tu)} \cdot V(u) \, du + N(t)

avec :

  •  P(t) — le prix de la devise à un certain moment.
  •  P_0 — le prix initial à un certain moment.
  •  A — l’amplitude de la tendance, qui caractérise l’ampleur des variations de prix.
  •  k — le ratio qui détermine le taux de changement (k > 0 signifie une tendance haussière ; k < 0 signifie une tendance baissière).
  •  V(u) — le volume d'échanges à un moment donné, qui influence l'activité du marché et peut accroître l'importance des variations de prix.
  •  N(t) — le bruit aléatoire qui reflète les fluctuations imprévisibles du marché.

Explication du texte

Cette équation décrit comment le prix d'une devise évolue au fil du temps, en fonction de plusieurs facteurs. Le prix initial est le point de départ, après quoi l'intégrale prend en compte l'influence de l'amplitude de la tendance et de son taux de variation, soumettant le prix à une croissance ou à une baisse exponentielle en fonction de son ampleur. Le volume des échanges représenté par cette fonction ajoute une autre dimension, montrant que l'activité du marché influence également les variations de prix.

Ce modèle permet ainsi de visualiser les mouvements de prix selon différentes tendances, en les affichant dans un espace 3D, où l'axe temporel, le prix et le volume créent une image riche de l'activité du marché. L'intensité des couleurs dans ce modèle peut indiquer la force de la tendance, les couleurs plus vives correspondant à des prix et des volumes de transactions plus élevés sur les produits dérivés, signalant ainsi de forts mouvements de volume sur le marché.


Affichage de l'inversion

Voici la période du 14 novembre au 28 novembre. Nous observerons un renversement de tendance des cotations approximativement au milieu de cette période. À quoi cela ressemble-t-il en coordonnées 3D ? Voici le résultat :

On observe la couleur jaune déjà familière au moment de l'inversion et de la hausse de la coordonnée de prix normalisée. Examinons maintenant une autre section de prix présentant un renversement de tendance, du 13 septembre 2024 au 10 octobre de la même année :

On retrouve la même image, sauf que la couleur jaune et son accumulation se trouvent maintenant en bas. Ça a l'air intéressant.  

Du 19 août 2024 au 30 août 2024, un renversement exact de la tendance est observable au milieu de cette période. Examinons nos coordonnées.

Encore une fois, exactement la même image. Prenons comme référence la période allant du 17 juillet 2024 au 8 août 2024. Le modèle montrera-t-il bientôt des signes d'inversion ?

La dernière période s'étend du 21 avril au 10 août 2023. La tendance haussière s'est arrêtée là.

On retrouve la couleur jaune familière.


Grappes jaunes

Lors du développement de barres 3D, je suis tombé sur une caractéristique très intéressante : les grappes jaunes de volatilité du volume. J'étais captivé par leur comportement sur le graphique ! Après avoir analysé une tonne de données historiques (plus de 400 000 barres pour 2022-2024, pour être précis), j'ai remarqué quelque chose de surprenant.

Au début, je n'en croyais pas mes yeux : sur environ 100 000 barres jaunes, presque toutes (97% !) étaient proches d'un renversement de prix. De plus, cela fonctionnait dans une plage de plus ou moins trois barres. Il est intéressant de noter que seulement 40% des retournements (et il y en avait environ 169 000 au total) affichaient des barres jaunes. Il s'avère qu'une barre jaune garantit presque systématiquement un retournement de tendance, même si des retournements peuvent se produire sans elle.

En analysant plus en profondeur les tendances, j'ai remarqué une tendance claire. Au début et pendant la tendance, il n'y a presque pas de barres jaunes, seulement des barres 3D régulières regroupées en un groupe dense. Mais avant ce renversement, les groupes jaunes brillent sur le graphique.

Cela est particulièrement visible dans les tendances de long terme. Prenons par exemple la croissance de l'EUR/USD du début de 2017 à février 2018, puis sa chute jusqu'en mars 2020. Dans les deux cas, ces grappes jaunes sont apparues avant le renversement, et leur position en 3D indiquait littéralement où le prix allait évoluer !

J'ai aussi testé ce truc sur de courtes périodes – j'ai pris plusieurs segments de 2 à 3 semaines en 2024. Ça a fonctionné comme sur des roulettes ! Avant chaque renversement, des barres jaunes apparaissaient, comme pour avertir : « Hé, mec, la tendance est sur le point de s'inverser ! »

Il ne s'agit pas simplement d'un indicateur. Je pense que nous avons mis le doigt sur un élément vraiment important de la structure même du marché : la façon dont les volumes sont répartis et dont la volatilité évolue avant un changement de tendance. Maintenant, quand je vois des grappes jaunes sur un graphique 3D, je sais qu'il est temps de se préparer à un renversement de tendance !


Conclusion

Au terme de notre exploration des barres 3D, je ne peux m'empêcher de souligner à quel point cette analyse a profondément modifié ma compréhension de la microstructure du marché. Ce qui a commencé comme une expérience de visualisation a évolué vers une manière fondamentalement nouvelle de voir et de comprendre le marché.

En travaillant sur ce projet, je me suis rendu compte à quel point la représentation bidimensionnelle traditionnelle des prix nous limite. Le passage à l'analyse tridimensionnelle a ouvert des horizons entièrement nouveaux pour comprendre les relations entre le prix, le volume et le temps. J'ai été particulièrement frappé par la clarté avec laquelle les schémas précédant les événements importants du marché apparaissaient dans l'espace tridimensionnel.

La découverte la plus importante a été la capacité à détecter précocement les potentiels retournements de tendance. L'accumulation caractéristique des volumes et le changement de palette de couleurs dans la représentation 3D se sont révélés être des indicateurs étonnamment fiables des changements de tendance à venir. Il ne s'agit pas d'une simple observation théorique ; nous l'avons confirmée par de nombreux exemples historiques.

Le modèle mathématique que nous avons développé nous permet non seulement de visualiser, mais aussi d'évaluer quantitativement la dynamique du marché. L'intégration des technologies de visualisation modernes et des outils logiciels a permis d'appliquer cette méthode au trading réel. J'utilise ces outils quotidiennement et ils ont radicalement transformé mon approche de l'analyse de marché.

Cependant, je crois que nous ne sommes qu'au début du voyage. Ce projet a ouvert la porte au monde de l'analyse multivariée de la microstructure des marchés, et je suis convaincu que des recherches plus approfondies dans cette direction permettront de faire de nombreuses autres découvertes intéressantes. La prochaine étape consistera peut-être à intégrer l'apprentissage automatique pour reconnaître automatiquement les modèles 3D ou à développer de nouvelles stratégies de trading basées sur l'analyse multivariée.

En fin de compte, la véritable valeur de cette recherche ne réside pas dans les jolis graphiques ou les équations complexes, mais dans les nouvelles perspectives de marché qu'elle apporte. En tant que chercheur, je crois fermement que l'avenir de l'analyse technique réside dans une approche multivariée de l'analyse des données de marché.

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

Fichiers joints |
3D_Bars_Visual.py (19.48 KB)
Derniers commentaires | Aller à la discussion (4)
Bogard_11
Bogard_11 | 4 déc. 2024 à 11:39
La question qui se pose immédiatement est la suivante : pourquoi? Un graphique plat ne suffit pas pour une analyse précise ? La géométrie ordinaire des écoles secondaires fonctionne ici.
Thibauld Charles Ghislain Robin
Thibauld Charles Ghislain Robin | 2 févr. 2025 à 08:28
Bogard_11 #:
La question qui se pose immédiatement est la suivante : pourquoi? Un graphique plat ne suffit pas pour une analyse précise ? C'est là qu'intervient la géométrie classique du lycée.

Tout algorithme explore essentiellement les dimensions spatiales. En créant des algorithmes, nous essayons de résoudre le problème fondamental de l'explosion combinatoire par une recherche multidimensionnelle. C'est notre façon de naviguer dans une mer infinie de possibilités.

(Je m'excuse si la traduction n'est pas parfaite)


Bogard_11
Bogard_11 | 2 févr. 2025 à 17:36
Thibauld Charles Ghislain Robin #:

Tout algorithme explore essentiellement les dimensions spatiales. En créant des algorithmes, nous essayons de résoudre le problème fondamental de l'explosion combinatoire par une recherche multidimensionnelle. C'est notre façon de naviguer dans une mer infinie de possibilités.

(Je m'excuse si la traduction n'est pas parfaite)

Je comprends. Si nous ne pouvons pas résoudre le problème de la prévision des tendances par de simples formules géométriques scolaires, les gens commencent à inventer un Lysaped avec une suralimentation turbo, avec un contrôle par smartphone, avec des visages souriants et d'autres guirlandes ! Sauf qu'il n'y a pas de roues, et qu'on ne s'attend pas à ce qu'ils en aient. Et sans roues, on ne peut pas aller loin sur un seul châssis.

BeeXXI Corporation
Nikolai Semko | 2 févr. 2025 à 18:21
Bogard_11 #:

Je vois. S'il est impossible de résoudre le problème de la prévision des tendances par de simples formules géométriques scolaires, les gens commencent à inventer un lisaped avec une suralimentation turbo, avec un contrôle par smartphone, avec des smiley et autres guirlandes ! Sauf qu'il n'y a pas de roues, et qu'on ne s'attend pas à ce qu'il y ait des roues. Et sans roues, on ne peut pas aller loin sur un seul châssis.

C'est un tas de conneries
Je ne peux que compatir avec quelqu'un qui est né avec un mécanisme de perception quadridimensionnel, mais qui ne pense qu'en concepts bidimensionnels.
Trading algorithmique basé sur des figures de retournement 3D Trading algorithmique basé sur des figures de retournement 3D
Découvrir un nouveau monde de trading automatisé sur barres 3D. À quoi ressemble un robot de trading sur des barres de prix multidimensionnelles ? Les grappes de barres 3D « jaunes » sont-elles capables de prédire les retournements de tendance ? À quoi ressemble le trading multidimensionnel ?
Modèles de régression non linéaire en bourse Modèles de régression non linéaire en bourse
Modèles de régression non linéaire sur le marché boursier : Est-il possible de prédire les marchés financiers ? Prenons l'exemple de la création d'un modèle de prévision des cours de l'EUR/USD, et de la création de deux robots basés sur ce modèle : l'un en Python et l'autre en MQL5.
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.
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
Comment appliquer les règles prédictives de l'analyse des données de vente au détail en supermarché au marché réel du Forex ? Quel est le lien entre les achats de biscuits, de lait et de pain et les transactions boursières ? Cet article présente une approche novatrice du trading algorithmique basée sur l'utilisation de règles d'association.