
Создаем 3D-бары на основе времени, цены и объема
Введение
Прошло полгода с того момента, как я начал этот проект. Полгода от идеи, которая казалось глупой, поэтому я особо к ней не возвращался, лишь обсуждая со знакомыми трейдерами мысль создания подобных котировок.
Все началось с простого вопроса — почему трейдеры упорно пытаются анализировать трехмерный рынок, глядя на двумерные графики? 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, здесь я сосредоточился на создании по-настоящему стационарных рядов.
Ключевая идея функции — создание четырехмерного представления рынка через стационарные признаки. Вместо простого масштабирования, каждое измерение преобразуется специальным образом для достижения стационарности:
- Временное измерение: здесь я применил тригонометрическое преобразование, превращая часы в синусоиды и косинусоиды. Формулы sin(2π * hour/24) и cos(2π * hour/24) создают циклические признаки, полностью избавляясь от проблемы суточной сезонности.
- Ценовое измерение: вместо абсолютных значений цен используются их относительные изменения. В коде это реализовано через расчет типичной цены (high + low + close)/3 и последующее вычисление доходностей и их ускорения. Такой подход делает ряд стационарным независимо от уровня цен.
- Объемное измерение: тут интересный момент — мы берем не просто изменения объемов, а их относительные приращения. Это важно, потому что объемы часто имеют очень неравномерное распределение. В коде это реализовано через последовательное применение pct_change() и diff() .
- Измерение волатильности: здесь я реализовал двухступенчатое преобразование — сначала расчет скользящей волатильности через стандартное отклонение доходностей, а затем взятие относительных изменений этой волатильности. Фактически, мы получаем "волатильность волатильности".
Каждый блок данных формируется в скользящем окне размером 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-графике, знаю — пора готовиться к развороту!
Заключение
Завершая наше исследование трехмерных баров, я не могу не отметить, насколько глубоко это погружение изменило мое понимание рыночной микроструктуры. То, что началось как эксперимент с визуализацией, превратилось в принципиально новый способ видеть и понимать рынок.
Работая над этим проектом, я постоянно сталкивался с тем, как сильно мы ограничены традиционным двумерным представлением цен. Переход к трехмерному анализу открыл совершенно новые горизонты понимания взаимосвязей между ценой, объемом и временем. Особенно меня поразило то, как четко в трехмерном пространстве проявляются паттерны, предшествующие важным рыночным событиям.
Наиболее значимым открытием стала возможность раннего обнаружения потенциальных разворотов тренда. Характерное накопление объемов и изменение цветовой гаммы в трехмерном представлении оказались удивительно надежными индикаторами грядущих изменений тренда. Это не просто теоретическое наблюдение — мы подтвердили его на множестве исторических примеров.
Математическая модель, которую мы разработали, позволяет не только визуализировать, но и количественно оценивать рыночную динамику. Интеграция современных технологий визуализации и программных средств сделала возможным применение этого метода в реальной торговле. Я использую эти инструменты ежедневно, и они существенно изменили мой подход к анализу рынка.
Однако я считаю, что мы только в начале пути. Этот проект открыл дверь в мир многомерного анализа рыночной микроструктуры, и я уверен, что дальнейшие исследования в этом направлении принесут еще много интересных открытий. Возможно, следующим шагом станет интеграция машинного обучения для автоматического распознавания трехмерных паттернов или разработка новых торговых стратегий, основанных на многомерном анализе.
В конечном счете, главная ценность этого исследования заключается не в красивых графиках или сложных формулах, а в том новом понимании рынка, которое оно дает. Как исследователь, я глубоко убежден, что будущее технического анализа лежит именно в многомерном подходе к анализу рыночных данных.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Сразу возникает вопрос - зачем? Плоского графика не достаточно для точного анализа? Там обычная школьная геометрия работает.
Любой алгоритм, по сути, исследует пространственные измерения. Создавая алгоритмы, мы пытаемся решить фундаментальную проблему комбинаторного взрыва через многомерный поиск. Это наш способ навигации в бесконечном море возможностей.
(Извините, если перевод не идеальный )
Любой алгоритм, по сути, исследует пространственные измерения. Создавая алгоритмы, мы пытаемся решить фундаментальную проблему комбинаторного взрыва через многомерный поиск. Это наш способ навигации в бесконечном море возможностей.
(Извините, если перевод не идеальный )
Понятно. Если не удается решить прогнозирование тренда через простые школьные геометрические формулы, народ начинает изобретать лисапед с турбо наддувом, с управлением через смартфон, со смайликами и прочей мишурой! Только вот колес как не было, так и не ожидается. А без колес на одной раме далеко не уедешь.
Понятно. Если не удается решить прогнозирование тренда через простые школьные геометрические формулы, народ начинает изобретать лисапед с турбо наддувом, с управлением через смартфон, со смайликами и прочей мишурой! Только вот колес как не было, так и не ожидается. А без колес на одной раме далеко не уедешь.