All streams
Search
Write a publication
Pull to refresh

Comments 52

Было интересно почитать. Вопрос с чем связано такой маленький бэктест в 3 месяца, а не 1 год например? Ведь чем больше временной интервал, тем точнее полученный результат

Спасибо за отличный вопрос.

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

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

Во-вторых, есть объективные ограничения по данным. Например, на дату 2025-08-05 на Binance Futures активно торгуются 505 тикеров. Если же взять потенциальный годовой бэктест от последней даты сбора (2025-06-01), то на 2024-06-01 в торговле было лишь 247 тикеров. Это означает, что при расширении окна мы теряем до 51% информации — не в объёме котировок, а в рыночном разнообразии: многие современные паттерны просто отсутствуют, особенно для новых листингов.

Для сравнения, на классических рынках вроде NASDAQ, где торгуются ~3500 тикеров и существуют минутные данные как минимум с 2008 года, годовой бэктест более уместен. Но даже в этом случае всё необходимо проверять эмпирически. На практике (в том числе по моему опыту) перенос подходов, работающих на фондовых рынках, в крипту часто приводит к провалу — условия слишком разные.

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

Я правильно понял, что трейдинг ведётся по одной переменной? То есть берутся в расчёт данные по одному инструменту? Не пытались улучшить результат за счёт анализа взаимозависимых активов? (ну, типа, когда ставка по доллару растёт, доллар растёт, а золото падает, а когда медь растёт, то акции растут и пр.)

Спасибо, это действительно хороший вопрос.

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

Тем не менее, идея анализа кросс-активных зависимостей (intermarket signals) интересна и перспективна. Архитектура проекта модульная, и расширение входного пространства дополнительными каналами — вопрос конфигурации. Можно, например, включать признаки от коррелированных тикеров, индексов, ставок и других рыночных индикаторов. Но важно учитывать: каждый новый источник — это рост размерности и потенциального шума, что требует строгого контроля качества и борьбы с утечками.

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

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

Ваще отлично! не часто так все подробно и с исходниками!

OHLCV -- конечно нет! это все МЛщики мучают.. просто в топку, это нормально не работает.

Советую замутить хотя бы анализ по пробоям/отбоем уровней или какие-нить еще схожие именно "визуал"-данные.

Благодарю за отзыв и за совет.

Что касается OHLCV — согласен, сами по себе эти данные являются только низкоуровневым представлением рыночной динамики. В отрыве от контекста они действительно слабо информативны.

Однако в данной системе они проходят через мощную цепочку преобразований: логарифмирование, нормализация, агрегация контекста, архитектурная фильтрация через CNN + dueling Q-head. То есть модель не опирается напрямую на "сырые свечки", а учится выделять стабильные поведенческие паттерны.

Тем не менее, направление, которое вы упомянули — визуальные или структурные сигналы, — очень перспективно. Их вполне можно внедрить как отдельный канал признаков.

Проходил уже все это, в конечном варианте было 3 сетки, dreamer фантазировал на 200 тиков вперёд, A2C ценность сделки и обоснование и сетка которая наблюдала за двумя этими сетками, и на основании их действий обучалась и подсказывала. В итоге лучшее решение что приняла модель - не торговать вообще.

Благодарю за честный комментарий — опыт, которым вы делитесь, очень показателен.

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

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

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

Шикарно! Как раз в процессе написания подобного бота с применением RL, но планирую прикрутить также анализ новостей. Как считаете, достаточно будетанализировать тональность или прям текст векторизовать и подавать ему в качестве эмбеддингов?

Спасибо, рад, что статья оказалась в тему.

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

Для этого подойдут модели семейства sentence-transformers или специализированные финверсии типа FinBERT. Ниже минимальный пример извлечения эмбеддингов с mean pooling:

from transformers import AutoTokenizer, AutoModel
import torch

# Загружаем компактную модель из семейства Sentence Transformers
model_name = "sentence-transformers/all-MiniLM-L6-v2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# Текстовая новость (можно брать заголовок или краткое содержание)
text = "Bitcoin breaks $70K amid macroeconomic pressure."

# Токенизация с усечением и паддингом
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)

