English 中文 Deutsch 日本語 Português
preview
Создаем 3D-бары на основе времени, цены и объема

Создаем 3D-бары на основе времени, цены и объема

MetaTrader 5Интеграция |
1 707 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Введение

Прошло полгода с того момента, как я начал этот проект. Полгода от идеи, которая казалось глупой, поэтому я особо к ней не возвращался, лишь обсуждая со знакомыми трейдерами мысль создания подобных котировок. 

Все началось с простого вопроса — почему трейдеры упорно пытаются анализировать трехмерный рынок, глядя на двумерные графики? Price action, технический анализ, волновая теория — все это работает с проекцией рынка на плоскость. Но что, если попробовать увидеть реальную структуру цены, объема и времени?

В своей работе над алгоритмическими системами я постоянно сталкивался с тем, что традиционные индикаторы упускают критически важные взаимосвязи между ценой и объемом.

Идея 3D-баров родилась не сразу. Сначала был эксперимент с трехмерной визуализацией market depth. Потом появились первые наброски volume-price кластеров. А когда я добавил временную компоненту и построил первый 3D-бар, стало очевидно, что это принципиально новый способ видеть рынок.

Сегодня я хочу поделиться с вами результатами этой работы. Покажу, как Python и MetaTrader 5 позволяют строить объемные бары в режиме реального времени. Расскажу о математике, лежащей в основе расчетов, и о том, как использовать эту информацию в практической торговле.


Чем отличается 3D-бар

Пока мы смотрим на рынок через призму двумерных графиков, мы упускаем самое важное — его реальную структуру. Традиционный технический анализ работает с проекциями цена-время, объем-время, но никогда не показывает полную картину взаимодействия этих компонентов.

3D-анализ фундаментально отличается тем, что позволяет видеть рынок как единое целое. Когда мы строим объемный бар, мы буквально создаем "слепок" рыночного состояния, где каждое измерение несет критически важную информацию:

  • высота бара показывает амплитуду движения цены
  • ширина отражает временной масштаб
  • глубина визуализирует распределение объема

Почему это важно? Представьте два одинаковых движения цены на графике. В двумерном представлении они выглядят идентично. Но когда мы добавляем объемную составляющую, картина кардинально меняется — одно движение может быть подкреплено массивным объемом, формируя глубокий и стабильный бар, а другое оказывается поверхностным всплеском с минимальной поддержкой реальных сделок.

Комплексный подход через 3D-бары решает классическую проблему технического анализа — запаздывание сигналов. Объемная структура бара начинает формироваться с первых тиков, позволяя увидеть зарождение сильного движения задолго до его проявления на обычном графике. По сути, мы получаем инструмент предиктивной аналитики, основанный не на исторических паттернах, а на реальной динамике текущих торгов.

Многомерный анализ данных — это не просто красивая визуализация, это принципиально новый способ понимания рыночной микроструктуры. Каждый 3D-бар содержит информацию о:

  • распределении объема внутри ценового диапазона
  • скорости накопления позиций
  • дисбалансах между покупателями и продавцами
  • волатильности на микроуровне
  • моментуме движения

Все эти компоненты работают как единый механизм, позволяя увидеть истинную природу ценового движения. Там, где классический технический анализ видит просто свечу или бар, 3D-анализ показывает сложную структуру взаимодействия спроса и предложения.


Формулы расчета основных метрик. Базовые принципы построения 7D-баров. Логика объединения различных измерений в единую систему

Математическая модель 3D-баров выросла из анализа реальной рыночной микроструктуры. Каждый бар в системе можно представить, как объемную фигуру, где:

class Bar3D:
    def __init__(self):
        self.price_range = None  # Диапазон цены
        self.time_period = None  # Временной интервал
        self.volume_profile = {} # Профиль объема по ценам
        self.direction = None    # Направление движения
        self.momentum = None     # Импульс
        self.volatility = None   # Волатильность
        self.spread = None       # Средний спред

Ключевой момент — расчет объемного профиля внутри бара. В отличие от классических баров, мы анализируем распределение объема по ценовым уровням.

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
        
    # Нормализуем профиль
    total_volume = sum(volume_by_price.values())
    for price in volume_by_price:
        volume_by_price[price] /= total_volume
        
    return volume_by_price

Момент движения рассчитывается, как комбинация скорости изменения цены и объема:

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

Особое внимание уделяется анализу волатильности внутри бара. Мы используем модифицированную формулу ATR, учитывающую микроструктуру движения:

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

