English Русский 中文 Español Deutsch 日本語 Português Türkçe
preview
Creare barre 3D in base a tempo, prezzo e volume

Creare barre 3D in base a tempo, prezzo e volume

MetaTrader 5Integrazione |
47 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introduzione

Sono passati sei mesi da quando ho iniziato questo progetto. Sei mesi dopo, da quando mi era venuta l'idea, sembrandomi stupida, non mi ci sono soffermato molto, limitandomi a discuterne con trader che conoscevo. 

Tutto è iniziato con una semplice domanda - perché i trader insistono nell’analizzare un mercato tridimensionale osservando grafici bidimensionali? L'analisi dei prezzi, l'analisi tecnica, la teoria delle onde - tutto ciò si basa sulla proiezione del mercato su un piano. Ma cosa succederebbe se provassimo a osservare la reale struttura del prezzo, del volume e del tempo?

Nel mio lavoro sui sistemi algoritmici, ho riscontrato ripetutamente che gli indicatori tradizionali non colgono relazioni cruciali tra prezzo e volume.

L'idea delle barre 3D non è nata subito. Innanzitutto, è stato condotto un esperimento con la visualizzazione 3D della profondità del mercato. Poi sono comparsi i primi schemi di raggruppamenti volume-prezzo. E quando ho aggiunto la componente temporale e ho costruito il primo grafico a barre 3D, è diventato evidente che si trattava di un modo completamente nuovo di interpretare il mercato.

Oggi desidero condividere con voi i risultati di questo lavoro. Vi mostrerò come Python e MetaTrader 5 consentono di creare grafici con barre di volume in tempo reale. Parlerò della matematica alla base dei calcoli e di come utilizzare queste informazioni nel trading pratico.


Che cosa c'è di diverso nella barra 3D?

Fintanto che osserviamo il mercato attraverso il prisma di grafici bidimensionali, perdiamo di vista l'aspetto più importante - la sua vera struttura. L'analisi tecnica tradizionale si basa su proiezioni prezzo-tempo e volume-tempo, ma non mostra mai il quadro completo dell'interazione tra queste componenti.

L'analisi 3D è fondamentalmente diversa in quanto ci permette di osservare il mercato nella sua interezza. Quando costruiamo una barra di volume, creiamo letteralmente un'istantanea dello stato del mercato, in cui ogni dimensione contiene informazioni cruciali:

  • L'altezza della barra mostra l'ampiezza del movimento del prezzo
  • la larghezza riflette la scala temporale
  • la profondità visualizza la distribuzione del volume

Perché è importante? Immaginate due movimenti di prezzo identici su un grafico. In due dimensioni appaiono identici. Ma quando aggiungiamo la componente volume, il quadro cambia drasticamente - un movimento può essere supportato da volumi enormi, formando una barra profonda e stabile, mentre un altro si rivela essere un movimento superficiale con un supporto minimo per i trade reali.

Un approccio integrato che utilizza barre 3D risolve un problema classico dell'analisi tecnica - il ritardo del segnale. La struttura volumetrica della barra inizia a formarsi fin dai primi tick, permettendoci di osservare l'emergere di un movimento significativo molto prima che appaia su un grafico tradizionale. In sostanza, otteniamo uno strumento di analisi predittiva basato non su modelli storici, ma sulle dinamiche reali delle transazioni in corso.

L'analisi multivariata dei dati è molto più di una semplice visualizzazione accattivante; è un modo radicalmente nuovo di comprendere la microstruttura del mercato. Ogni barra 3D contiene informazioni su:

  • distribuzione del volume all'interno della fascia di prezzo
  • velocità di accumulo delle posizioni
  • squilibri tra compratori e venditori
  • volatilità a livello micro
  • momentum del movimento

Tutti questi componenti funzionano come un unico meccanismo, permettendoti di osservare la vera natura delle variazioni di prezzo. Laddove l'analisi tecnica classica vede solo una candela o una barra, l'analisi 3D mostra la complessa struttura dell'interazione tra domanda e offerta.


Equazioni per il calcolo delle metriche principali. Principi di base per la costruzione di barre 7D. La logica di combinare diverse dimensioni in un unico sistema

