Блог им. RomellaAkumov
В классическом алготрейдинге рынок часто моделируется как временной ряд: индикаторы, скользящие средние, осцилляторы. Аукционная теория рассматривает рынок иначе — как процесс распределения объёма по ценовым уровням, где цена ищет баланс между спросом и предложением.
Ключевым элементом такого подхода является Volume Profile, а именно Point of Control (POC) — уровень цены, на котором за выбранный период был проторгован максимальный объём. В терминах аукционной теории POC соответствует зоне максимального согласия участников рынка.
В статье рассматривается создание алгоритмического торгового бота, основанного на реакции цены относительно:
POC
Value Area High (VAH)
Value Area Low (VAL)
В качестве основы используется Python‑скрипт back.py, предназначенный для параметрического бэктеста стратегии.
Все скрипты из статьи я выложил на github для вашего удобства.
Скрипт логически разделён на несколько уровней:
Загрузка и подготовка рыночных данных (Binance Futures)
Расчёт Volume Profile и POC
Генерация торговых сигналов
Рыночная структура (swing‑экстремумы)
Управление риском (SL / TP)
Симуляция сделок
Анализ результатов
Такое разделение важно: в дальнейшем те же блоки будут переиспользованы в real‑time боте практически без изменений.
Ключевая функция стратегии — calculate_volume_profile.
<code>def calculate_volume_profile(df, bins=100, buffer_ratio=0.05): price_min = df['low'].min() price_max = df['high'].max() buffer = (price_max - price_min) * buffer_ratio price_min -= buffer price_max += buffer price_bins = np.linspace(price_min, price_max, bins) bin_centers = (price_bins[:-1] + price_bins[1:]) / 2 volume_profile = np.zeros(bins - 1)</code>
Берётся диапазон цен за весь доступный период
Добавляется буфер, чтобы исключить краевые искажения
Диапазон разбивается на фиксированное число ценовых бинов
Далее каждая свеча распределяет свой объём по пересекаемым ценовым уровням:
<code>for i in range(len(df)): low = df['low'].iloc[i] high = df['high'].iloc[i] vol = df['volume'].iloc[i] bin_indices = np.digitize([low, high], price_bins) - 1 start_bin = max(0, min(bin_indices)) end_bin = min(bins - 2, max(bin_indices)) bin_vol = vol / (end_bin - start_bin + 1) for b in range(start_bin, end_bin + 1): volume_profile[b] += bin_vol</code>
Таким образом формируется реальный объёмный профиль
<code>poc_index = np.argmax(volume_profile) poc = bin_centers[poc_index]</code>
Value Area считается классическим способом — через накопление 68% объёма от POC наружу.
Логика входа реализована в generate_signals().
<code>if (prev_close <= va_low * (1 + threshold) and close > va_low): signals.iloc[i] = 'Buy'</code>
цена находилась в зоне дисбаланса ниже VAL
затем вернулась внутрь value area
рынок «принял» цену обратно
Short-сигнал зеркален относительно VAH.
<code>if vol < avg_vol_i * min_volume_ratio: continue</code>
Сигнал игнорируется, если возврат в value происходит без участия объёма.
Для управления сделкой используется структура рынка.
<code>def detect_internal_swings(df): if low[i] < low[i-1] and low[i] < low[i+1]: swing_low[i] = True</code>
Используются для:
First Trouble Area — первая зона, в которой можно получить реакцию
ближних целей
<code>def detect_external_swings(df, win=20): if low[i] == low[L:R].min(): swing_low[i] = True</code>
Используются как цели ликвидности и структурные стоп-лоссы
<code>def calculate_sl(df, entry_idx, entry_price, direction):
if direction == 'long':
return last_swing_low</code>Логика:
SL ставится за ближайший структурный экстремум
Если экстремума нет — fallback на ATR
Это принципиально отличает стратегию от индикаторных систем.
<code>if tp_mode == 'liquidity': tp = tp_liquidity(df, i, direction)</code>
TP может быть:
фиксированным по волатильности
по цели ликвидности
по первой проблемной зоне
Каждая сделка проходит фильтр, чтобы риск-ревард был более 1. В ином случае эта сделка просто не выгодна.
<code>for j in range(i + 1, i + max_bars):
if bar_low <= sl:
exit_price = sl</code>Модель исполнения:
проверка SL → TP
без подглядывания в будущее
одна позиция = одна сделка
Комиссии учитываются с двух сторон.
<code>winrate = len(wins) / trades_cnt * 100 profit_factor = wins.sum() / abs(losses.sum())</code>
Также рассчитываются:
Sharpe Ratio (annualized)
Max Drawdown
Net PnL
Метрики считаются по сделкам, а не по свечам.
Используется реальное распределение объёма
Входы строятся от логики аукциона, а не индикаторов
Риск контролируется структурой рынка
Бэктест максимально приближен к реальному исполнению
Запустив бектест, мы получим ряд данных. Для примера:
<code>Trades: 54, Net PnL (USD): 937.03, Winrate: 57.41%, MaxDD: -3.85 Result: Trades=54, Net PnL=937.03, Winrate=57.41% Trades: 45, Net PnL (USD): 937.38, Winrate: 60.00%, MaxDD: -3.36 Result: Trades=45, Net PnL=937.38, Winrate=60.00% Result: Trades=0, Net PnL=0.00, Winrate=0.00%</code>
Здесь мы можем увидеть, что есть действительно неплохая стратегия с винрейтом в 60%. Она дала нам 937$ прибыли при входе на 0.002 BTC в каждой сделке.
Параметры следующие:
{'bins': 100, 'threshold': 0.003, 'tp_mode': 'liquidity', 'atr_coeff': 2, 'min_tp_pct': 0.003, 'min_volume_ratio': 1.2, 'require_trend_confirmation': False}
В real-time боте будем использовать именно их.
Часть 2. Реализация real-time ботаВ этой части не повторяется логика бэктеста и не объясняются основы стратегии.
Задача real-time реализации — одна:
корректно и без искажений перенести готовую стратегию в живой рынок.
Ключевой принцип — жёсткое разделение ответственности:
Binance (данные) → Strategy Engine → BingX (исполнение)
Binance используется исключительно как источник свечей
BingX — только как торговая площадка, так как имеет меньшие комисии
Это позволяет избежать логических расхождений и упрощает отладку.
Для грамотной работы нам необходим bingX client — для удобства я написал SDK библиотеку со всеми функциями для этой биржи. Это позволить не переписывать сложные функции с подписями и запросами для каждой стратегии, а использовать один скрипт. Он вместе со стратегией хранится на github.
Инициализируем библиотеку:
<code>bingx_client = BingxClient( api_key=API_KEY, api_secret=API_SECRET, symbol="BTCUSDT" )</code>
Сделки будем открывать с помощью функции:
<code>def place_market_order(self, side: str, qty: float, symbol: str = None, stop: float = None, tp: float = None):</code>
<code>df = fetch_klines_paged( SYMBOL, INTERVAL, total_bars=2000, client=binance_client )</code>
Особенности:
подгружается достаточно длинная история для корректного Volume Profile
данные каждый цикл пересобираются заново
используются только закрытые свечи
Это дороже по API, но гарантирует идентичность логики с бэктестом.
<code>df = detect_internal_swings(df) df = detect_external_swings(df) df = calculate_atr(df) df = generate_signals(df, params)</code>
Важно:
порядок вызовов полностью совпадает с бэктестом
никаких оптимизаций или «ускорений» не используется
Любое отклонение здесь приводит к несовпадению сигналов.
if df['long_signal'].iloc[-2]:
Бот:
не реагирует на текущую формирующуюся свечу
не пересчитывает POC intra-bar
Это сознательный компромисс:
меньше сделок
но полное соответствие backtest → live
<code>def open_order_bingx(direction, qty, entry_idx, df): entry_price = float(df.iloc[entry_idx]['close']) sl = calculate_sl(...) tp = tp_liquidity(...) or tp_fta(...)</code>
Все параметры сделки:
entry
stop-loss
take-profit
рассчитываются до отправки ордера.
Биржа не принимает решений — она только исполняет.
<code>threading.Thread( target=open_order_bingx, args=(direction, QTY, entry_idx, df) ).start()</code>
Это решает проблему прерывания основного цикла, это не является чем-то основным, но всё же делает логику безопаснее.
<code>while True: run_live_bot(params) time.sleep(60)</code>
Минимализм цикла — осознанный выбор:
нет хранения позиций
нет локального state
нет логики сопровождения
Позиция живёт на стороне биржи.
1. Drift между backtest и live
одинаковый код
одинаковый порядок расчётов
2. Рассинхронизация свечей
только закрытые бары
3. API latency
threading
4. Ошибка исполнения
торговая логика изолирована от AP
Этот real-time бот — не отдельная система, а прямое продолжение бэктест-движка.
Если стратегия работает в истории, она будет вести себя так же и в реальном рынке — с учётом комиссии, проскальзывания и latency.
Именно это и является основной задачей real-time реализации.
Бот способен на дистанции действительно принести иксы, что является отличным результатом. Исходя из бектеста можем сказать, что частота сделать — чуть меньше 1 сделки в день с довольно долгим периодом удержания. Так что этот скрипт — действительно качественный и консервативный свинг — робот для крипторынка.
Многим «платформам» с ГитХаб скоро придет трындец. Он уже приходит. Проще с нуля через ИИ написать готовое с коннекторами, бэктестерами и блэк-джеками, чем изучать чужое поделие и изделие.