preview
Нейросимвольные системы в алготрейдинге: Объединение символьных правил и нейронных сетей

Нейросимвольные системы в алготрейдинге: Объединение символьных правил и нейронных сетей

MetaTrader 5Торговые системы | 22 января 2025, 08:09
378 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Введение в нейросимвольные системы: принципы объединения правил и нейросетей

Представьте, что вы пытаетесь объяснить компьютеру, как торговать на бирже. С одной стороны, у нас есть классические правила и паттерны — те самые "голова и плечи", "двойное дно" и сотни других фигур, знакомых любому трейдеру. Многие из нас писали советники на MQL5, пытаясь закодировать эти закономерности. Но рынок — это живой организм, он постоянно меняется, и жёсткие правила часто дают сбой.

С другой стороны, есть нейронные сети — модные, мощные, но порой абсолютно непрозрачные в своих решениях. Скормите LSTM-сети исторические данные, и она будет делать прогнозы с неплохой точностью. Вот только почему она приняла то или иное решение — часто остаётся загадкой. А в торговле каждый неверный шаг может стоить реальных денег.

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

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

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

Готовы погрузиться в мир, где классические правила трейдинга встречаются с нейронными сетями? Тогда поехали!


Символьные правила в трейдинге: паттерны и их статистика

Начнем с простого — что такое паттерн на рынке? В классическом техническом анализе это определённая фигура на графике, например, "двойное дно" или "флаг". Но когда мы говорим о программировании торговых систем, нам нужно мыслить более абстрактно. В нашем коде паттерн — это последовательность движений цены, закодированная в бинарном виде: 1 для роста, 0 для падения.

Казалось бы, примитивно? Отнюдь. Такое представление даёт нам мощный инструмент для анализа. Возьмём последовательность [1, 1, 0, 1, 0] — это не просто набор цифр, а закодированный мини-тренд. На Python мы можем искать такие паттерны с помощью простого, но эффективного кода:

pattern = tuple(np.where(data['close'].diff() > 0, 1, 0))

Но настоящая магия начинается, когда мы начинаем анализировать статистику. Для каждого паттерна мы можем рассчитать три ключевых параметра:

  1. Частота появления (frequency) — сколько раз паттерн встречался в истории
  2. Процент успешных сработок (winrate) — как часто после паттерна цена шла в прогнозируемом направлении
  3. Надёжность (reliability) — комплексный показатель, учитывающий и частоту, и винрейт

Вот реальный пример из моей практики: паттерн [1, 1, 1, 0, 0] на 4-часовом графике EURUSD показывал винрейт 68% при частоте появления более 200 раз за год. Звучит заманчиво, да? Но тут важно не попасть в ловушку переоптимизации.

Поэтому мы добавили динамический фильтр надёжности:

reliability = frequency * winrate * (1 - abs(0.5 - winrate))

Эта формула удивительна в своей простоте. Она не только учитывает частоту и винрейт, но и штрафует паттерны с подозрительно высокой эффективностью, которая часто оказывается статистической аномалией.

Отдельная история — длина паттернов. Короткие паттерны (3-4 бара) встречаются часто, но дают много шума. Длинные (20-25 баров) более надёжны, но редки. Золотая середина обычно находится в диапазоне 5-8 баров. Хотя, признаюсь, для некоторых инструментов я видел отличные результаты и на 12-барных паттернах.

Важный момент — горизонт прогнозирования. В нашей системе мы используем параметр forecast_horizon, который определяет, на сколько баров вперёд мы пытаемся предсказать движение. Эмпирическим путём пришли к значению 6 — оно даёт оптимальный баланс между точностью прогноза и торговыми возможностями.

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


Архитектура нейронной сети для анализа рыночных данных

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

Почему именно LSTM? Дело в том, что рыночные данные — это не просто набор чисел, а последовательность, где каждое значение связано с предыдущими. LSTM-сети отлично улавливают такие долгосрочные зависимости. Вот как выглядит базовая структура нашей сети:

model = tf.keras.Sequential([
    tf.keras.layers.LSTM(256, input_shape=input_shape, return_sequences=True),
    tf.keras.layers.Dropout(0.4),
    tf.keras.layers.LSTM(128),
    tf.keras.layers.Dropout(0.3),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

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

Важный момент — размерность входных данных. Параметр input_shape определяется тремя ключевыми факторами:

  1. Размер окна анализа (у нас это 10 временных шагов)
  2. Количество базовых признаков (цена, объём, технические индикаторы)
  3. Количество признаков, извлеченных из паттернов

В итоге получается тензор размерности (batch_size, 10, features), где features — это суммарное количество всех признаков. Именно такой формат данных ожидает первый LSTM-слой.

Обратите внимание на параметр return_sequences=True в первом LSTM-слое. Это означает, что слой возвращает последовательность выходов для каждого временного шага, а не только для последнего. Это позволяет второму LSTM-слою получить более детальную информацию о временной динамике. А вот второй LSTM уже выдаёт только финальное состояние — его выход идёт на полносвязные слои.

Полносвязные слои (Dense) играют роль "интерпретатора" — они преобразуют сложные паттерны, найденные LSTM, в конкретное решение. Первый Dense-слой с ReLU-активацией обрабатывает нелинейные зависимости, а финальный слой с сигмоидной активацией выдаёт вероятность движения цены вверх.

Отдельного внимания заслуживает процесс компиляции модели:

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
)

Мы используем оптимизатор Adam — он хорошо зарекомендовал себя для нестационарных данных, которыми и являются рыночные цены. Binary crossentropy как функция потерь идеально подходит для нашей задачи бинарной классификации (предсказание направления движения цены). А набор метрик помогает отслеживать не только точность, но и качество предсказаний в разрезе ложноположительных и ложноотрицательных срабатываний.

В процессе разработки мы экспериментировали с разными конфигурациями сети. Пробовали добавлять сверточные слои (CNN) для выявления локальных паттернов, экспериментировали с механизмом внимания (Attention), но в итоге пришли к выводу, что простота и прозрачность архитектуры важнее. Чем сложнее сеть, тем труднее интерпретировать её решения, а в трейдинге понимание логики работы системы критически важно.


Интеграция паттернов в нейросеть: обогащение входных данных

Теперь самое интересное — как мы "скрещиваем" классические паттерны с нейронной сетью. Это не просто конкатенация признаков, а целая система предварительной обработки и анализа данных.

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

base_features = [
    'close',  # Цена закрытия
    'volume',  # Объём
    'rsi',    # Relative Strength Index
    'macd',   # MACD
    'bb_upper', 'bb_lower'  # Границы Bollinger Bands
]

Но это только начало. Главная инновация – добавление статистик паттернов. Для каждого паттерна мы рассчитываем три ключевых показателя:

pattern_stats = {
    'winrate': np.mean(outcomes),  # Процент успешных сработок
    'frequency': len(outcomes),     # Частота появления
    'reliability': len(outcomes) * np.mean(outcomes) * (1 - abs(0.5 - np.mean(outcomes)))  # Надёжность
}

Особое внимание к последней метрике — reliability. Это наша авторская разработка, которая учитывает не только частоту и винрейт, но и "подозрительность" статистики. Если винрейт слишком близок к 100% или слишком волатилен, показатель надёжности снижается.

Процесс интеграции этих данных в нейросеть требует особой осторожности. 

def prepare_data(df):
    # Базовые признаки нормализуем через MinMaxScaler
    X_base = self.scaler.fit_transform(df[base_features].values)
    
    # Для статистик паттернов используем специальную нормализацию
    pattern_features = self.pattern_analyzer.extract_pattern_features(
        df, lookback=len(df)
    )
    
    return np.column_stack((X_base, pattern_features))

Решение проблемы разной размерности паттернов:

def extract_pattern_features(self, data, lookback=100):
    features_per_length = 5  # фиксированное количество признаков на паттерн
    total_features = len(self.pattern_lengths) * features_per_length
    
    features = np.zeros((len(data) - lookback, total_features))
    # ... заполнение массива признаков

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

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

market_features = {
    'volatility': calculate_atr(data),  # Волатильность через ATR
    'trend_strength': calculate_adx(data),  # Сила тренда через ADX
    'market_phase': identify_market_phase(data)  # Фаза рынка
}

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

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

# Заполнение пропусков с учётом специфики каждого признака
df['close'] = df['close'].fillna(method='ffill')  # для цен
df['volume'] = df['volume'].fillna(df['volume'].rolling(24).mean())  # для объёмов
pattern_features = np.nan_to_num(pattern_features, nan=-1)  # для признаков паттернов

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


Система принятия решений: от анализа к сигналам

Давайте поговорим о том, как система реально принимает решения. Забудьте на минуту про нейронки и паттерны — в конце дня нам нужно принять чёткое решение: входить в рынок или нет. И если входить — то с каким объёмом.

Базовая логика у нас простая — мы берём два потока данных: прогноз от нейросети и статистику паттернов. Нейросеть даёт нам вероятность движения вверх/вниз, а паттерны подтверждают или опровергают этот прогноз. Но дьявол, как обычно, в деталях.

Вот что происходит под капотом:

def get_trading_decision(self, market_data):
    # Получаем прогноз от нейросети
    prediction = self.model.predict(market_data)
    
    # Вытаскиваем активные паттерны
    patterns = self.pattern_analyzer.get_active_patterns(market_data)
    
    # Базовая проверка условий рынка
    if not self._market_conditions_ok():
        return None  # Не торгуем если что-то не так
        
    # Проверяем согласованность сигналов
    if not self._signals_aligned(prediction, patterns):
        return None  # Нет консенсуса - нет сделки
        
    # Рассчитываем уверенность в сигнале
    confidence = self._calculate_confidence(prediction, patterns)
    
    # Определяем размер позиции
    size = self._get_position_size(confidence)
    
    return TradingSignal(
        direction='BUY' if prediction > 0.5 else 'SELL',
        size=size,
        confidence=confidence,
        patterns=patterns
    )

Первое, что мы проверяем — базовые условия рынка. Никакого rocket science, просто здравый смысл:

def _market_conditions_ok(self):
    # Проверяем время
    if not self.is_trading_session():
        return False
        
    # Смотрим спред
    if self.current_spread > self.MAX_ALLOWED_SPREAD:
        return False
        
    # Проверяем волатильность
    if self.current_atr > self.volatility_threshold:
        return False
    
    return True

Дальше идёт проверка согласованности сигналов. Тут важный момент — мы не требуем, чтобы все сигналы были идеально согласованы. Достаточно, чтобы основные индикаторы не противоречили друг другу:

def _signals_aligned(self, ml_prediction, pattern_signals):
    # Определяем базовое направление
    ml_direction = ml_prediction > 0.5
    
    # Считаем, сколько паттернов его подтверждают
    confirming_patterns = sum(1 for p in pattern_signals 
                            if p.predicted_direction == ml_direction)
    
    # Нужно подтверждение хотя бы 60% паттернов
    return confirming_patterns / len(pattern_signals) >= 0.6

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

def _calculate_confidence(self, prediction, patterns):
    # Базовая уверенность от ML-модели
    base_confidence = abs(prediction - 0.5) * 2
    
    # Учитываем подтверждающие паттерны
    pattern_confidence = self._get_pattern_confidence(patterns)
    
    # Взвешенное среднее с эмпирически подобранными коэффициентами
    return (base_confidence * 0.7 + pattern_confidence * 0.3)

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


Заключение

Объединение классических паттернов с нейросетевым анализом даёт качественно новый результат: нейросеть улавливает тонкие рыночные взаимосвязи, в то время как проверенные временем паттерны обеспечивают базовую структуру торговых решений. В наших тестах такой подход показал стабильно лучшие результаты как по сравнению с чисто техническим анализом, так и с изолированным применением машинного обучения.

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Evgeniy Chernish
Evgeniy Chernish | 22 янв. 2025 в 09:25
Основная проблема - это устойчивость вычисленной частоты появления белой или черной свечи после появления паттерна. На малых выборках она недостоверна, а на больших 50/50. 

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


Stanislav Korotky
Stanislav Korotky | 22 янв. 2025 в 11:53
Не касаясь самого подхода, низведение реальных диапазонов движений до двух классов прибивает полезную информацию, которую могла бы выделить нейросеть (ради чего мы её и прикручиваем) - сродни тому, как если бы мы систему распознаваний цветных изображений стали кормить чернобелыми. ИМХО, нужно не под старые методики бинарных паттернов подстраивать сеть, а выделять реальные, нечеткие, на полных данных.
От начального до среднего уровня: Переменные (III) От начального до среднего уровня: Переменные (III)
Сегодня мы рассмотрим, как использовать переменные и константы, предопределенные языком MQL5. Кроме того, мы проанализируем еще один особый тип переменных: функции. Умение правильно работать с этими переменными может определить разницу между работающим и неработающим приложением. Для того, чтобы понять представленное здесь, необходимо разобраться с материалом, который был рассмотрен в предыдущих статьях.
Введение в MQL5 (Часть 8): Руководство для начинающих по созданию советников (II) Введение в MQL5 (Часть 8): Руководство для начинающих по созданию советников (II)
В этой статье рассматриваются частые вопросы, которые начинающие программисты задают на форуме MQL5. Также демонстрируются практические решения. Мы научимся совершать основные действия: покупку и продажу, получение цен свечей, а также управление торговыми аспектами, включая торговые лимиты, периоды и пороговые значения прибыли/убытка. В статье представлены пошаговые инструкции, которые помогут вам лучше понять и реализовать обсуждаемые концепции на MQL5.
Нейросети в трейдинге: Контекстно-зависимое обучение, дополненное памятью (MacroHFT) Нейросети в трейдинге: Контекстно-зависимое обучение, дополненное памятью (MacroHFT)
Предлагаю познакомиться с фреймворком MacroHFT, который применяет контекстно зависимое обучение с подкреплением и память, для улучшения решений в высокочастотной торговле криптовалютами, используя макроэкономические данные и адаптивные агенты.
Функции активации нейронов при обучении: ключ к быстрой сходимости? Функции активации нейронов при обучении: ключ к быстрой сходимости?
В данной работе представлено исследование взаимодействия различных функций активации с алгоритмами оптимизации в контексте обучения нейронных сетей. Особое внимание уделяется сравнению классического ADAM и его популяционной версии при работе с широким спектром функций активации, включая осциллирующие функции ACON и Snake. Используя минималистичную архитектуру MLP (1-1-1) и единичный обучающий пример, производится изоляция влияния функций активации на процесс оптимизации от других факторов. Предложен подход к контролю весов сети через границы функций активации и механизма отражения весов, что позволяет избежать проблем с насыщением и застоем в обучении.