Принципиальное отличие от классических баров — все метрики рассчитываются в режиме реального времени, позволяя видеть формирование структуры бара:

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)
    
    # Пересчитываем объемный центр тяжести
    self.volume_poc = self.calculate_poc()

Объединение всех измерений происходит через систему весовых коэффициентов, настраиваемых под конкретный инструмент:

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)

В реальной торговле эта математическая модель позволяет увидеть такие аспекты рынка как:

  • дисбалансы в накоплении объема
  • аномалии в скорости формирования цены
  • зоны консолидации и прорыва
  • истинную силу тренда через объемные характеристики

Каждый 3D-бар становится не просто точкой на графике, а полноценным индикатором состояния рынка в конкретный момент времени.


Подробный разбор алгоритма создания 3D-баров. Особенности работы с MetaTrader 5. Специфика обработки данных

После отладки основного алгоритма я, наконец, добрался до самой интересной части — реализации многомерных баров в реальном времени. Признаюсь, поначалу это казалось непростой задачей. MetaTrader 5 не особо дружелюбен к внешним скриптам, да и документация местами откровенно хромает. Но давайте я расскажу, как в итоге удалось это победить.

Начал я с базовой структуры для хранения данных. После нескольких итераций родился такой класс:

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

Самое сложное было придумать, как правильно рассчитывать размер блока. После кучи экспериментов я остановился на такой формуле:

def calculate_brick_size(symbol_info, multiplier=45):
    spread = symbol_info.spread
    point = symbol_info.point
    min_price_brick = spread * multiplier * point
    
    # Адаптивная корректировка под волатильность
    atr = calculate_atr(symbol_info.name)
    if atr > min_price_brick * 2:
        min_price_brick = atr / 2
        
    return min_price_brick

С объемами тоже намучился изрядно. Сначала хотел использовать фиксированный размер volume_brick, но быстро понял, что это не работает. Решение пришло в виде адаптивного алгоритма:

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)

А вот с расчетом статистических метрик я, кажется, немного перемудрил:

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']
    
    # Наверное, это уже лишнее
    df['zscore_price'] = stats.zscore(df['close'], nan_policy='omit')
    df['zscore_volume'] = stats.zscore(df['tick_volume'], nan_policy='omit')
    return df

Забавно, но самым сложным оказалось не написание кода, а его отладка в реальных условиях. 

Вот конечный итог функции, где есть также нормализация в диапазоне 3-9. Почему 3-9? И Ганн, и Тесла утверждали, что в этих числах скрыта некая магия. Я также лично видел трейдера на известной площадке, который якобы создал успешный скрипт определения разворота на основе этих чисел. Не буду погружаться в теорию заговора и мистику, а просто попробую:

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
А вот так выглядит полученный нами в едином масштабе ряд баров. Не очень стационарно, не правда?

Статистические распределения:

 

Меня, понятное дело, такой ряд не устроил, ибо я поставил целью создать более-менее стационарный ряд — стационарный, временно-объемно-ценовой ряд. И вот что я сделал дальше:


Вводим измерение волатильности и колдуем 

В процессе реализации функции create_stationary_4d_features, я пошел по принципиально иному пути. В отличие от оригинальных 3D-баров, где мы просто масштабировали данные в диапазон 3-9, здесь я сосредоточился на создании по-настоящему стационарных рядов.

Ключевая идея функции — создание четырехмерного представления рынка через стационарные признаки. Вместо простого масштабирования, каждое измерение преобразуется специальным образом для достижения стационарности:

  1. Временное измерение: здесь я применил тригонометрическое преобразование, превращая часы в синусоиды и косинусоиды. Формулы sin(2π * hour/24) и cos(2π * hour/24) создают циклические признаки, полностью избавляясь от проблемы суточной сезонности.
  2. Ценовое измерение: вместо абсолютных значений цен используются их относительные изменения. В коде это реализовано через расчет типичной цены (high + low + close)/3 и последующее вычисление доходностей и их ускорения. Такой подход делает ряд стационарным независимо от уровня цен.
  3. Объемное измерение: тут интересный момент — мы берем не просто изменения объемов, а их относительные приращения. Это важно, потому что объемы часто имеют очень неравномерное распределение. В коде это реализовано через последовательное применение pct_change() и diff() .
  4. Измерение волатильности: здесь я реализовал двухступенчатое преобразование — сначала расчет скользящей волатильности через стандартное отклонение доходностей, а затем взятие относительных изменений этой волатильности. Фактически, мы получаем "волатильность волатильности".