with torch.no_grad():
    outputs = model(**inputs)
    attention_mask = inputs['attention_mask']
    last_hidden = outputs.last_hidden_state

    # Вычисление эмбеддинга через среднее по токенам с учётом attention mask
    embeddings = (last_hidden * attention_mask.unsqueeze(-1)).sum(1) / attention_mask.sum(1, keepdim=True)

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

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

Прикольно, то есть ставим на комп и зарабатываем или как?

Нет, ставишь на комп и допиливаешь в направлении постепенного перехода от улучшенного DQN к policy-based или модель-ориентированным методам, сопровождаемый:

-расширением данных и признаков,

-учётом неопределённости и риска в награде,

-реальным stream + execution контуром.

Таким образом надо:

1. Переписать replay-буфер: PER + β schedule; добавить n-step sampling.

2. Заменить ε-жадность на NoisyLinear; переобучить базовую сеть.

3. Докрутить Optuna для n-step, γ, β, noisy_σ; 200 trials достаточно.

4. Интегрировать risk-penalty в reward и пересчитать метрики.

5. Ветка A (механика): Transformer-encoder вместо CNN, затем C51.

6. Ветка B (action space): прототип SAC (continuous size), backtest 2025-03—06.

7. Развёртывание dry-run на Binance Testnet с real-time stream.

8. Собрать baseline Dreamer на тех же данных; сравнить sample-efficiency.

9. Выбрать победителя → live ключи под строгими лимитами.

Хороший вопрос.

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

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

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

зарегался чтоб плюсануть, оч круто !

Спасибо, ценю ваш отклик. Рад, что проект оказался вам интересен.

Прошу пояснить вот этот пример:

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

Т е в реальной торговле в этой позиции будет не прибыль, а убыток.

Именно об этом я писал ранее.

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

Спасибо за вопрос — разберу механику на примере этого графика.

Вертикальная пунктирная линия — это момент фиксации высоковолатильного импульса по процедуре, описанной в разделе «Данные» статьи. До этого момента агент не торгует.

В проекте используются минутные котировки OHLCV. Полная свеча за минуту t загружается сразу после её закрытия.

Дальнейший алгоритм:

  1. Минута t закрылась → в систему поступает её OHLCV.

  2. Формируется входное состояние из последних N минут истории (в зависимости от конфигурации).

  3. Агент принимает решение.

  4. При открытии позиции исполнение моделируется по цене закрытия этой же минуты t с учётом параметров среды: slippage ±0.05% и taker-fee 0.04%.

В данном примере сигнал был зафиксирован 2025-01-10 08:56 UTC (индекс 10 на оси времени). Вход в позицию происходит по цене закрытия минуты возникновения сигнала ≈ 0.65, скорректированной на проскальзывание и комиссию. Дальнейшее движение до ~0.78 относится уже к последующим минутам удержания позиции.

Таким образом, агент не «покупает по цене из прошлого» и не ждёт ещё одну минуту: решение и исполнение синхронизированы с моментом появления полной минутной котировки. Именно эти цены учитываются в расчёте PnL и визуализациях действий агента.

Из графика не видно, сколько свечей от времени 10 когда цена=0.65 до значения цены 0.78. Если 1 свеча, то будет явно убыток.

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

Цена совершенной сделки - это всегда цена в прошлом.

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

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

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

Задание slippage ±0.05% и taker-fee 0.04%. никак не связано ни со спредом в стакане заявок, ни с движением цены на следующей свече. Т е это параметры с "потолка".

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

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

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

-------------------

Кроме того, если рынок двигается сильно,как в вашем случае, то лимитная заявка по цене предыдущей сделки часто не будет совершаться очень долго, так как цена в заявках быстро изменится. Что тогда будет делать Ваш агент?

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

В проекте, описанном выше, используется модель исполнения, приближённая к рыночным ордерам: вход осуществляется по цене закрытия минуты, на которой зафиксирован сигнал, с учётом заданного проскальзывания (±0.05%) и комиссии (0.04%). Это стандартная практика в исследовательских backtest-платформах, где цель — проверка стратегии принятия решений на минутном таймфрейме, а не симуляция HFT с тиковой глубиной стакана.

Агент не “покупает по цене из прошлого” — момент принятия решения и входа синхронизированы. Проскальзывание моделирует разрыв между последней сделкой и лучшим bid/ask на момент исполнения, что даёт усреднённо реалистичный результат без необходимости хранения L2/L3 данных.