Il modello matematico delle barre 3D è nato dall'analisi della microstruttura reale del mercato. Ciascuna barra del sistema può essere rappresentata come una figura tridimensionale, dove:

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

Il punto chiave è il calcolo del profilo volumetrico all'interno della barra. A differenza delle classiche barre, noi analizziamo la distribuzione del volume in base ai livelli di prezzo.

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

il momentum viene calcolato come una combinazione del tasso di variazione del prezzo e del 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

Particolare attenzione viene dedicata all'analisi della volatilità all’interno della barra. Utilizziamo un'equazione ATR modificata che tiene conto della microstruttura del movimento:

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 differenza fondamentale rispetto ai classici grafici a barre è che tutte le metriche vengono calcolati in tempo reale, permettendoci di osservare la formazione della struttura della barra:

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

Tutte le misurazioni vengono combinate attraverso un sistema di fattori di ponderazione adattati allo specifico strumento:

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)

Nel trading reale, questo modello matematico ci permette di osservare aspetti del mercato quali:

  • squilibri nell'accumulo di volume
  • anomalie nella velocità di formazione dei prezzi
  • zone di consolidamento e di breakout
  • la vera forza di un trend attraverso le caratteristiche del volume

Ogni barra 3D non è più solo un punto sul grafico, ma un indicatore a tutti gli effetti dello stato del mercato in un preciso momento.


Un'analisi dettagliata dell'algoritmo per la creazione di barre 3D. Caratteristiche di utilizzo di MetaTrader 5. Dettagli sulla gestione dei dati

Dopo aver effettuato il debug dell'algoritmo principale, sono finalmente arrivato alla parte più interessante - l'implementazione di barre multidimensionali in tempo reale. Lo ammetto, all'inizio mi è sembrata un'impresa ardua. MetaTrader 5 non è particolarmente compatibile con gli script esterni e la documentazione a volte non fornisce una comprensione adeguata. Ma lasciatemi raccontare come alla fine sono riuscito a superare tutto questo.

Ho iniziato con una struttura di base per l'archiviazione dei dati. Dopo diverse iterazioni, è nata la seguente classe:

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

La parte più difficile è stata capire come calcolare correttamente la dimensione del blocco. Dopo molti esperimenti, sono giunto a questa equazione:

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

Ho avuto anche molti problemi con i volumi. Inizialmente volevo utilizzare un volume_brick di dimensioni fisse, ma mi sono subito reso conto che non funziona. La soluzione è arrivata sotto forma di un algoritmo adattivo:

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)

Ma credo di aver esagerato un po' con il calcolo delle metriche statistiche:

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

È buffo, ma la parte più difficile non è stata scrivere il codice, bensì eseguirne il debug in condizioni reali. 

Ecco il risultato finale della funzione che utilizza la normalizzazione nell'intervallo 3-9. Perché 3-9? Sia Gann che Tesla sostenevano che in quei numeri si celava una sorta di magia. Ho anche visto personalmente un trader su una piattaforma molto conosciuta che, a quanto pare, ha creato uno script di inversione di tendenza di successo basato su questi numeri. Ma evitiamo di addentrarci in teorie del complotto e misticismo. Provate invece questo:

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
Ed ecco come appare la serie di barre che abbiamo ottenuto su una singola scala. Non è molto stazionaria, vero?

Distribuzioni statistiche:

 

Naturalmente, non ero soddisfatto di una serie del genere, perché il mio obiettivo era creare una serie più o meno stazionaria - una serie stazionaria di tempo-volume-prezzo. Ecco cosa ho fatto dopo:


Introduzione alla misurazione della volatilità 

Durante l'implementazione della funzione create_stationary_4d_features, ho intrapreso un percorso fondamentalmente diverso. A differenza delle barre 3D originali, in cui ci limitavamo a scalare i dati nell'intervallo 3-9, qui mi sono concentrato sulla creazione di serie realmente stazionarie.