Каждый блок данных формируется в скользящем окне размером 20 периодов. Это не случайное число — оно выбрано, как компромисс между сохранением локальной структуры данных и обеспечением статистической значимости расчетов.

Все рассчитанные признаки в итоге масштабируются в диапазон 3-9, но это уже вторичное преобразование, применяемое к уже стационарным рядам. Это позволяет сохранить совместимость с оригинальной реализацией 3D-баров при использовании принципиально другого подхода к предобработке данных.

Особенно важным моментом является сохранение всех ключевых метрик из оригинальной функции — скользящих средних, волатильности, z-оценок. Это позволяет использовать новую реализацию, как прямую замену исходной функции, получая при этом более качественные, стационарные данные.

В результате мы получаем набор признаков, которые не только стационарны в статистическом смысле, но и сохраняют всю важную информацию о структуре рынка. Такой подход делает данные гораздо более пригодными для применения методов машинного обучения и статистического анализа, при этом не теряя связи с исходным контекстом торговли.

Вот функция: 

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

Вот так это выглядит в 2D:

Далее попробуем создать интерактивную 3D-модель для 3D-цен при помощи plotly. Рядом должны наблюдать обычный двумерный график. Вот код:

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

Вот так выглядит наш новый ценовой ряд:



В целом, выглядит очень интересно. Мы видим определенные последовательности группировки цен по времени, и выбросы группировки цен по объему. То есть, создается ощущение, и оно прямо подтверждается опытом ведущих трейдеров, что когда рынок неспокоен, когда шпарят огромные объемы, когда прет волатильность, — мы имеем дело с опасным выбросом, выходящим за рамки статистики, пресловутые хвостовые риски. Поэтому тут мы можем сразу же определить подобный выход цены "за пределы нормальности", на подобного рода координатах. И только за это я бы сказал спасибо идее многомерных графиков цен! 

Обратите внимание на это:

Переходим к осмотру пациента (3D-графика)

Далее я предлагаю повизуализировать. Но не наше с вами светлое будущее под пальмой у терминала, а графики 3D-цен. Давайте разобьем ситуации на четыре кластера: восходящий тренд, нисходящий тренд, разворот от восходящего тренда к нисходящему, и разворот от нисходящего тренда к восходящему. Для этого потребуется немного изменить код: номера баров нам уже не нужны, мы будем грузиться данными по определенным датам. Собственно, для этого нужно лишь перейти на 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

Вот наш измененный код:

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

Берем первый участок данных — евро доллар, с 1 января 2017 по 1 февраля 2018. По факту, мощнейший бычий тренд. Готовы увидеть, как это выглядит в 3D-барах?

Вот так выглядит еще одна визуализация:

Обратим внимание на начало восходящего тренда:

И на его окончание:

Теперь рассмотрим нисходящий тренд. С 1 февраля 2018 по 20 марта 2020:

Вот начало медвежьего тренда:

А вот его окончание:

Итак, что мы видим: оба тренда (как медвежий, так и бычий) в 3D-представлении начинались как область точек под 3D-плотностью точек. А окончание тренда в обоих случаях знаменовалось яркой цветовой желтой гаммой. 

Для описания этого явления, и поведения цен валютной пары, такой как евро-доллар, в условиях бычьего и медвежьего трендов можно использовать следующую универсальную формулу:

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

где:

  •  P(t) — цена валюты в момент времени .
  •  P_0 — начальная цена на момент .
  •  A — амплитуда тренда, характеризующая масштаб изменения цены.
  •  k — коэффициент, определяющий скорость изменения (при  k > 0 наблюдается бычий тренд, при  k < 0 — медвежий).
  •  V(u) — объем торгов в момент времени , который влияет на активность рынка и может увеличить значимость изменения цены.
  •  N(t) — случайный шум, который отражает непредсказуемые рыночные колебания.

Текстовое пояснение

Эта формула описывает, как цена валюты изменяется со временем, в зависимости от ряда факторов. Начальная цена является отправной точкой, после чего интеграл учитывает влияние амплитуды тренда и скорости его изменения, подвергая цену экспоненциальному росту или падению в зависимости от величины. Объём торгов, представленный функцией, добавляет ещё одно измерение, показывающее, что активность на рынке так же влияет на изменение цены.

Таким образом, эта модель позволяет визуализировать движения цен в условиях различных трендов, отображая их в 3D-пространстве, где ось времени, цена и объем создают богатую картину рыночной активности. Яркость цветовой гаммы в этой модели может указывать на силу тренда, где более яркие цвета соответствуют большим значениям производной цены и объема торгов, что сигнализирует о мощных движениях переливаемого объема на рынке.