В реальном исполнении система использует отдельный Execution Layer, который работает по логике, исключающей описанные вами риски:

  • Подключение к Binance Futures WebSocket через ThreadedWebsocketManager для получения ценовых данных в режиме высокой частоты.

  • Фиксация решения агента о входе → поиск лучшей цены в стакане для лимитного ордера.

  • Если лимитный ордер не исполняется в заданный временной интервал — ордер отменяется (_clean_orders()), чтобы избежать входа по невыгодной цене.

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

Пример кода Execution Layer:

# Инициализация WebSocket
self._web_socket = ThreadedWebsocketManager(
  api_key=cfg.BINANCE_API, 
  api_secret=cfg.BINANCE_SECRET, 
  testnet=cfg.USE_TESTNET
)

# Подписка на поток котировок
self._web_socket.start_kline_futures_socket(
    callback=self._get_update_event_by_stream(ticker),
    symbol=ticker_name,
    interval=KLINE_INTERVAL
)

# Логика ордера
# ...
self._clean_orders()
position_values, prices = self._get_position_values_and_prices(req_pos)
# …выставление нового лимитного ордера по лучшей цене

Таким образом:

  • В backtest мы моделируем немедленное исполнение по рынку с параметрами, отражающими усреднённые издержки Binance Futures.

  • В боевой системе Execution Layer обеспечивает динамическое размещение и отзыв лимитных ордеров в реальном времени, либо немедленное исполнение по рынку.

  • Проблема «зависших» ордеров или исполнения по цене, которая уже ушла, в системе не существует.

Это полноценный инженерный контур исполнения, а не теоретическая модель.

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

Благодарю за ответ.

1) Какая задержка обмена данными с биржей у Вас в реальных торгах?

2) Есть ли сравнение реально полученной прибыли с прибылью, рассчитанной по эмуляции выставления заявки по цене закрытия сделки.

3) Как часто совершаются сделки на бирже?

Тоже зарегался чтобы плюсануть, спасибо.
Очень круто!

Можете пояснить,

для тестирования Вы тоже выделяете участки с импульсами или используете все данные с биржи?

С импульсами. Можно посмотреть в test_data.npz

Забавная работа, тоже сейчас занимаюсь трейд системой (только на технический идентификаторах), прибыльность на репрезентативных выборках 5-11% за квартал, только торги идут на спокойном рынке (волатильность ~0.002). Не знаю на счёт стержня системы, но как вариант для максимизации прибыли выглядит очень даже хорошо, спасибо за статью

Похоже Вы считаете среднее и отклонение по всему эпизоду (150), а потом с их помощью нормализуете каждое наблюдение (90 или 30). То есть происходит утечка из будущего.
И к тому же при реальной торговле таких данных естественно не получить.

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

Коротко: «утечки из будущего» -> нет.

Я не делаю сессионную нормализацию по окну 150 минут. Per-канальные mean/std считаются один раз только по train-сету, фиксируются и применяются к любому наблюдению на val / test / backtest / онлайне. Каждое состояние в момент времени t строится каузально из последних N минут до t; будущие данные в расчёт не попадают.

Разберем по пунктам:

  • Никакой “150-минутной” z-нормализации. Статистики нормализации не считаются «по текущей сессии» и не знают о её пост-сигнальной части. Это базовый продакшн-паттерн: единое масштабирование, замороженное на train.

  • Кауза соблюдена. Сначала формирую историческое окно до t, затем apply_normalization(...) применяет train-статистики, и только после этого добавляются экстра данные (позиция, unrealized PnL, elapsed/remaining time, one-hot история действий).

  • “В реальной торговле таких данных не получить”. Это неверная предпосылка, как раз наоборот: в онлайне используются предварительно посчитанные train-статистики, а не «динамические» stats по текущему окну. Это исключает дрейф масштабирования и исключает leakage.

  • Про “отрезание первых 90 минут”. Резать ни в коем случае ничего не надо: stats не считаются по индивидуальному 150-минутному эпизоду в принципе. Даже если искусственно пересчитать mean/std только по первым 90 минутам каждой сессии и перегнать оценку, разница в метриках окажется в пределах статистического шума; выигрыша от такой «локальной» нормализации нет, а стабильность хуже.

  • RL ≠ SL. Здесь нет таргетов как в обучение с учителем (возможно именно этот момент вашего опыта в SL пробудил в вас сомнения), которые можно «подсмотреть». Агент видит только историю до t, получает reward каузально по шагам, а нормализация выполняет сугубо инженерную функцию — стабилизацию входов под фиксированные train-масштабы.

