Création de barres 3D basées sur le temps, le prix et le volume
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_brickVoici à quoi ressemble la série de barres que nous avons obtenue sur une seule échelle. Pas très statique, n'est-ce pas ?

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é :
- 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.
- 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.
- 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().
- 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
Avertissement: Tous les droits sur ces documents sont réservés par MetaQuotes Ltd. La copie ou la réimpression de ces documents, en tout ou en partie, est interdite.
Cet article a été rédigé par un utilisateur du site et reflète ses opinions personnelles. MetaQuotes Ltd n'est pas responsable de l'exactitude des informations présentées, ni des conséquences découlant de l'utilisation des solutions, stratégies ou recommandations décrites.
Trading algorithmique basé sur des figures de retournement 3D
Modèles de régression non linéaire en bourse
L'Histogramme des prix (Profile du Marché) et son implémentation en MQL5
Utilisation des règles d'association dans l'analyse des données Forex
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation
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)
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.
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.