Как отображается разворот

Вот период с 14 по 28 ноября, примерно посередине данного отрезка времени у нас будет разворот котировок. Как это выглядит на 3D-координатах? А вот так:

Видим уже привычный желтый цвет в момент разворота и повышения нормализованной координаты цен. Теперь рассмотрим другой участок цены с разворотом тренда, с 13 сентября 2024 по 10 октября того же года:

Вновь та же картинка, только желтый цвет и его накопление у нас теперь снизу. Интересно? Интересно. Продолжим? 

Продолжаем! 19 августа 2024 - 30 августа 2024, посередине этой даты — точный разворот тренда. Глянем на наши координаты?

Вновь точно такая же картинка. Рассмотрим период с 17 июля 2024 по 8 августа 2024. Покажет ли модель признаки скорого разворота?

Показала или нет? Как думаете?

Ну и последний период — с 21 апреля по 10 августа 2023. Там закончился бычий тренд.

Вновь видим знакомый желтый цвет.


Отдельно о желтых кластерах

В ходе разработки 3D-баров, я наткнулся на интереснейшую особенность — желтые объемно-волатильные кластеры. Не скрою, меня буквально захватило их поведение на графике! Перебрав тонну исторических данных (если точнее, то более 400 тысяч баров за 2022-2024 годы), я заметил нечто удивительное.

Сначала я глазам своим не поверил — из примерно 100 тысяч желтых баров почти все (97%!) оказались рядом с разворотами цены. Причем это работало в диапазоне плюс-минус три бара. Любопытно, что если взять все развороты, а их было около 169 тысяч, только 40% из них показывали желтые бары. Получается, желтый бар практически гарантирует разворот, хотя развороты бывают и без них.

Копаясь дальше в трендах, я заметил четкий паттерн. В начале и ходе тренда желтых баров почти нет, — только обычные 3D-бары плотной группой. Зато перед разворотом — желтые кластеры так и сияют на графике.

Особенно четко это видно на длинных трендах. Взять хотя бы рост евро-доллара с начала 2017 по февраль 2018, а потом падение до марта 2020-го. В обоих случаях перед разворотом появлялись эти желтые кластеры, причем их расположение в 3D буквально указывало, куда пойдет цена!

Я проверил эту штуку и на коротких периодах — взял несколько отрезков по 2-3 недели в 2024 году. И знаете что? Работает, как часы! Каждый раз перед разворотом появлялись желтые бары, словно предупреждая: "Эй, парень, тренд скоро развернется!"

Это не просто какой-то там индикатор. Я считаю, мы нащупали что-то действительно важное в самой структуре рынка — как распределяются объемы, как меняется волатильность перед сменой тренда. Теперь, когда я вижу желтые кластеры на 3D-графике, знаю — пора готовиться к развороту!


Заключение

Завершая наше исследование трехмерных баров, я не могу не отметить, насколько глубоко это погружение изменило мое понимание рыночной микроструктуры. То, что началось как эксперимент с визуализацией, превратилось в принципиально новый способ видеть и понимать рынок.

Работая над этим проектом, я постоянно сталкивался с тем, как сильно мы ограничены традиционным двумерным представлением цен. Переход к трехмерному анализу открыл совершенно новые горизонты понимания взаимосвязей между ценой, объемом и временем. Особенно меня поразило то, как четко в трехмерном пространстве проявляются паттерны, предшествующие важным рыночным событиям.

Наиболее значимым открытием стала возможность раннего обнаружения потенциальных разворотов тренда. Характерное накопление объемов и изменение цветовой гаммы в трехмерном представлении оказались удивительно надежными индикаторами грядущих изменений тренда. Это не просто теоретическое наблюдение — мы подтвердили его на множестве исторических примеров.

Математическая модель, которую мы разработали, позволяет не только визуализировать, но и количественно оценивать рыночную динамику. Интеграция современных технологий визуализации и программных средств сделала возможным применение этого метода в реальной торговле. Я использую эти инструменты ежедневно, и они существенно изменили мой подход к анализу рынка.

Однако я считаю, что мы только в начале пути. Этот проект открыл дверь в мир многомерного анализа рыночной микроструктуры, и я уверен, что дальнейшие исследования в этом направлении принесут еще много интересных открытий. Возможно, следующим шагом станет интеграция машинного обучения для автоматического распознавания трехмерных паттернов или разработка новых торговых стратегий, основанных на многомерном анализе.