Дополнительно: для онлайна именно такая схема (фиксированные train‑stats) и применяется в индустрии: она устойчива и корректно переносится в продакшн‑поток без «онлайн‑оценок будущего».

Спасибо за подробный и развернутый ответ!!!

Я уже пересчитал со статс по 90 и, действительно, результат практически не отличается.
Возник еще один вопрос. В трейне по одной и той же торговой паре конечно же много разных эпизодов и по каждому считается статс. Какой из них (или среднее) потом будет использоваться в реале для этой торговой пары?
Спасибо

Для удобства добавил в репозиторий утилиту для просмотра top-N лучших триалов Optuna (не только одного победителя). Это помогает выбирать конфигурацию под свой риск-профиль (например, меньше сделок, но выше точность).

Команда:

python get_info_from_optuna.py configs/alpha.py --n-best-trials 10

Все результаты автоматически сохраняются в той же папке рядом с файлом исследования: output/cfg_name/optuna_cfg_optimization_results/ —> как best_<N>_trials_<metric>.txt и .csv.

Интересная работа, но возникает вопрос: откуда берутся все ваши пороговые значения/выборы констант. Как по мне это совершенно не очевидный выбор, что с выбором порога высокой волатильности, что выбор сессионного окна, что даже выбор пороговых значений сигналов. Была ли какая-то оптимизация/оценки сверху данных параметров или это не более чем "прикинул на глаз"?

Спасибо за вопрос. Выбранные параметры в проекте — это не жёсткие константы, а демонстрационный пресет. Проект изначально сделан так, чтобы вы могли строить свой data layer, подставлять любые значения и тестировать разные комбинации.

Направления для экспериментов:

  1. Порог волатильности: ниже — больше сигналов и выше чувствительность, но больше шума; выше — сигналы реже, но чище.

  2. Окно волатильности: короче — реакция быстрее, но больше ложных входов; длиннее — меньше шум, но медленнее реакция.

  3. Длина сессии: короче — чаще сброс контекста; длиннее — больше истории, полезно для трендов.

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

славно пролив отработал, прям как человек )))

крутая все ж штуковина отработать такой сквиз это прям оч хорошо.

надеюсь там реальная торговля с депом 10$к

/ слежу за ботом в телеге ) /

Вот у Вас в задачах проекта значится:
"Эффективно выявлять скрытые закономерности из исторических данных;"
Очень интересно.
И что-то далее про это ни слова...

Отличная, структурированная работа!

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

Возникло желание воспроизвести эксперимент немного на других данных, но в репозитории нет кода с логикой отбора этих локальных окон, или я не заметил? Из описания не совсем понятно, входят ли 10 минут высокой волатильности в 90 минут отсутствия сильных движений.

Спасибо за вопрос!

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


Что касается отбора локальных окон — в проекте предусмотрена гибкая конфигурация: можно менять длину исторического контекста, длительность торговой сессии и набор используемых каналов данных.
Однако сам процесс первичного формирования датасетов (например, под другие биржи или иные условия) действительно остаётся на стороне исследователя: описанная в статье логика легко переносится и может быть реализована в отдельном data layer.

Теперь про 10-минутное окно. Оно не входит в предшествующее 90-минутное окно «тишины». Алгоритм работает так:

  1. Сначала скользящим окном ищется 10-минутный отрезок с изменением цены ≥ 5%.

  2. Если условие выполняется — мы берём 90 минут, предшествующих началу этого окна, и проверяем их на отсутствие сильных движений (контрастность).

  3. Только если обе проверки пройдены — событие фиксируется как сигнал, и формируется финальная 150-минутная сессия (90 мин до + 60 мин после).

Псевдокод:

for each index t in close_prices:
    # проверяем 10-минутное окно на сильный импульс
    change = (Close[t+10] - Close[t]) / Close[t]
    if |change| >= MinAbsChange:

        # считаем среднюю волатильность за 90 минут до начала этого окна
        avg_volatility = mean( |Close[i+10] - Close[i]| / Close[i] for i in [t-90 .. t] )

        if avg_volatility * ContrastCoeff < |change|:
            record signal at time (t+10)

