/
fgtmenow
/
tb
Обзор
Документация
Войти
/
fgtmenow
/
tb
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
analyze/start.py
1 130 строк
52 KB
Azarev Artem
start implement bot
15 апр 2025, 16:52
15 апр 2025, 16:52
1ac3c18
Код
Авторство
О чём код?
import datetime as dt import time from typing import List, Dict, Any import numpy as np import pandas as pd import plotly.graph_objects as go from pandas import DataFrame from plotly.graph_objs import Figure from _common.data.repository.BacktestRepository import getBacktestPeriodConfig from _common.data.repository.KlineRepository import load_data from _common.domain.enums.BacktestPeriod import BacktestPeriod # Определяем константы для цветов BULLISH_COLOR: str = '#089981' # Зеленый цвет для бычьих паттернов BEARISH_COLOR: str = '#F23645' # Красный цвет для медвежьих паттернов SWING_COLOR: str = '#2157f3' # Синий для swing points def find_initial_swing_points(df: pd.DataFrame, window: int) -> None: """ Находит начальные точки разворота (swing points) на графике цены. Args: df: DataFrame с данными OHLCV, должен содержать колонки 'high' и 'low' window: Окно поиска минимумов и максимумов Добавляет колонки в DataFrame: - swing_point: 1 для максимумов, -1 для минимумов - swing_level: Уровни цен точек разворота """ # Находим начальные точки разворота swing_highs_lows: np.ndarray = np.where( df["high"] == df["high"].shift(-(window // 2)).rolling(window).max(), 1, # максимум np.where( df["low"] == df["low"].shift(-(window // 2)).rolling(window).min(), -1, # минимум np.nan # не точка разворота ), ) # Обработка краевых случаев positions: np.ndarray = np.where(~np.isnan(swing_highs_lows))[0] if len(positions) > 0: # Если первая точка максимум - добавляем минимум в начало if swing_highs_lows[positions[0]] == 1: swing_highs_lows[0] = -1 # Если первая точка минимум - добавляем максимум в начало if swing_highs_lows[positions[0]] == -1: swing_highs_lows[0] = 1 # Если последняя точка минимум - добавляем максимум в конец if swing_highs_lows[positions[-1]] == -1: swing_highs_lows[-1] = 1 # Если последняя точка максимум - добавляем минимум в конец if swing_highs_lows[positions[-1]] == 1: swing_highs_lows[-1] = -1 # Вычисляем уровень level: np.ndarray = np.where( swing_highs_lows == 1, df["high"], np.where(swing_highs_lows == -1, df["low"], np.nan) ) # Добавляем результаты в DataFrame df["swing_point"] = swing_highs_lows df["swing_level"] = level df["swing_retracement"] = np.nan # Инициализируем столбец для ретрейсментов def filter_swing_points_by_fibo(df: pd.DataFrame, fibo_level: float) -> None: """ Фильтрует точки разворота по уровню Фибоначчи. Args: df: DataFrame с данными OHLCV и найденными точками разворота fibo_level: Уровень фибоначи для фильтрации истинной коррекции Добавляет/обновляет колонку в DataFrame: - swing_retracement: Процент отката для каждой точки """ if "swing_point" not in df.columns: raise ValueError("Сначала должна быть вызвана функция find_initial_swing_points") # Используем отдельный массив numpy для манипуляций с точками разворота # Убедимся, что массив имеет тип float для поддержки NaN swing_points_array: np.ndarray = df["swing_point"].values.copy().astype(float) # Инициализируем массив для ретрейсментов retracements = np.full_like(swing_points_array, np.nan, dtype=float) # Основной цикл - он пока необходим, т.к. фильтрация меняет структуру массива while True: # Находим позиции всех непустых точек разворота positions = np.where(~np.isnan(swing_points_array))[0] if len(positions) < 4: # Нужно минимум 4 точки для анализа break # Создаем массивы для P1, P2, P3, P4 позиций и значений p_positions = np.column_stack([ positions[:-3], # P1 позиции positions[1:-2], # P2 позиции positions[2:-1], # P3 позиции positions[3:] # P4 позиции ]) # Получаем массив уровней для всех точек разворота levels = np.where( swing_points_array == 1, df["high"].values, np.where(swing_points_array == -1, df["low"].values, np.nan) ) # Векторизованный расчет ретрейсментов для 3-й и 4-й точек found_invalid = False for i, p_pos in enumerate(p_positions): p1_pos, p2_pos, p3_pos, p4_pos = map(int, p_pos) # Явно преобразуем в int # Расчет ретрейсмента для 4-й точки # Явно преобразуем элементы numpy-массива в float level2: float = float(levels[p2_pos]) level3: float = float(levels[p3_pos]) level4: float = float(levels[p4_pos]) impulse_size = abs(level3 - level2) if impulse_size > 0: p2p4_retracement = abs(level4 - level3) / impulse_size * 100 else: p2p4_retracement = np.nan if not np.isnan(p2p4_retracement): df.iloc[p4_pos, df.columns.get_loc("swing_retracement")] = p2p4_retracement retracements[p4_pos] = p2p4_retracement # Проверка паттернов # Используем типы int для получения точного порядка order = [ int(swing_points_array[p1_pos]), int(swing_points_array[p2_pos]), int(swing_points_array[p3_pos]), int(swing_points_array[p4_pos]) ] # Проверяем паттерны и фильтруем по уровню Фибоначчи if (order == [1, -1, 1, -1] and not np.isnan(p2p4_retracement) and p2p4_retracement < fibo_level): swing_points_array[p4_pos] = np.nan found_invalid = True # Обновляем массив точек разворота в DataFrame временно для очистки df_temp = df.copy() df_temp["swing_point"] = swing_points_array cleanup_consecutive(df_temp, df_temp["swing_point"].values) swing_points_array = df_temp["swing_point"].values.astype( float) # Преобразуем в float для поддержки NaN break if (order == [-1, 1, -1, 1] and not np.isnan(p2p4_retracement) and p2p4_retracement < fibo_level): swing_points_array[p4_pos] = np.nan found_invalid = True # Обновляем массив точек разворота в DataFrame временно для очистки df_temp = df.copy() df_temp["swing_point"] = swing_points_array cleanup_consecutive(df_temp, df_temp["swing_point"].values) swing_points_array = df_temp["swing_point"].values.astype( float) # Преобразуем в float для поддержки NaN break # Если не найдено недопустимых точек, выходим из цикла if not found_invalid: break # Обновляем DataFrame отфильтрованными данными df["swing_point"] = swing_points_array def resample_ohlcv_data(df: pd.DataFrame, resample_period: str) -> pd.DataFrame: """ Ресемплирует данные OHLCV на указанный период. Args: df: DataFrame с данными OHLCV и временным индексом resample_period: Период ресемплирования данных (например, '5min', '1H', '4H') Returns: pd.DataFrame: Ресемплированный DataFrame """ # Ресемплируем данные df_resampled = df.resample(resample_period).agg({ 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum', 'timestamp': 'first' }).dropna() # Обновляем timestamp после ресемплирования df_resampled['timestamp'] = df_resampled.index.astype(int) // 10 ** 6 return df_resampled def map_swing_points_to_original(df_original: pd.DataFrame, df_resampled: pd.DataFrame, resample_period: str) -> None: """ Отображает найденные точки разворота с ресемплированного DataFrame на оригинальный. Args: df_original: Оригинальный DataFrame, куда будут отображены точки df_resampled: Ресемплированный DataFrame с найденными точками разворота resample_period: Период ресемплирования, использованный для df_resampled """ # Инициализируем колонки в оригинальном DataFrame, если они еще не существуют if 'swing_point' not in df_original.columns: df_original.loc[:, 'swing_point'] = np.nan if 'swing_level' not in df_original.columns: df_original.loc[:, 'swing_level'] = np.nan if 'swing_retracement' not in df_original.columns: df_original.loc[:, 'swing_retracement'] = np.nan # Отображаем точки максимумов (highs) обратно на оригинальный DataFrame high_swings = df_resampled[df_resampled['swing_point'] == 1] for _, row in high_swings.iterrows(): mask = (df_original.index >= row.name) & (df_original.index < row.name + pd.Timedelta(resample_period)) matching_rows = df_original[mask & (df_original['high'] == row['high'])] if not matching_rows.empty: idx = matching_rows.index[0] df_original.loc[idx, 'swing_point'] = 1 df_original.loc[idx, 'swing_level'] = row['high'] df_original.loc[idx, 'swing_retracement'] = row['swing_retracement'] # Отображаем точки минимумов (lows) обратно на оригинальный DataFrame low_swings = df_resampled[df_resampled['swing_point'] == -1] for _, row in low_swings.iterrows(): mask = (df_original.index >= row.name) & (df_original.index < row.name + pd.Timedelta(resample_period)) matching_rows = df_original[mask & (df_original['low'] == row['low'])] if not matching_rows.empty: idx = matching_rows.index[0] df_original.loc[idx, 'swing_point'] = -1 df_original.loc[idx, 'swing_level'] = row['low'] df_original.loc[idx, 'swing_retracement'] = row['swing_retracement'] def _find_swing_points_internal(df: pd.DataFrame, fibo_level: float, window: int) -> None: """Internal function that implements the original find_swing_points logic""" # Находим начальные точки разворота swing_highs_lows: np.ndarray = np.where( df["high"] == df["high"].shift(-(window // 2)).rolling(window).max(), 1, # максимум np.where( df["low"] == df["low"].shift(-(window // 2)).rolling(window).min(), -1, # минимум np.nan # не точка разворота ), ) cleanup_consecutive(df, swing_highs_lows) # Обработка краевых случаев positions: np.ndarray = np.where(~np.isnan(swing_highs_lows))[0] if len(positions) > 0: # Если первая точка максимум - добавляем минимум в начало if swing_highs_lows[positions[0]] == 1: swing_highs_lows[0] = -1 # Если первая точка минимум - добавляем максимум в начало if swing_highs_lows[positions[0]] == -1: swing_highs_lows[0] = 1 # Если последняя точка минимум - добавляем максимум в конец if swing_highs_lows[positions[-1]] == -1: swing_highs_lows[-1] = 1 # Если последняя точка максимум - добавляем минимум в конец if swing_highs_lows[positions[-1]] == 1: swing_highs_lows[-1] = -1 # Вычисляем уровень level: np.ndarray = np.where( swing_highs_lows == 1, df["high"], np.where(swing_highs_lows == -1, df["low"], np.nan) ) # Добавляем результаты в оригинальный DataFrame df["swing_point"] = swing_highs_lows df["swing_level"] = level df["swing_retracement"] = np.nan def cleanup_consecutive(df: pd.DataFrame, swing_highs_lows: np.ndarray) -> None: """ Оптимизированная версия функции очистки последовательных точек разворота. Использует векторизованные операции вместо циклов. """ while True: # Получаем позиции непустых точек разворота positions = np.where(~np.isnan(swing_highs_lows))[0] if len(positions) < 2: break # Векторизованное сравнение соседних точек current = swing_highs_lows[positions[:-1]] next_points = swing_highs_lows[positions[1:]] # Проверяем последовательные точки одного типа consecutive_mask = (current == next_points) if not consecutive_mask.any(): break # Более эффективная обработка уровней current_levels = np.where( swing_highs_lows[positions[:-1]] == 1, df["high"].iloc[positions[:-1]], df["low"].iloc[positions[:-1]] ) next_levels = np.where( swing_highs_lows[positions[1:]] == 1, df["high"].iloc[positions[1:]], df["low"].iloc[positions[1:]] ) # Создаем маску для удаления точек более эффективно index_to_remove = np.zeros(len(positions), dtype=bool) # Обработка последовательных точек одного типа consecutive_same = (current == next_points) is_highs = (current == 1) # Стандартная логика фильтрации для оставшихся точек index_to_remove[:-1] |= consecutive_same & ( (is_highs & (current_levels < next_levels)) | (~is_highs & (current_levels > next_levels)) ) index_to_remove[1:] |= consecutive_same & ( (is_highs & (current_levels >= next_levels)) | (~is_highs & (current_levels <= next_levels)) ) if not index_to_remove.any(): break swing_highs_lows[positions[index_to_remove]] = np.nan def create_chart(symbol: str, df: DataFrame) -> Figure: """ Создает базовый график свечей. Args: symbol: Символ торговой пары df: DataFrame с данными OHLCV Returns: Figure: График plotly с свечами """ # Создаем график fig = go.Figure( go.Candlestick( name=symbol, x=[dt.datetime.fromtimestamp(int(t) / 1000) for t in df['timestamp']], # Конвертируем timestamp из мс в datetime open=df['open'], high=df['high'], low=df['low'], close=df['close'] ) ) fig.update_layout( xaxis_title='Date', xaxis_rangeslider_visible=False, showlegend=False, yaxis=dict( tickformat=' ', separatethousands=True ) ) return fig def draw_swing_points(fig: Figure, df: DataFrame, size: int = 6, color: str = SWING_COLOR) -> None: """ Отрисовывает точки разворота на графике. """ # Создаем временные метки в формате datetime для отображения df['datetime'] = [dt.datetime.fromtimestamp(int(t) / 1000) for t in df['timestamp']] highs = df.index[df['swing_point'] == 1] lows = df.index[df['swing_point'] == -1] # Добавляем точки максимумов (HH) fig.add_trace(go.Scatter( x=df.loc[highs, 'datetime'], y=df.loc[highs, 'high'], mode='markers+text', text=[f"{retr:.1f}%" if not pd.isna(retr) else "" for retr in df.loc[highs, 'swing_retracement']], textposition="top center", marker=dict( color=color, size=size, ), name='HH' )) # Добавляем точки минимумов (LL) fig.add_trace(go.Scatter( x=df.loc[lows, 'datetime'], y=df.loc[lows, 'low'], mode='markers+text', text=[f"{retr:.1f}%" if not pd.isna(retr) else "" for retr in df.loc[lows, 'swing_retracement']], textposition="bottom center", marker=dict( color=color, size=size, ), name='LL' )) def add_bos_choch(fig: Figure, df: DataFrame) -> Figure: """ Добавляет Break of Structure (BOS), Change of Character (CHoCH), и Confirmation (Confirm) на график. BOS - прорыв структурного уровня, указывающий на потенциальное изменение тренда. CHoCH - изменение характера движения цены, подтверждающее смену тренда. Confirm - второй BOS в том же направлении, подтверждающий тренд. Args: fig: График plotly для добавления индикаторов df: DataFrame с данными свечей и результатами анализа структуры рынка Returns: Figure: График с добавленными BOS, CHoCH и Confirm """ # Добавляем столбец datetime, если его еще нет if 'datetime' not in df.columns: df['datetime'] = [dt.datetime.fromtimestamp(int(t) / 1000) for t in df['timestamp']] for idx in df.index: # Пропускаем строки где нет BOS, CHoCH или Confirm if pd.isna(df.loc[idx, "bos"]) and pd.isna(df.loc[idx, "choch"]) and pd.isna(df.loc[idx, "confirm"]): continue # Получаем общие данные для строки if pd.isna(df.loc[idx, "struct_start_idx"]) or pd.isna(df.loc[idx, "struct_broken_idx"]) or pd.isna( df.loc[idx, "struct_level"]): continue start_idx: pd.Timestamp = df.loc[idx, "struct_start_idx"] broken_idx: pd.Timestamp = df.loc[idx, "struct_broken_idx"] level_value: float = df.loc[idx, "struct_level"] is_bullish: bool = df.loc[idx, "struct_direction"] == 1 # Конвертируем индексы в даты x_start: dt.datetime = df.loc[start_idx, 'datetime'] x_end: dt.datetime = df.loc[broken_idx, 'datetime'] x_mid: dt.datetime = x_start + (x_end - x_start) / 2 # Обработка BOS if not pd.isna(df.loc[idx, "bos"]): bos_color: str = BULLISH_COLOR if is_bullish else BEARISH_COLOR text_offset: float = level_value * 0.0001 # 0.05% от значения уровня # Горизонтальная линия fig.add_trace( go.Scatter( x=[x_start, x_end], y=[level_value, level_value], mode="lines", line=dict(color=bos_color, width=1), showlegend=False ) ) # Текст с отступом fig.add_trace( go.Scatter( x=[x_mid], y=[level_value + text_offset if is_bullish else level_value - text_offset], mode="text", text="BOS", textposition="top center" if is_bullish else "bottom center", textfont=dict(color=bos_color, size=8), showlegend=False ) ) # Обработка CHoCH if not pd.isna(df.loc[idx, "choch"]): choch_color: str = BULLISH_COLOR if is_bullish else BEARISH_COLOR text_offset: float = level_value * 0.0001 # 0.05% от значения уровня # Горизонтальная линия fig.add_trace( go.Scatter( x=[x_start, x_end], y=[level_value, level_value], mode="lines", line=dict(color=choch_color, width=2), showlegend=False ) ) # Текст с отступом fig.add_trace( go.Scatter( x=[x_mid], y=[level_value + text_offset if is_bullish else level_value - text_offset], mode="text", text="CHoCH", textposition="top center" if is_bullish else "bottom center", textfont=dict(color=choch_color, size=8), showlegend=False ) ) # Обработка Confirm if not pd.isna(df.loc[idx, "confirm"]): confirm_color: str = BULLISH_COLOR if is_bullish else BEARISH_COLOR text_offset: float = level_value * 0.0001 # 0.05% от значения уровня # Горизонтальная линия - используем пунктирную линию для отличия от BOS и CHoCH fig.add_trace( go.Scatter( x=[x_start, x_end], y=[level_value, level_value], mode="lines", line=dict(color=confirm_color, width=2), showlegend=False ) ) # Текст с отступом fig.add_trace( go.Scatter( x=[x_mid], y=[level_value + text_offset if is_bullish else level_value - text_offset], mode="text", text="Confirm", textposition="top center" if is_bullish else "bottom center", textfont=dict(color=confirm_color, size=8), showlegend=False ) ) return fig def market_structure(df: pd.DataFrame) -> None: """ Анализирует структуру рынка на основе точек разворота (swing points). Определяет: - BOS (Break of Structure): когда цена пробивает предыдущий структурный уровень - CHoCH (Change of Character): первый BOS в новом направлении (смена тренда) - Confirm: второй BOS в том же направлении (подтверждение тренда) Args: df: DataFrame с данными OHLCV и определенными точками разворота """ # Проверяем и инициализируем индекс, если он не определен if df.index.name is None and len(df.index) > 0: df.index.name = 'index' # Инициализируем столбцы - используем loc чтобы избежать SettingWithCopyWarning df['bos'] = np.nan df['choch'] = np.nan df['confirm'] = np.nan df['trend'] = np.nan df['struct_level'] = np.nan df['struct_direction'] = np.nan # Преобразуем struct_start_idx и struct_broken_idx в тип object для правильного хранения временных меток df['struct_start_idx'] = pd.Series(dtype='object') df['struct_broken_idx'] = pd.Series(dtype='object') # Получаем все точки разворота swing_points = df[~df['swing_point'].isna()] if len(swing_points) < 3: return # Необходимо минимум 3 точки разворота для анализа структуры last_direction = 0 # 0: нет направления, 1: вверх, -1: вниз bos_count = 0 # Счетчик BOS в текущем направлении first_bos_found = False # Отслеживаем, нашли ли мы первый BOS # Перебираем точки разворота, начиная с третьей точки for i in range(3, len(swing_points)): current_idx = swing_points.index[i] prev_idx = swing_points.index[i - 1] prev_prev_idx = swing_points.index[i - 2] current_point = swing_points.loc[current_idx] prev_prev_point = swing_points.loc[prev_prev_idx] # BOS ВВЕРХ: текущий максимум пробивает предыдущий максимум if (current_point['swing_point'] == 1 and prev_prev_point['swing_point'] == 1 and current_point['high'] > prev_prev_point['high']): direction = 1 struct_level = prev_prev_point['high'] struct_start_idx = prev_prev_idx # Находим свечи, где цена закрытия пробивает уровень candles_between = df.loc[prev_prev_idx:current_idx] potential_breaks = candles_between[candles_between['close'] > struct_level] struct_broken_idx = None if not potential_breaks.empty: for j in range(len(potential_breaks) - 1): # Проверяем все потенциальные свечи пробоя, кроме последней break_idx = potential_breaks.index[j] next_idx_loc = df.index.get_loc(break_idx) + 1 if next_idx_loc < len(df.index): next_idx = df.index[next_idx_loc] # Подтверждаем, что тело следующей свечи полностью выше уровня if df.loc[next_idx, 'open'] > struct_level and df.loc[next_idx, 'close'] > struct_level: struct_broken_idx = break_idx break # Если подтвержденный пробой не найден, используем текущую точку разворота if struct_broken_idx is None: struct_broken_idx = current_idx # Сохраняем общие данные о структуре df.loc[current_idx, 'struct_level'] = struct_level df.loc[current_idx, 'struct_direction'] = direction df.loc[current_idx, 'struct_start_idx'] = struct_start_idx df.loc[current_idx, 'struct_broken_idx'] = struct_broken_idx # Первый BOS всегда CHoCH if not first_bos_found: df.loc[current_idx, 'choch'] = 1 bos_count = 1 first_bos_found = True # Проверяем, является ли это CHoCH (изменение направления) elif last_direction == -1: df.loc[current_idx, 'choch'] = 1 bos_count = 1 # Проверяем, является ли это подтверждением (второй BOS в том же направлении) elif last_direction == 1 and bos_count == 1: df.loc[current_idx, 'confirm'] = 1 bos_count = 2 # Иначе это обычный BOS else: df.loc[current_idx, 'bos'] = 1 bos_count += 1 last_direction = direction # BOS ВНИЗ: текущий минимум пробивает предыдущий минимум elif (current_point['swing_point'] == -1 and prev_prev_point['swing_point'] == -1 and current_point['low'] < prev_prev_point['low']): direction = -1 struct_level = prev_prev_point['low'] struct_start_idx = prev_prev_idx # Находим свечи, где цена закрытия пробивает уровень candles_between = df.loc[prev_prev_idx:current_idx] potential_breaks = candles_between[candles_between['close'] < struct_level] struct_broken_idx = None if not potential_breaks.empty: for j in range(len(potential_breaks) - 1): # Проверяем все потенциальные свечи пробоя, кроме последней break_idx = potential_breaks.index[j] next_idx_loc = df.index.get_loc(break_idx) + 1 if next_idx_loc < len(df.index): next_idx = df.index[next_idx_loc] # Подтверждаем, что тело следующей свечи полностью ниже уровня if df.loc[next_idx, 'open'] < struct_level and df.loc[next_idx, 'close'] < struct_level: struct_broken_idx = break_idx break # Если подтвержденный пробой не найден, используем текущую точку разворота if struct_broken_idx is None: struct_broken_idx = current_idx # Сохраняем общие данные о структуре df.loc[current_idx, 'struct_level'] = struct_level df.loc[current_idx, 'struct_direction'] = direction df.loc[current_idx, 'struct_start_idx'] = struct_start_idx df.loc[current_idx, 'struct_broken_idx'] = struct_broken_idx # Первый BOS всегда CHoCH if not first_bos_found: df.loc[current_idx, 'choch'] = 1 bos_count = 1 first_bos_found = True # Проверяем, является ли это CHoCH (изменение направления) elif last_direction == 1: df.loc[current_idx, 'choch'] = 1 bos_count = 1 # Проверяем, является ли это подтверждением (второй BOS в том же направлении) elif last_direction == -1 and bos_count == 1: df.loc[current_idx, 'confirm'] = 1 bos_count = 2 # Иначе это обычный BOS else: df.loc[current_idx, 'bos'] = 1 bos_count += 1 last_direction = direction # Отмечаем диапазоны тренда if last_direction != 0: # Находим все индексы, где у нас есть подтвержденные тренды confirm_points = df[~df['confirm'].isna()] choch_points = df[~df['choch'].isna()] if not confirm_points.empty: for i in range(len(confirm_points)): confirm_idx = confirm_points.index[i] # Получаем фактическую свечу, которая пробила структуру struct_broken_idx = df.loc[confirm_idx, 'struct_broken_idx'] # Находим следующий CHoCH после этого подтверждения next_choch = choch_points[choch_points.index > confirm_idx] if not next_choch.empty: next_choch_idx = next_choch.index[0] # Получаем фактическую свечу, которая пробила структуру для CHoCH next_choch_broken_idx = df.loc[next_choch_idx, 'struct_broken_idx'] end_idx = next_choch_broken_idx else: end_idx = df.index[-1] # Отмечаем тренд для этого диапазона - начиная со свечи пробоя структуры direction = df.loc[confirm_idx, 'struct_direction'] df.loc[struct_broken_idx:end_idx, 'trend'] = direction def add_trend_intervals(fig: Figure, df: DataFrame) -> None: """ Добавляет цветные прямоугольники для отображения интервалов тренда на графике. Args: fig: Фигура Plotly для добавления интервалов тренда df: DataFrame с данными о тренде (1 для бычьего, -1 для медвежьего) """ # Убеждаемся, что столбец datetime существует if 'datetime' not in df.columns: df['datetime'] = [dt.datetime.fromtimestamp(int(t) / 1000) for t in df['timestamp']] # Находим диапазоны с непрерывными значениями тренда trend_changes = df['trend'].ne(df['trend'].shift()).cumsum() trend_groups = df.groupby([trend_changes, 'trend']) # Перебираем каждую группу тренда for (_, trend_value), group in trend_groups: # Пропускаем группы без тренда if pd.isna(trend_value): continue # Получаем начало и конец интервала тренда start_time = group['datetime'].iloc[0] end_time = group['datetime'].iloc[-1] # Получаем диапазон цен для прямоугольника y_min = group['low'].min() * 0.999 # Добавляем небольшой отступ y_max = group['high'].max() * 1.001 # Добавляем небольшой отступ # Устанавливаем цвет в зависимости от направления тренда color = BULLISH_COLOR if trend_value == 1 else BEARISH_COLOR # Добавляем прямоугольник для интервала тренда fig.add_shape( type="rect", x0=start_time, x1=end_time, y0=y_min, y1=y_max, fillcolor=color, opacity=0.1, layer="below", line=dict(width=0) ) def transfer_mtf_trend_data(df_higher: pd.DataFrame, df_lower: pd.DataFrame) -> None: """ Переносит данные о тренде с более высокого таймфрейма на более низкий. Сначала определяет интервалы тренда на более высоком таймфрейме на основе точек CHoCH. Затем переносит эти значения тренда на более низкий таймфрейм как 'mtf_trend'. Args: df_higher: DataFrame с данными более высокого таймфрейма (например, недельного) df_lower: DataFrame с данными более низкого таймфрейма (например, дневного) """ # Добавляем новые столбцы в dataframe высокого таймфрейма - тренд df_higher.loc[:, 'trend'] = np.nan # Получаем все точки CHoCH choch_points = df_higher[~df_higher['choch'].isna()] if not choch_points.empty: # Получаем фактические индексы пробоя структуры и направления break_indices = [] directions = [] # Собираем все точки пробоя структуры и их направления for idx in choch_points.index: if not pd.isna(df_higher.loc[idx, 'struct_broken_idx']): # Используем фактическую точку пробоя структуры break_indices.append(df_higher.loc[idx, 'struct_broken_idx']) directions.append(df_higher.loc[idx, 'struct_direction']) # Заполняем значения тренда между точками пробоя структуры for i in range(len(directions)): start_idx = break_indices[i] # Для последней точки пробоя заполняем до конца if i < len(directions) - 1: end_idx = break_indices[i + 1] else: end_idx = df_higher.index[-1] # Заполняем тренд направлением текущего пробоя структуры df_higher.loc[start_idx:end_idx, 'trend'] = directions[i] # Создаем новый столбец в нижнем таймфрейме для мульти-таймфреймового тренда df_lower.loc[:, 'mtf_trend'] = np.nan # Получаем общие даты между нижним и верхним таймфреймами common_indices = df_lower.index.intersection(df_higher.index) # Переносим столбец 'trend' с высшего на низший как 'mtf_trend' df_lower.loc[common_indices, 'mtf_trend'] = df_higher.loc[common_indices, 'trend'] def add_mtf_trend(fig: Figure, df: DataFrame) -> None: """ Добавляет информацию о тренде более высокого таймфрейма на график с использованием затенения фона. Args: fig: Фигура Plotly для добавления индикаторов MTF тренда df: DataFrame с данными mtf_trend """ # Убеждаемся, что столбец datetime существует if 'datetime' not in df.columns: df['datetime'] = [dt.datetime.fromtimestamp(int(t) / 1000) for t in df['timestamp']] # Пропускаем, если нет данных MTF тренда if 'mtf_trend' not in df.columns or df['mtf_trend'].isna().all(): return # Находим непрерывные блоки одного и того же MTF тренда mtf_trend_changes = df['mtf_trend'].ne(df['mtf_trend'].shift()).cumsum() mtf_trend_groups = df.groupby([mtf_trend_changes, 'mtf_trend']) # Перебираем каждую группу тренда for (_, trend_value), group in mtf_trend_groups: # Пропускаем группы без тренда if pd.isna(trend_value): continue # Получаем начало и конец интервала тренда start_time = group['datetime'].iloc[0] end_time = group['datetime'].iloc[-1] # Получаем диапазон цен для всего видимого графика y_min = df['low'].min() * 0.995 # Добавляем отступ y_max = df['high'].max() * 1.005 # Добавляем отступ # Устанавливаем цвет в зависимости от направления тренда color = BULLISH_COLOR if trend_value == 1 else BEARISH_COLOR # Добавляем прямоугольник для интервала MTF тренда с очень светлым фоном fig.add_shape( type="rect", x0=start_time, x1=end_time, y0=y_min, y1=y_max, fillcolor=color, opacity=0.05, # Очень светлый фон layer="below", line=dict(width=0) ) # Добавляем метку в верхней части графика fig.add_annotation( x=start_time, y=y_max, text="1H Trend Up" if trend_value == 1 else "1H Trend Down", showarrow=False, font=dict(color=color, size=10), bgcolor="rgba(255, 255, 255, 0.7)", bordercolor=color, borderwidth=1, borderpad=2 ) def analyze_fvg(df: pd.DataFrame, min_size_pct: float = 0.1, length: int = 500) -> List[Dict[str, Any]]: """ Анализирует зоны FVG (Fair Value Gap) и возвращает список зон. Args: df: DataFrame с данными OHLCV и временным индексом min_size_pct: Минимальный размер зоны в % для фильтрации length: Грубина анализа Returns: List[Dict[str, Any]]: Список найденных зон FVG """ # Ограничиваем анализ последними 500 свечами df_analysis = df.iloc[-length:].copy() # Обеспечиваем наличие столбца datetime if 'datetime' not in df_analysis.columns: df_analysis['datetime'] = [pd.Timestamp.fromtimestamp(int(t) / 1000) for t in df_analysis['timestamp']] fvg_zones = [] # Проходим по всем свечам, начиная с третьей for i in range(2, len(df_analysis)): curr_idx = df_analysis.index[i] prev_prev_idx = df_analysis.index[i - 2] # Проверяем условие бычьего FVG (low[i-2] > high[i]) if df_analysis.loc[prev_prev_idx, 'low'] > df_analysis.loc[curr_idx, 'high']: zone = { 'top': df_analysis.loc[prev_prev_idx, 'low'], 'bottom': df_analysis.loc[curr_idx, 'high'], 'start_time': df_analysis.loc[curr_idx, 'datetime'], 'end_time': None, 'direction': 'bearish', 'visited': False, 'visit_time': None, 'size_pct': (df_analysis.loc[prev_prev_idx, 'low'] - df_analysis.loc[curr_idx, 'high']) / df_analysis.loc[curr_idx, 'high'] * 100 } # Добавляем зону только если она больше минимального размера if zone['size_pct'] >= min_size_pct: fvg_zones.append(zone) # Проверяем условие медвежьего FVG (high[i-2] < low[i]) elif df_analysis.loc[prev_prev_idx, 'high'] < df_analysis.loc[curr_idx, 'low']: zone = { 'top': df_analysis.loc[curr_idx, 'low'], 'bottom': df_analysis.loc[prev_prev_idx, 'high'], 'start_time': df_analysis.loc[curr_idx, 'datetime'], 'end_time': None, 'direction': 'bullish', 'visited': False, 'visit_time': None, 'size_pct': (df_analysis.loc[curr_idx, 'low'] - df_analysis.loc[prev_prev_idx, 'high']) / df_analysis.loc[prev_prev_idx, 'high'] * 100 } # Добавляем зону только если она больше минимального размера if zone['size_pct'] >= min_size_pct: fvg_zones.append(zone) # Проверяем пересечение каждой зоны последующими свечами for i, zone in enumerate(fvg_zones): # Находим индекс строки, где datetime соответствует start_time зоны mask = df_analysis['datetime'] == zone['start_time'] if not mask.any(): continue # Пропускаем, если не найдено совпадение start_idx = df_analysis.index[mask].tolist()[0] start_loc = df_analysis.index.get_loc(start_idx) for j in range(start_loc + 1, len(df_analysis)): candle_idx = df_analysis.index[j] candle_high = df_analysis.loc[candle_idx, 'high'] candle_low = df_analysis.loc[candle_idx, 'low'] # Проверяем, пересекает ли свеча зону FVG if ((candle_high >= zone['bottom'] and candle_high <= zone['top']) or # верхняя граница свечи внутри зоны (candle_low >= zone['bottom'] and candle_low <= zone['top']) or # нижняя граница свечи внутри зоны (candle_low <= zone['bottom'] and candle_high >= zone['top'])): # свеча полностью перекрывает зону zone['visited'] = True zone['visit_time'] = df_analysis.loc[candle_idx, 'datetime'] zone['end_time'] = df_analysis.loc[candle_idx, 'datetime'] break # Если зона не была посещена, устанавливаем end_time на конец данных if not zone['visited']: zone['end_time'] = df_analysis['datetime'].iloc[-1] return fvg_zones def render_fvg(fig: Figure, zones: List[Dict[str, Any]], show_visited: bool = False) -> None: """ Отображает найденные зоны FVG на графике. Args: fig: График plotly для добавления зон FVG zones: Список зон FVG, полученный из analyze_fvg show_visited: Если True, отображаются только посещенные зоны, если False - только непосещенные """ if not zones: return for zone in zones: # Пропускаем зоны в зависимости от параметра show_visited if not show_visited and zone['visited']: continue # Устанавливаем синюю обводку для всех зон FVG border_color = 'rgba(33, 87, 243, 0.8)' # SWING_COLOR (синий) # Вычисляем середину прямоугольника по времени x_mid = zone['start_time'] + (zone['end_time'] - zone['start_time']) / 2 # Вычисляем середину прямоугольника по цене y_mid = (zone['top'] + zone['bottom']) / 2 # Добавляем прямоугольник для зоны FVG fig.add_shape( type="rect", x0=zone['start_time'], x1=zone['end_time'], y0=zone['bottom'], y1=zone['top'], fillcolor='rgba(0, 0, 0, 0)', # прозрачный фон line=dict(color=border_color, width=1), layer="below" ) # Добавляем метку с направлением по центру прямоугольника fig.add_annotation( x=x_mid, y=y_mid, text=f"FVG {zone['direction']}", showarrow=False, font=dict(size=8, color="gray"), xanchor="center", yanchor="middle" ) if __name__ == '__main__': from bot.objects.Store import Store symbol = "BTCUSDT" store = Store() period = getBacktestPeriodConfig(BacktestPeriod.DAY) df_1m = load_data(store.session, symbol, "1", period, True) df_day = df_1m.iloc[-720:].copy() df_5m = resample_ohlcv_data(df_day, "5min") df_1h = resample_ohlcv_data(df_1m, "60min") start_time_full = time.time() # Анализ дневного графика с использованием отдельных функций # Шаг 1: Нахождение начальных точек разворота start_time = time.time() find_initial_swing_points(df_5m, window=3) print(f"Шаг 1 (Поиск точек разворота): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 2: Очистка последовательных точек start_time = time.time() cleanup_consecutive(df_5m, df_5m["swing_point"].values) print(f"Шаг 2 (Очистка последовательных точек): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 3: Фильтрация по уровню Фибоначчи start_time = time.time() filter_swing_points_by_fibo(df_5m, fibo_level=38.2) print(f"Шаг 3 (Фильтрация по Фибоначчи): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 4: Отображение точек на оригинальный DataFrame start_time = time.time() map_swing_points_to_original(df_day, df_5m, "5min") print(f"Шаг 4 (Отображение на оригинальный DF): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 5. Анализ структуры рынка start_time = time.time() market_structure(df=df_day) print(f"Шаг 5 (Анализ структуры рынка): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 6. Анализ FVG зон для дневного графика # start_time = time.time() # fvg_zones_day = analyze_fvg(df_5m, min_size_pct=0.1, length=500) # print(f"Шаг 6 Анализ FVG зон для дневного графика: {(time.time() - start_time) * 1000:.2f} мс") print("\nАнализ недельного графика:") # Шаг 1: Нахождение начальных точек разворота start_time = time.time() find_initial_swing_points(df_1h, window=3) print(f"Шаг 1 (Поиск точек разворота): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 2: Очистка последовательных точек start_time = time.time() cleanup_consecutive(df_1h, df_1h["swing_point"].values) print(f"Шаг 2 (Очистка последовательных точек): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 3: Фильтрация по уровню Фибоначчи start_time = time.time() filter_swing_points_by_fibo(df_1h, fibo_level=38.2) print(f"Шаг 3 (Фильтрация по Фибоначчи): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 4: Отображение точек на оригинальный DataFrame start_time = time.time() map_swing_points_to_original(df_1m, df_1h, "60min") print(f"Шаг 4 (Отображение на оригинальный DF): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 5. Анализ структуры рынка start_time = time.time() market_structure(df=df_1m) print(f"Шаг 5 (Анализ структуры рынка): {(time.time() - start_time) * 1000:.2f} мс") # Шаг 6. Переносим данные тренда с недельного на дневной график start_time = time.time() transfer_mtf_trend_data(df_1m, df_day) print(f"Шаг 6 Перенос данных тренда: {(time.time() - start_time) * 1000:.2f} мс") full_time = time.time() - start_time_full print(f"\nВремя выполнения: {full_time * 1000:.2f} мс") # Создаем график с данными оригинального таймфрейма и анализом fig = create_chart(symbol, df_day) draw_swing_points(fig, df_day) add_bos_choch(fig, df_day) add_trend_intervals(fig, df_day) # Добавляем интервалы тренда add_mtf_trend(fig, df_day) # Добавляем индикаторы MTF тренда # Отображаем непосещенные FVG зоны #render_fvg(fig, fvg_zones_day, show_visited=True) fig.show()