L'idea principale della funzione è quella di creare una rappresentazione quadridimensionale del mercato attraverso elementi stazionari. Anziché limitarsi a scalare, ogni dimensione viene trasformata in modo specifico per ottenere la stazionarietà:

  1. Dimensione temporale: Qui ho applicato la trasformazione trigonometrica, convertendo le ore in onde sinusoidali e cosinusoidali. Le equazioni sin(2π * ore/24) e cos(2π * ore/24) creano caratteristiche cicliche, eliminando completamente il problema della stagionalità giornaliera.
  2. Misurazione dei prezzi: anziché i valori assoluti dei prezzi, si utilizzano le loro variazioni relative. Nel codice, ciò viene implementato calcolando il “typical price” (massimo + minimo + chiusura)/3 e quindi calcolando i rendimenti e la loro accelerazione. Questo approccio rende la serie stazionaria indipendentemente dal livello di prezzo.
  3. Misurazione volumetrica: ecco un punto interessante - non consideriamo solo le variazioni di volume, ma anche i loro incrementi relativi. Questo è importante perché i volumi sono spesso distribuiti in modo molto disomogeneo. Nel codice, ciò viene implementato tramite l'applicazione successiva delle funzioni pct_change() e diff().
  4. Misurare la volatilità: Qui ho implementato una trasformazione in due fasi - prima calcolo la volatilità corrente attraverso la deviazione standard dei rendimenti, e poi calcolo le variazioni relative di tale volatilità. In effetti, otteniamo la "volatilità della volatilità".

Ciascun blocco di dati viene formato in una finestra scorrevole di 20 periodi. Non si tratta di un numero casuale, bensì di un numero scelto come compromesso tra la conservazione della struttura locale dei dati e la garanzia della significatività statistica dei calcoli.

Tutte le caratteristiche calcolate vengono infine scalate nell'intervallo 3-9, ma questa è già una trasformazione secondaria applicata a serie già stazionarie. Questo ci permette di mantenere la compatibilità con l'implementazione originale delle barre 3D, pur utilizzando un approccio fondamentalmente diverso alla preelaborazione dei dati.

Un punto particolarmente importante è quello di preservare tutte le metriche più importanti della funzione originale: medie mobili, volatilità, z-score. Ciò consente di utilizzare la nuova implementazione come sostituto diretto della funzione originale, ottenendo al contempo dati stazionari di qualità superiore.

Di conseguenza, otteniamo un insieme di caratteristiche che non solo sono stazionarie in senso statistico, ma conservano anche tutte le informazioni importanti sulla struttura del mercato. Questo approccio rende i dati molto più adatti all'applicazione di metodi di apprendimento automatico e analisi statistica, pur mantenendo il loro collegamento al contesto di trading originale.

Ecco la funzione: 

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

Ecco come appare in 2D:

Ora proviamo a creare un modello 3D interattivo per i prezzi in 3D utilizzando Plotly. Accanto, dovrebbe essere visibile un normale grafico bidimensionale. Ecco il codice:

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

Ecco come si presenta la nostra nuova fascia di prezzo:



Nel complesso, sembra molto interessante. Osserviamo determinate sequenze di raggruppamento dei prezzi nel tempo e valori anomali nel raggruppamento dei prezzi in base al volume. Si crea quindi la sensazione (e viene confermata direttamente dall'esperienza dei trader più esperti) che quando il mercato è instabile, quando vengono immessi volumi enormi, quando la volatilità è alle stelle, ci troviamo di fronte a un evento pericoloso che va oltre le statistiche - i famigerati rischi estremi. Pertanto, qui possiamo rilevare immediatamente tale uscita "anomala" del prezzo su tali coordinate. Vorrei ringraziare l'idea dei grafici di prezzo multivariati solo per questo! 

Notate che:

Esame del paziente (grafica 3D)

In seguito, suggerisco di visualizzare. Ma non il nostro radioso futuro sotto una palma, bensì grafici dei prezzi in 3D. Analizziamo le situazioni suddividendole in quattro categorie: tendenza al rialzo, tendenza al ribasso, inversione da tendenza al rialzo a tendenza al ribasso e inversione da tendenza al ribasso a tendenza al rialzo. Per fare ciò, dovremo modificare leggermente il codice: non avremo più bisogno degli indici delle barre, caricheremo i dati relativi a date specifiche. In realtà, è sufficiente accedere a 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

Ecco il nostro codice modificato:

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

Prendiamo in esame la prima sezione di dati: EURUSD, dal 1° Gennaio 2017 al 1° Febbraio 2018. Si tratta, infatti, di un trend rialzista molto forte. Pronti a vedere come appare in barre 3D?

Ecco un altro esempio di visualizzazione:

Concentriamoci sull'inizio del trend rialzista:

E per concludere:

Ora esaminiamo il trend ribassista. Dal 1° Febbraio 2018 al 20 Marzo 2020:

Ecco l'inizio del trend ribassista:

Ed ecco la fine:

Ciò che vediamo è che entrambe i trend (sia ribassista che rialzista) nella rappresentazione 3D iniziano come un'area di punti al di sotto della densità di punti 3D. In entrambi i casi, la fine del trend è stato segnato da una combinazione di colori giallo brillante. 

Per descrivere questo fenomeno e il comportamento dei prezzi di EUR/USD nei trend rialzisti e ribassisti, si può utilizzare la seguente equazione universale:

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

dove:

  •  P(t) — prezzo della valuta in un determinato momento.
  •  P_0 — prezzo iniziale in un determinato momento.
  •  A — ampiezza del trend, che caratterizza la scala delle variazioni di prezzo.
  •  k — rapporto che determina il tasso di variazione (k > 0 indica una tendenza rialzista; k < 0 indica una tendenza ribassista).
  •  V(u) — volume degli scambi in un dato momento, che influenza l'attività di mercato e può aumentare la rilevanza delle variazioni di prezzo.
  •  N(t) — rumore casuale che riflette le fluttuazioni imprevedibili del mercato.

Spiegazione del testo

Questa equazione descrive come il prezzo di una valuta cambia nel tempo, a seconda di una serie di fattori. Il prezzo iniziale è il punto di partenza, dopodiché l'integrale tiene conto dell'influenza dell'ampiezza del trend e del suo tasso di variazione, sottoponendo il prezzo a una crescita o a un declino esponenziale a seconda dell'entità. Il volume degli scambi rappresentato dalla funzione, aggiunge un'ulteriore dimensione, dimostrando che l'attività di mercato influenza anche le variazioni di prezzo.

Questo modello consente quindi di visualizzare le oscillazioni dei prezzi in base a diversi trend, rappresentandole in uno spazio 3D, dove l'asse temporale, il prezzo e il volume creano un quadro completo dell'attività del mercato. La luminosità della combinazione di colori in questo schema può indicare la forza del trend, con colori più brillanti che corrispondono ad un prezzo derivato più alto e di volume di scambio, segnalando forti movimenti di volume sul mercato.


Visualizzazione dell'inversione

Ecco il periodo che va dal 14 al 28 novembre. Ci sarà un'inversione delle quotazioni approssimativamente a metà di questo periodo. Che aspetto ha in coordinate 3D? Eccola:

Nel momento dell'inversione e dell'aumento della coordinata del prezzo normalizzato, ritroviamo il familiare colore giallo. Ora esaminiamo un'altra sezione di prezzo con un'inversione di tendenza, dal 13 settembre 2024 al 10 ottobre dello stesso anno:

Possiamo rivedere la stessa immagine, solo che ora il colore giallo e il suo accumulo si trovano in basso. Sembra interessante.  

Dal 19 agosto 2024 al 30 agosto 2024, si può osservare un'esatta inversione di tendenza a metà di questo intervallo di date. Diamo un'occhiata alle nostre coordinate.

Di nuovo, esattamente la stessa immagine. Consideriamo il periodo che va dal 17 luglio 2024 all'8 agosto 2024. Il modello mostrerà presto segni di inversione di tendenza?

L'ultimo periodo va dal 21 aprile al 10 agosto 2023. La tendenza rialzista si è conclusa lì.

Ritroviamo ancora il familiare colore giallo.


Cluster gialli

Durante lo sviluppo di barre 3D, mi sono imbattuto in una caratteristica molto interessante: cluster di volatilità del volume di colore giallo. Sono rimasto affascinato dal loro comportamento sul grafico! Dopo aver esaminato una quantità enorme di dati storici (più di 400.000 barre per il periodo 2022-2024, per la precisione), ho notato qualcosa di sorprendente.

Inizialmente non potevo credere ai miei occhi - su circa 100.000 barre gialle, quasi tutte (il 97%!) erano prossime a un'inversione di prezzo. Inoltre, questo metodo ha funzionato in un intervallo di più o meno tre barre. È interessante notare che solo il 40% delle inversioni (e ce ne sono state circa 169 mila in totale) ha mostrato barre gialle. A quanto pare, una barra gialla quasi garantisce un'inversione di trend, sebbene le inversioni possano verificarsi anche in sua assenza.

Analizzando più a fondo i trend, ho notato un pattern ben preciso. All'inizio e durante il trend, non ci sono quasi barre gialle, solo normali barre 3D in un gruppo denso. Ma prima dell'inversione di trend, i cluster gialli brillano sul grafico.

Ciò è particolarmente evidente nei trend di lungo periodo. Considerate, ad esempio, la crescita del tasso di cambio EURUSD dall'inizio del 2017 a febbraio 2018, e poi il calo fino a marzo 2020. In entrambi i casi, questi cluster gialli sono apparsi prima dell'inversione di trend e il loro posizionamento tridimensionale ha letteralmente indicato dove sarebbe andato il prezzo!

Ho testato questa cosa anche per brevi periodi: ho fatto diversi cicli di 2-3 settimane nel 2024. Ha funzionato come un orologio! Ogni volta prima di un'inversione, comparivano delle barre gialle, quasi come un avvertimento: "Ehi, amico, il trend sta per invertirsi!"

Non si tratta di un semplice indicatore. Penso che abbiamo individuato un aspetto davvero importante nella struttura stessa del mercato - il modo in cui i volumi sono distribuiti e come la volatilità cambia prima di un'inversione di trend. Ora, quando vedo cluster gialli su un grafico 3D, so che è il momento di prepararsi a un'inversione di trend!


Conclusioni

Concludendo la nostra analisi dei grafici a barre 3D, non posso fare a meno di notare quanto profondamente questa esplorazione abbia modificato la mia comprensione della microstruttura del mercato. Quello che era iniziato come un esperimento di visualizzazione si è evoluto in un modo radicalmente nuovo di vedere e comprendere il mercato.

Lavorando a questo progetto, mi sono reso conto più volte di quanto siamo limitati dalla tradizionale rappresentazione bidimensionale dei prezzi. Il passaggio all'analisi tridimensionale ha aperto orizzonti completamente nuovi per la comprensione delle relazioni tra prezzo, volume e tempo. Sono rimasto particolarmente colpito dalla chiarezza con cui i pattern che precedono importanti eventi di mercato apparivano nello spazio tridimensionale.

La scoperta più significativa è stata la capacità di individuare tempestivamente potenziali inversioni di trend. Il caratteristico accumulo di volumi e il cambiamento nella combinazione di colori nella rappresentazione 3D si sono rivelati indicatori sorprendentemente affidabili degli imminenti cambiamenti di trend. Non si tratta solo di un'osservazione teorica - l'abbiamo confermata con numerosi esempi storici.

Il modello matematico che abbiamo sviluppato ci permette non solo di visualizzare, ma anche di valutare quantitativamente le dinamiche di mercato. L'integrazione di moderne tecnologie di visualizzazione e strumenti software ha reso possibile l'applicazione di questo metodo nel trading reale. Utilizzo questi strumenti quotidianamente e hanno fatto un'enorme differenza nel mio approccio all'analisi di mercato.

Tuttavia, credo che siamo solo all'inizio del viaggio. Questo progetto ha aperto le porte al mondo dell'analisi multivariata della microstruttura del mercato e sono convinto che ulteriori ricerche in questa direzione porteranno a molte altre scoperte interessanti. Forse, il prossimo passo sarà l'integrazione dell'apprendimento automatico per riconoscere automaticamente i pattern 3D o lo sviluppo di nuove strategie di trading basate sull'analisi multivariata.

In definitiva, il vero valore di questa ricerca non risiede nei bei grafici o nelle complesse equazioni, bensì nelle nuove informazioni di mercato che essa fornisce. In qualità di ricercatore, credo fermamente che il futuro dell'analisi tecnica risieda in un approccio multivariato all'analisi dei dati di mercato.

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

File allegati |
3D_Bars_Visual.py (19.48 KB)
Ultimi commenti | Vai alla discussione (4)
Bogard_11
Bogard_11 | 4 dic 2024 a 11:39
Sorge subito la domanda: perché? Un grafico piatto non è sufficiente per un'analisi accurata? La normale geometria delle scuole superiori funziona.
Thibauld Charles Ghislain Robin
Thibauld Charles Ghislain Robin | 2 feb 2025 a 08:28
Bogard_11 #:
Sorge subito la domanda: perché? Un grafico piatto non è sufficiente per un'analisi accurata? È qui che funziona la normale geometria delle scuole superiori.

Qualsiasi algoritmo esplora essenzialmente le dimensioni spaziali. Creando algoritmi, cerchiamo di risolvere il problema fondamentale dell'esplosione combinatoria attraverso la ricerca multidimensionale. È il nostro modo di navigare in un mare infinito di possibilità.

(Ci scusiamo se la traduzione non è perfetta).


Bogard_11
Bogard_11 | 2 feb 2025 a 17:36
Thibauld Charles Ghislain Robin #:

Qualsiasi algoritmo esplora essenzialmente le dimensioni spaziali. Creando algoritmi, cerchiamo di risolvere il problema fondamentale dell'esplosione combinatoria attraverso la ricerca multidimensionale. È il nostro modo di navigare in un mare infinito di possibilità.

(Ci scusiamo se la traduzione non è perfetta).

Capito. Se non riusciamo a risolvere la previsione delle tendenze attraverso semplici formule geometriche scolastiche, la gente inizia a inventare un Lysaped con sovralimentazione turbo, con controllo via smartphone, con faccine sorridenti e altri orpelli! Solo che non ci sono ruote e non ci si aspetta che le abbiano. E senza ruote non si può andare lontano con un solo telaio.

BeeXXI Corporation
Nikolai Semko | 2 feb 2025 a 18:21
Bogard_11 #:

Capisco. Se è impossibile risolvere le previsioni di tendenza attraverso semplici formule geometriche scolastiche, la gente inizia a inventare un lisaped con sovralimentazione turbo, con controllo via smartphone, con faccine sorridenti e altri orpelli! Solo che non ci sono ruote, e non ci si aspetta che le abbiano. E senza ruote non si può andare lontano con un solo telaio.

Sono un mucchio di sciocchezze
Non posso che essere solidale con chi nasce con un meccanismo di percezione a 4 dimensioni, ma pensa solo in concetti bidimensionali.
Arriva il Nuovo MetaTrader 5 e MQL5 Arriva il Nuovo MetaTrader 5 e MQL5
Questa è solo una panoramica di MetaTrader 5. Non posso descrivere tutte le nuove funzionalità del sistema per un periodo di tempo così breve: i test sono iniziati il 09.09.2009. Questa è una data simbolica e sono sicuro che sarà un numero fortunato. Sono passati alcuni giorni da quando ho ricevuto la versione beta del terminale MetaTrader 5 e MQL5. Non sono riuscito a provare tutte le sue funzionalità, ma sono già sorpreso.
Modelli di regressione non lineare nei mercati finanziari Modelli di regressione non lineare nei mercati finanziari
Modelli di regressione non lineare nei mercati finanziari: È possibile prevedere i mercati finanziari? Consideriamo la possibilità di creare un modello per la previsione dei prezzi della coppia EUR/USD e di realizzare due robot basati su questo, utilizzando Python e MQL5.
Utilizza i canali MQL5.community e le chat di gruppo Utilizza i canali MQL5.community e le chat di gruppo
Il sito web MQL5.com riunisce trader di tutto il mondo. Gli utenti pubblicano articoli, condividono codici gratuiti, vendono prodotti nel Market, offrono servizi da freelance e copiano segnali di trading. Puoi comunicare con loro sul Forum, nelle chat dei trader e nei canali MetaTrader.
L’utilizzo delle regole di associazione nell'analisi dei dati Forex L’utilizzo delle regole di associazione nell'analisi dei dati Forex
Come applicare le regole predittive dell'analisi dei dati di vendita al dettaglio dei supermercati al mercato Forex reale? Che relazione c'è tra l'acquisto di biscotti, latte e pane e le transazioni in borsa? L'articolo illustra un approccio innovativo al trading algoritmico basato sull'utilizzo delle regole di associazione.