Таким образом, итоговое окно состоит из 90 минут контекста (без резких движений), за которыми следует 60-минутная торговая сессия, начинающаяся сразу после обнаруженного импульса.

В предыдущем ответе в псевдокоде при расчёте волатильности была опечатка.
Правильный вариант:

avg_volatility = mean( |Close[i+10] - Close[i]| / Close[i] for i in [t-90 .. t-10] )

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

Отличная статья с исследовательским подходом! Почерпнул много интересного


Любопытно, делали ли исследование по влиянию других возможных фич на качество предсказаний (RSI, скользящие средние и пр.)? Да и роль текущих в качестве предсказаний. Стоит ли вообще на такой feature engineering тратить время, учитывая, что нейронка и так обладает высокой обобщающей способностью?

Не совсем понял способ полученияvolume_weighted_averageи что оно значит? Решил посмотреть тестовые данные, и понял, что это что-то сильно связанное с ценой.

from data import data_utils as du

res = du.load_npz_dataset("data/test_data.npz", "test", "discovery/plots")
[r[1] for r in res][0][1]
Out: array([5.150000e-02, 5.156000e-02, 5.143000e-02, 5.125000e-02,        
5.148000e-02, 1.629173e+06, 5.860000e+02])


Как понимаю, бот работает, потому вопрос про практику: мониторите ли широкий список активов? Судя по отбору данных для тренировки, прибыль приносит только работа на волатильном рынке. Осуществляете ли какую-то селекцию для налюдения до начала торгов или подключения наблюдения во время? Если же мониторите все, не появляется ли ложных срабатываний?

Благодарю за отзыв и отличный вопрос.

Что касается влияния дополнительных фич — на практике классические индикаторы теханализа (RSI, скользящие и т.п.), построенные на основе тех же ценовых рядов, редко дают прирост качества. Современные нейросетевые архитектуры уже умеют извлекать локальные и глобальные паттерны непосредственно из «сырых» OHLCV-данных. Тем не менее, я сторонник экспериментальной проверки: при грамотном комбинировании технических индикаторов и feature engineering вполне возможен прирост точности или устойчивости. Важно не ограничиваться минутными свечами, а рассматривать другие источники данных — например тиковые потоки.

Что касается volume_weighted_average — здесь речь идёт о VWAP (Volume Weighted Average Price). В идеальном случае это считается на тиковом уровне по формуле:

VWAP = \frac{\sum (price \times volume)}{\sum volume}

В нашем случае доступны только минутные OHLCV-свечи, поэтому применяется упрощённый вариант — усреднение по (Open, High, Low, Close). Это приближение, которое позволяет хотя бы частично сохранить информацию о «средней цене сделки» в рамках свечи. Если у вас есть доступ к тиковым данным, всегда стоит использовать точный расчёт.

По поводу мониторинга: агент онлайн отслеживает все активные фьючерсные контракты Binance Futures со статусом TRADING. На текущий момент (30.08.2025) это 521 тикер. Действительно, прибыльные возможности возникают преимущественно на волатильных рынках, но фильтрация делается уже на этапе обработки сигналов. Ошибочные входы случаются — их стоимость контролируется встроенным риск-менеджментом. Это обязательный элемент любой продакшен-системы: ошибки неизбежны, но важно, чтобы они стоили кратно меньше, чем приносят прибыльные сделки.

Спасибо за исчерпывающий ответ!

Есть ли где доступ к тиковым данным по торгующимся активам офлайн, или их надо собирать и хранить уже внутри своей системы, снимая реальные онлайн данные?

И вопрос вне рамок статьи, если позволите:

Про глубину стакана для просмотра: можно выбрать от 1 до 50 + объем сделок в тик. Насколько эти параметры важны для качества работы системы?

Binance публикует офлайн-архивы сделок (trades, aggTrades) и снимков стакана (bookDepth) на data.binance.vision. Эти данные можно скачивать и использовать для исследований.