В конечном счете, главная ценность этого исследования заключается не в красивых графиках или сложных формулах, а в том новом понимании рынка, которое оно дает. Как исследователь, я глубоко убежден, что будущее технического анализа лежит именно в многомерном подходе к анализу рыночных данных.

Прикрепленные файлы |
3D_Bars_Visual.py (19.48 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Bogard_11
Bogard_11 | 4 дек. 2024 в 11:39
Сразу возникает вопрос - зачем? Плоского графика не достаточно для точного анализа? Там обычная школьная геометрия работает.
Thibauld Charles Ghislain Robin
Thibauld Charles Ghislain Robin | 2 февр. 2025 в 08:28
Bogard_11 #:
Сразу возникает вопрос - зачем? Плоского графика не достаточно для точного анализа? Там обычная школьная геометрия работает.

Любой алгоритм, по сути, исследует пространственные измерения. Создавая алгоритмы, мы пытаемся решить фундаментальную проблему комбинаторного взрыва через многомерный поиск. Это наш способ навигации в бесконечном море возможностей.

(Извините, если перевод не идеальный )


Bogard_11
Bogard_11 | 2 февр. 2025 в 17:36
Thibauld Charles Ghislain Robin #:

Любой алгоритм, по сути, исследует пространственные измерения. Создавая алгоритмы, мы пытаемся решить фундаментальную проблему комбинаторного взрыва через многомерный поиск. Это наш способ навигации в бесконечном море возможностей.

(Извините, если перевод не идеальный )

Понятно. Если не удается решить прогнозирование тренда через простые школьные геометрические формулы, народ начинает изобретать лисапед с турбо наддувом, с управлением через смартфон, со смайликами и прочей мишурой! Только вот колес как не было, так и не ожидается. А без колес на одной раме далеко не уедешь.

BeeXXI Corporation
Nikolai Semko | 2 февр. 2025 в 18:21
Bogard_11 #:

Понятно. Если не удается решить прогнозирование тренда через простые школьные геометрические формулы, народ начинает изобретать лисапед с турбо наддувом, с управлением через смартфон, со смайликами и прочей мишурой! Только вот колес как не было, так и не ожидается. А без колес на одной раме далеко не уедешь.

Несёте откровенную пургу
Могу лишь посочувствовать тому, кто рождён с 4х мерным механизмом восприятия, но мыслит лишь двумерными понятиями. 
Возможности Мастера MQL5, которые вам нужно знать (Часть 23): CNN Возможности Мастера MQL5, которые вам нужно знать (Часть 23): CNN
Свёрточные нейронные сети (Convolutional Neural Networks, CNNs) — ещё один алгоритм машинного обучения, который, как правило, специализируется на разложении многомерных наборов данных на ключевые составные части. Мы рассмотрим принцип его работы и исследуем возможное применение для трейдеров в очередном классе сигналов Мастера MQL5.
Нейросети в трейдинге: Мультиагентная адаптивная модель (MASA) Нейросети в трейдинге: Мультиагентная адаптивная модель (MASA)
Предлагаю познакомиться с мультиагентным адаптивным фреймворком MASA, который объединяет обучение с подкреплением и адаптивные стратегии, обеспечивая гармоничный баланс между доходностью и управлением рисками в турбулентных рыночных условиях.
Популяционный ADAM (Adaptive Moment Estimation) Популяционный ADAM (Adaptive Moment Estimation)
В статье представлено превращение известного и популярного градиентного метода оптимизации ADAM в популяционный алгоритм и его модификация с введением гибридных особей. Новый подход позволяет создавать агентов, комбинирующих элементы успешных решений с использованием вероятностного распределения. Ключевое нововведение — формирование гибридных популяционных особей, которые адаптивно аккумулируют информацию от наиболее перспективных решений, повышая эффективность поиска в сложных многомерных пространствах.
Прогнозирование временных рядов с использованием нейронных сетей LSTM: Нормализация цены и токенизация времени Прогнозирование временных рядов с использованием нейронных сетей LSTM: Нормализация цены и токенизация времени
В статье описывается простая стратегия нормализации рыночных данных с использованием дневного диапазона и обучения нейронной сети для улучшения рыночных прогнозов. Разработанные модели могут использоваться совместно с существующими системами технического анализа или отдельно для прогнозирования общего направления рынка. Структура, изложенная в этой статье, может быть дополнительно усовершенствована техническим аналитиком для разработки моделей, подходящих как для ручных, так и для автоматизированных торговых стратегий.