В классическом алготрейдинге рынок часто моделируется как временной ряд: индикаторы, скользящие средние, осцилляторы. Аукционная теория рассматривает рынок иначе — как процесс распределения объёма по ценовым уровням, где цена ищет баланс между спросом и предложением.
Ключевым элементом такого подхода является Volume Profile, а именно Point of Control (POC) — уровень цены, на котором за выбранный период был проторгован максимальный объём. В терминах аукционной теории POC соответствует зоне максимального согласия участников рынка.
В статье рассматривается создание алгоритмического торгового бота, основанного на реакции цены относительно:
POC
Value Area High (VAH)
Value Area Low (VAL)
В качестве основы используется Python‑скрипт back.py, предназначенный для параметрического бэктеста стратегии.
Все скрипты из статьи я выложил на github для вашего удобства.
Архитектура backtest‑скрипта
Скрипт логически разделён на несколько уровней:
Загрузка и подготовка рыночных данных (Binance Futures)
Расчёт Volume Profile и POC
Генерация торговых сигналов
Рыночная структура (swing‑экстремумы)
Управление риском (SL / TP)
Симуляция сделок
Анализ результатов
Такое разделение важно: в дальнейшем те же блоки будут переиспользованы в real‑time боте практически без изменений.
Расчёт Volume Profile и POC
Ключевая функция стратегии — calculate_volume_profile.
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)Что происходит:
Берётся диапазон цен за весь доступный период
Добавляется буфер, чтобы исключить краевые искажения
Диапазон разбивается на фиксированное число ценовых бинов
Далее каждая свеча распределяет свой объём по пересекаемым ценовым уровням:
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Таким образом формируется реальный объёмный профиль
Расчёт POC и Value Area
poc_index = np.argmax(volume_profile)
poc = bin_centers[poc_index]Value Area считается классическим способом — через накопление 68% объёма от POC наружу.
Генерация торговых сигналов
Логика входа реализована в generate_signals().
if (prev_close <= va_low * (1 + threshold) and close > va_low):
signals.iloc[i] = 'Buy'Интерпретация:
цена находилась в зоне дисбаланса ниже VAL
затем вернулась внутрь value area
рынок "принял" цену обратно
Short-сигнал зеркален относительно VAH.
Объёмный фильтр
if vol < avg_vol_i * min_volume_ratio:
continueСигнал игнорируется, если возврат в value происходит без участия объёма.
Рыночная структура: swing-экстремумы
Для управления сделкой используется структура рынка.
Internal swings
def detect_internal_swings(df):
if low[i] < low[i-1] and low[i] < low[i+1]:
swing_low[i] = TrueИспользуются для:
First Trouble Area - первая зона, в которой можно получить реакцию
ближних целей
External swings
def detect_external_swings(df, win=20):
if low[i] == low[L:R].min():
swing_low[i] = TrueИспользуются как цели ликвидности и структурные стоп-лоссы
Stop Loss: приоритет структуры
def calculate_sl(df, entry_idx, entry_price, direction):
if direction == 'long':
return last_swing_lowЛогика:
SL ставится за ближайший структурный экстремум
Если экстремума нет — fallback на ATR
Это принципиально отличает стратегию от индикаторных систем.
Take Profit: ATR, Liquidity, FTA
if tp_mode == 'liquidity':
tp = tp_liquidity(df, i, direction)TP может быть:
фиксированным по волатильности
по цели ликвидности
по первой проблемной зоне
Каждая сделка проходит фильтр, чтобы риск-ревард был более 1. В ином случае эта сделка просто не выгодна.
Симуляция сделок
for j in range(i + 1, i + max_bars):
if bar_low <= sl:
exit_price = slМодель исполнения:
проверка SL → TP
без подглядывания в будущее
одна позиция = одна сделка
Комиссии учитываются с двух сторон.
Анализ результатов
winrate = len(wins) / trades_cnt * 100
profit_factor = wins.sum() / abs(losses.sum())Также рассчитываются:
Sharpe Ratio (annualized)
Max Drawdown
Net PnL
Метрики считаются по сделкам, а не по свечам.
Почему этот подход работает
Используется реальное распределение объёма
Входы строятся от логики аукциона, а не индикаторов
Риск контролируется структурой рынка
Бэктест максимально приближен к реальному исполнению
Запустив бектест, мы получим ряд данных. Для примера:
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%Здесь мы можем увидеть, что есть действительно неплохая стратегия с винрейтом в 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 реализации — одна:
корректно и без искажений перенести готовую стратегию в живой рынок.
Общая архитектура real-time бота
Ключевой принцип — жёсткое разделение ответственности:
Binance (данные) → Strategy Engine → BingX (исполнение)
Binance используется исключительно как источник свечей
BingX — только как торговая площадка, так как имеет меньшие комисии
Это позволяет избежать логических расхождений и упрощает отладку.
Execution layer: BingxClient
Для грамотной работы нам необходим bingX client - для удобства я написал SDK библиотеку со всеми функциями для этой биржи. Это позволить не переписывать сложные функции с подписями и запросами для каждой стратегии, а использовать один скрипт. Он вместе со стратегией хранится на github.
Инициализируем библиотеку:
bingx_client = BingxClient(
api_key=API_KEY,
api_secret=API_SECRET,
symbol="BTCUSDT"
)Сделки будем открывать с помощью функции:
def place_market_order(self, side: str, qty: float, symbol: str = None, stop: float = None, tp: float = None):Цикл получения данных
df = fetch_klines_paged(
SYMBOL,
INTERVAL,
total_bars=2000,
client=binance_client
)Особенности:
подгружается достаточно длинная история для корректного Volume Profile
данные каждый цикл пересобираются заново
используются только закрытые свечи
Это дороже по API, но гарантирует идентичность логики с бэктестом.
Подготовка данных перед сигналом
df = detect_internal_swings(df)
df = detect_external_swings(df)
df = calculate_atr(df)
df = generate_signals(df, params)Важно:
порядок вызовов полностью совпадает с бэктестом
никаких оптимизаций или «ускорений» не используется
Любое отклонение здесь приводит к несовпадению сигналов.
Работа строго по последней закрытой свече
if df['long_signal'].iloc[-2]:
Бот:
не реагирует на текущую формирующуюся свечу
не пересчитывает POC intra-bar
Это сознательный компромисс:
меньше сделок
но полное соответствие backtest → live
Формирование ордера
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(...)Все параметры сделки:
entry
stop-loss
take-profit
рассчитываются до отправки ордера.
Биржа не принимает решений — она только исполняет.
Отправка ордера в отдельном потоке
threading.Thread(
target=open_order_bingx,
args=(direction, QTY, entry_idx, df)
).start()Это решает проблему прерывания основного цикла, это не является чем-то основным, но всё же делает логику безопаснее.
Основной цикл бота
while True:
run_live_bot(params)
time.sleep(60)Минимализм цикла — осознанный выбор:
нет хранения позиций
нет локального state
нет логики сопровождения
Позиция живёт на стороне биржи.
Ключевые проблемы real-time и как они решены
1. Drift между backtest и live
одинаковый код
одинаковый порядок расчётов
2. Рассинхронизация свечей
только закрытые бары
3. API latency
threading
4. Ошибка исполнения
торговая логика изолирована от AP
Вывод
Этот real-time бот — не отдельная система, а прямое продолжение бэктест-движка.
Если стратегия работает в истории, она будет вести себя так же и в реальном рынке — с учётом комиссии, проскальзывания и latency.
Именно это и является основной задачей real-time реализации.
Бот способен на дистанции действительно принести иксы, что является отличным результатом. Исходя из бектеста можем сказать, что частота сделать - чуть меньше 1 сделки в день с довольно долгим периодом удержания. Так что этот скрипт - действительно качественный и консервативный свинг - робот для крипторынка.