Что касается стакана.
Глубина заявок критична в HFT и субсекундных стратегиях, где важна микроструктура рынка. Для минутных моделей, как описано в статье, достаточно OHLCV и при желании тиковых данных, а расширенный стакан добавляет сложность и не всегда даёт прирост качества.

Спасибо за интересную статью с подробным описанием и исходным кодом. Есть ли где то пример кода подготовки данных для обучения?

Спасибо за интерес к проекту!

Подготовка данных для обучения в данном случае не является сложной задачей — вся логика и структура уже подробно описаны в статье. Реализацию data layer я оставил на усмотрение исследователей, чтобы было пространство для творчества.
Вы можете написать свой data layer под разные площадки (Binance, Bybit, OKX и др.) и тем самым реально прокачать свои навыки.

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

Продолжаю следить за ботом в телеге, торгует бодренько )

Вопрос от начинающих ) Мы обучили агента на Всех монетах сразу и он выработал какие то свои паттерны поведения на ВСЕХ монетах сразу или он 'знает' как какая монета себя вела и принимает решение имнено по этой конкретной монете, те он обучился на каждой конкретной монете и все ходы выходы маркетмейкера ему известны ))) или решение 'берется' из всей кучи знаний ? Надеюсь смысл понятен )

Спасибо за интересный вопрос!

Агент действительно не учится на отдельных тикерах, а воспринимает весь рынок целиком. Это даёт ему возможность усваивать более сложные и универсальные паттерны поведения, которые затем применяются к любому активу. Когда появляется новый сигнал, агент не «знает» историю конкретной монеты — он опирается на свой общий опыт, накопленный на данных всего рынка Binance Futures.

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

Спасибо, за ответ !

НО вряд ли опытный трейдер начнет торговать, что то совсем не знакомое .... )

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

и да мало что понятно , но очень интересно !

Добрый день!

Спасибо за очень интересную и качественную статью. Хочу задать Вам вопрос. Откуда Вы брали количество сделок num_trades? В частности для бэктеста. Я сверил с тем, что отдаёт binance, оно отличается. Совсем частный случай, пара SXT/USDT данные за 5 мая 13:46. В binance правда сдвиг по времени, на сайте эта свеча (O=0.272410, H=0.273270, L=0.271840, C=0.272130) была в 17:45 по Мск. Но если вытащить через API, то количество сделок там 224 (OHLC совпадает). А у Вас в бэктестовых данных 464. Подозрение что это не завершенные сделки, а все из стакана по какой-то глубине. Или я как-то не так смотрю?

Отвечу сам себе: ccxt агрегирует сделки, поэтому значение было разным.

Спасибо за внимание к статье и отличный вопрос!
Хочу вас отдельно похвалить — вы сами нашли ответ на ваш вопрос. Это действительно бесценный опыт, когда человек самостоятельно разбирается в технических деталях, понимание остаётся с ним навсегда и формирует настоящий исследовательский навык.

Вы абсолютно правы, CCXT в некоторых режимах агрегирует сделки, из-за чего поле num_trades может отличаться от нативных значений Binance. Поэтому для точных исследований я всегда рекомендую использовать оригинальные данные напрямую из Binance Futures API. Один из самых удобных способов — через библиотеку python-binance (https://github.com/sammchardy/python-binance, устанавливается через pip install python-binance). Важно учитывать, что библиотека неофициальная (Binance её напрямую не поддерживает), но это самый используемый и функциональный инструмент в сообществе.

Минимальный пример получения минутных свечей по фьючерсам:

import datetime as dt
from binance.client import Client, BaseClient
from binance.enums import HistoricalKlinesType

symbol = "*USDT"
start_dt = dt.datetime(..., tzinfo=dt.timezone.utc)
end_dt = dt.datetime(..., tzinfo=dt.timezone.utc)

klines_type = HistoricalKlinesType.FUTURES
interval = BaseClient.KLINE_INTERVAL_1MINUTE

client = Client()

data = client.futures_klines(
    klines_type=klines_type,
    symbol=symbol,
    interval=interval,
    startTime=int(start_dt.timestamp() * 1000),
    endTime=int(end_dt.timestamp() * 1000),
)

client.close_connection()

Чтобы окончательно снять сомнения, я ещё раз вручную проверил через официальный API Binance и сравнил с локальной базой проекта. Все данные полностью совпали. Для прозрачности привожу три последовательные котировки (14:45, 14:46, 14:47 UTC), включая ту, которая вызвала вопрос:

=== Raw quotes ===

[1746456240000, '0.2720500', '0.2728000', '0.2712000', '0.2726500', '110004', 1746456299999, '29917.2349400', 557, '51011', '13887.8376800', '0']
[1746456300000, '0.2724100', '0.2732700', '0.2718400', '0.2721300', '99912', 1746456359999, '27229.3868100', 464, '41200', '11236.2752000', '0']
[1746456360000, '0.2723500', '0.2726500', '0.2719900', '0.2724700', '49486', 1746456419999, '13476.6411000', 349, '24452', '6662.0162200', '0']

=== Human-readable ===

[Date: 2025-05-05  Time: 14:45:00] Open=0.272050, High=0.272800, Low=0.271200, Close=0.272650, Volume=110004.00, Num Trades=557
[Date: 2025-05-05  Time: 14:46:00] Open=0.272410, High=0.273270, Low=0.271840, Close=0.272130, Volume=99912.00, Num Trades=464
[Date: 2025-05-05  Time: 14:47:00] Open=0.272350, High=0.272650, Low=0.271990, Close=0.272470, Volume=49486.00, Num Trades=349

Таким образом, в бэктесте использовались именно «чистые» данные Binance Futures API, без сторонней агрегации.

Спасибо за подробный ответ!

Нашёл ещё один непонятный мне момент, а именно как у Вас вычисляется размер комиссии? Поясню.

В конфиге указано, что уровень комиссии задаётся в базовых пунктах и равен 0,0004, что примерно соответствует комиссии binance (там 0.05%, или 0,0005 bps для обычных пользователей для тейк и в 2 раза меньше для мейк):

transaction_fee: float = 0,0004

# 0.01% – 0.05% (1–5 bps -> basis points) 1 bps = 0.01% = 0.0001

А дальше в коде вроде как забывается про то что это bps а не процент (или я не увидел, где это преобразуется), и везде комиссия считается по формулам

fee = (entry + exit) * position_size * cfg.market.transaction_fee

pnl_change -= exec_price volume * self.transaction_fee

Как я понимаю, здесь нужно ещё умножить на 100... Ну или поставить в конфиге не 0,0005, а 0,05.

Но когда меняешь на 0,05, агент отказывается совершать сделки после обучения: видимо считает, что это самая правильная позиция :)

Здравствуйте!
В конфиге комиссия задаётся в долях, а не в процентах. Значение 0.0004 = 0.04% = 4 bps, что соответствует стандартным комиссиям Binance. Дополнительное умножение на 100 не требуется. Когда вы подставляете 0.05, то это уже 5% комиссии за сделку, что не имеет никакого отношения к реальности и естественно полностью убивает торговлю.

Добрый день!

Спасибо, там действительно было всё правильно. Хочу ещё обратить Ваше внимание на один момент в алгоритме бэктеста.
В цикле по grouped_backtest_data Вы берёте только первые тикеры по количеству слотов selected_signals = signals[:free_slots]
Тут проблема: если агент/окружение отклонили сделки, то остальные тикеры уже не проверяются. И тут поведение бэктеста будет достаточно ощутимо отличаться от лайва, т.к. по остальным событиям сделки в лайве могут открыться.
Например, в signal_dt = 2025-03-27 17:28 в signals 5 тикеров.
Я в локальной версии у себя внёс правки, график pnl поменялся, как раз в конце марта появились просадки...

А вообще, хочу сказать Вам спасибо! Очень интересно копать и разбираться)

Хочу ещё спросить вдогонку: как можно избежать, не знаю как лучше выразиться, "переобучения оптуной"?
Я скачал с binance vision данные с начала 2025г по сентябрь, сделал из них npz (проверил, одинаковые события совпадают с Вашим датасетом).

Запускаю optuna на Вашем backetest_data, потом backtest_engine. Всё хорошо, агент торгует, pnl растёт.
Запускаю бэктест на своих данных с января по сентябрь. С января по март сделок нет, с марта по июнь огонь и рост, с июня по сентябрь сделок почти нет, просадка. Явно optuna перестаралась и привела к подгонке параметров под конкретные условия.
Вы с таким моментом не сталкивались?

Sign up to leave a comment.

Articles