. Вдохновившись недавней статьей на Veles Finance "«Bomberman»: стратегия для крипторынка с индикаторами BOP, Mean Reversion и Дончиана", я решил воплотить эту идею в жизнь. Не просто в теории, а в коде: создал полноценного алгобота на Python, который автоматизирует торговлю, тестирует параметры и визуализирует результаты.
Этот бот прозрачен: использует открытые данные с binance, классические индикаторы технического анализа и строгий walk-forward бэктест, чтобы избежать look-ahead bias (смещения в будущее).
В статье разберем логику стратегии, архитектуру бота, ключевые функции и реальные результаты на исторических данных BTC/USDT. Если вы программист с интересом к финансам или трейдер, жаждущий автоматизации, — добро пожаловать. Мы пройдемся по коду, формулам и рискам.
Почему Bomberman
Стратегия "Bomberman", описанная в оригинальной статье, черпает вдохновение из аркадного хита 1980-х. Вкратце, основная идея самой стратегии в комбинации трех индикаторов на разных таймфреймах:
BOP (Balance of Power) на 30-минутном (M30) — измеряет баланс сил между быками и медведями.
Mean Reversion Channel на 15-минутном (M15) — канал возврата к среднему для зон перепроданности/перекупленности.
Donchian Channel на 5-минутном (M5) — прорывы для подтверждения импульса.
Бот реализует это с учетом шортов (коротких позиций), левериджа и строгого риск-менеджмента (1% на сделку). В отличие от ручной торговли, бот вычисляет индикаторы только на исторических данных до текущего бара, имитируя реальную торговлю без подглядывания в будущее. Это не просто бэктест — это симуляция, готовая к деплою на реальном API. Достаточно будет просто добавить подгрузку актуальных данных в бота и открытие сделок с помощью клиента для биржи (ранее писали собственный клиент для bingX).
Логика стратегии: Вход, выход и "бомбы"
Основа бота — функция backtest_one, которая симулирует торговлю. Давайте разберем правила стратегии. Все расчеты на основе OHLCV-данных (Open, High, Low, Close, Volume).
1. Индикаторы
Бот вычисляет индикаторы динамически, чтобы избежать предвзятости. Вот формулы:
-
BOP (Balance of Power):

-
Mean Reversion Channel (на M15):

-
Donchian Channel (на M5):

2. Условия входа: "Подрыв барьера"
Толерантность tol (0.5–1.5%) добавляет гибкости для приближения к уровням.
-
Лонг (LONG):
BOP > 0 (быки доминируют на M30).
Цена ≤ Lower × (1 + tol) (перепроданность на M15).
Цена > предыдущий Donchian_high (прорыв на M5).
-
Шорт (SHORT):
BOP < 0 (медведи доминируют).
Цена ≥ Upper × (1 - tol) (перекупленность).
Цена < предыдущий Donchian_low (прорыв вниз).
Размер позиции расчитываем исходя из указанного риска и капитала. Это в первую очередь нужно при реализации реальной торговле, для бектеста пока что достаточно получить прибыль в %.
3. Условия выхода: "Взрыв и отступление"
Выход минимизирует убытки и фиксирует прибыль:
-
Для лонга:
Тейк-профит: цена ≥ Upper (возврат к сопротивлению).
Стоп-лосс: цена ≤ entry × 0.99 (1% убыток).
-
Для шорта:
Тейк-профит: цена ≤ Lower.
Стоп-лосс: цена ≥ entry × 1.01 (1% убыток).
Эта логика обеспечивает баланс: mean reversion ловит отскоки, Donchian — трендовые прорывы, BOP — фильтр тренда. В "both" режиме бот торгует и лонг, и шорт.
Архитектура бота: Функции и модули
Бот — модульный скрипт на ~300 строк Python. Зависимости: ccxt (для данных), pandas/numpy (анализ), matplotlib (визуализация). Нет ML или сложных фреймворков — чистый TA-Lib стиль, но самописный для контроля.
Для начала сделаем импорты:
import ccxt
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
from datetime import datetime
import time
1. Загрузка данных: fetch_binance_ohlcv
Публичный API binance(без ключей). Фетчит OHLCV за N баров назад:
from binance.client import Client
client = Client()
def fetch_binance_ohlcv(symbol='ETHUSDT', interval='15m', total_bars=50000, client=client):
limit = 1000
data = []
current_end = None
while len(data) < total_bars:
bars_to_fetch = min(limit, total_bars - len(data))
try:
klines = client.futures_klines(
symbol=symbol.upper(),
interval=interval,
limit=bars_to_fetch,
endTime=current_end
)
except Exception as e:
print("Ошибка Binance API:", e)
break
if not klines:
break
data = klines + data # prepend
current_end = klines[0][0] - 1
time.sleep(0.1)
if not data:
return pd.DataFrame()
df = pd.DataFrame(data, columns=[
'timestamp','open','high','low','close','volume',
'close_time','quote_asset_volume','number_of_trades',
'taker_buy_base','taker_buy_quote','ignore'
])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df[['open','high','low','close','volume']] = df[['open','high','low','close','volume']].astype(float)
df = df.drop_duplicates('timestamp').sort_values('timestamp').reset_index(drop=True)
df = df.rename(columns={'timestamp': 'time'})
return df[["time", "open", "high", "low", "close", "volume"]]
Синхронизирует таймфреймы: ~2000 баров M30 ≈ 4000 M15 ≈ 12000 M5 (~1 год данных). Обработка дубликатов и NaN.
2. Вычисление индикаторов: add_bop, add_mean_reversion, add_donchian
Простые rolling-операции на pandas. Для каждого бара (данные свечи) режем DF до current_time и пересчитываем только прошлое. Также необходимо синхронизовать таймфреймы, так как нам нужно чтобы все индексы свечей совпадали и не было ненужных нам смещений.
def add_bop(df: pd.DataFrame, smooth: int = 1) -> pd.DataFrame:
df = df.copy()
df['bop'] = (df['close'] - df['open']) / (df['high'] - df['low']).replace(0, np.nan)
if smooth > 1:
df['bop'] = df['bop'].rolling(smooth).mean()
return df
def add_mean_reversion(df: pd.DataFrame, period: int = 20, mult: float = 2.0) -> pd.DataFrame:
df = df.copy()
df['sma'] = df['close'].rolling(period).mean()
df['std'] = df['close'].rolling(period).std()
df['upper'] = df['sma'] + mult * df['std']
df['lower'] = df['sma'] - mult * df['std']
return df
def add_donchian(df: pd.DataFrame, period: int = 20) -> pd.DataFrame:
df = df.copy()
df['donchian_high'] = df['high'].rolling(period).max()
df['donchian_low'] = df['low'].rolling(period).min()
return df
# ------------------------------------------------------------------
# 3. Синхронизация (walk-forward)
# ------------------------------------------------------------------
def sync_timeframes(df30, df15, df5):
df = df30.copy()
df = df.join(df15[['upper', 'lower']], how='left')
df = df.join(df5[['donchian_high', 'donchian_low']], how='left')
cols = ['upper', 'lower', 'donchian_high', 'donchian_low']
df[cols] = df[cols].ffill() # Только прошлые данные
return df
Три таймфрейма по этим индикатором (M30 → M15 → M5) создают иерархическую фильтрацию:
M30: "Кто сильнее?"
M15: "Где перекупленность?"
M5: "Есть ли прорыв?"
Такой мультитаймфреймовый подход - основа стратегии.
3. Бэктест: backtest_one
Walk-forward симуляция: цикл по барам M30, слайс данных + индикаторы + сигналы. Возвращает словарь с результатом и основыми данными.
def backtest_one(df: pd.DataFrame,
tol: float,
direction: str = "both",
initial_capital: float = 10_000,
leverage: int = 5,
risk_per_trade: float = 0.01) -> dict:
capital = initial_capital
position = 0 # 1 = long, -1 = short, 0 = flat
entry_price = 0.0
size = 0.0
equity = [capital]
trades = []
for i in range(50, len(df)):
row = df.iloc[i]
prev = df.iloc[i-1]
price = row['close']
# --- ВХОД ---
if position == 0:
# LONG
if direction in ["long", "both"]:
long_cond = (
row['bop'] > 0 and
price <= row['lower'] * (1 + tol) and
price > prev['donchian_high']
)
if long_cond:
size = (capital * risk_per_trade * leverage) / price
entry_price = price
position = 1
trades.append(f"LONG at {price:.5f}")
# SHORT
if direction in ["short", "both"]:
short_cond = (
row['bop'] < 0 and
price >= row['upper'] * (1 - tol) and
price < prev['donchian_low']
)
if short_cond:
size = (capital * risk_per_trade * leverage) / price
entry_price = price
position = -1
trades.append(f"SHORT at {price:.5f}")
# --- ВЫХОД ---
if position == 1: # LONG
if price >= row['upper']:
pnl = size * (price - entry_price) * leverage - size * 0.0005
capital += pnl
position = 0
trades.append(f"EXIT LONG at {price:.5f} | PnL: {pnl:+.2f}")
elif price <= entry_price * 0.99:
pnl = size * (price - entry_price) * leverage - size * 0.0005
capital += pnl
position = 0
trades.append(f"STOP LONG at {price:.5f} | PnL: {pnl:+.2f}")
if position == -1: # SHORT
if price <= row['lower']:
pnl = (size * (entry_price - price) * leverage) - size * 0.0005
capital += pnl
position = 0
trades.append(f"EXIT SHORT at {price:.5f} | PnL: {pnl:+.2f}")
elif price >= entry_price * 1.01:
pnl = size * (entry_price - price) * leverage - size * 0.0005
capital += pnl
position = 0
trades.append(f"STOP SHORT at {price:.5f} | PnL: {pnl:+.2f}")
equity.append(capital)
total_ret = (capital - initial_capital) / initial_capital
return {
'final_capital': capital,
'total_return': total_ret,
'equity': equity,
'trades': trades
}
Обратите внимание на расчёт прибыли:
pnl = size (price - entry_price) leverage - size * 0.005
Комиссия учтена и она равняется size * 0.005 (для bingx со скидкой на комиссию taker 0.0025=0.25%). Для binance, bybit - 0.01, что будет уже не так приятно, но в рамках не самого большого количества сделок всё еще терпимо.
4. Оптимизация: grid_search
Грид по 5+ параметрам (576 комбинаций в полном режиме, но для теста — подмножество). Скрипт выполняется довольно быстро ввиду не самого большого количества свечей. Если хотите доработать его и добавить новые фильтры и перебор большего числа параметров - несложно можно добавить multiprocessing (параллелизм).
def grid_search(df30, df15, df5):
param_grid = {
'mr_period': [15, 20],
'mr_mult' : [2.0, 2.5],
'dc_period': [15, 20],
'bop_smooth': [1],
'tol' : [0.015],
'direction': ['both']
}
results = []
total = np.prod([len(v) for v in param_grid.values()])
print(f"Грид-поиск: {total} комбинаций...")
cnt = 0
for mr_p in param_grid['mr_period']:
for mr_m in param_grid['mr_mult']:
for dc_p in param_grid['dc_period']:
for bop_s in param_grid['bop_smooth']:
for tol in param_grid['tol']:
for dir_ in param_grid['direction']:
cnt += 1
print(f"[{cnt}/{total}] MR={mr_p}*{mr_m}, DC={dc_p}, BOP_s={bop_s}, tol={tol*100:.1f}%, {dir_}")
df_mr = add_mean_reversion(df15.copy(), mr_p, mr_m)
df_dc = add_donchian(df5.copy(), dc_p)
df_bop = add_bop(df30.copy(), bop_s)
df_test = sync_timeframes(df_bop, df_mr, df_dc)
res = backtest_one(df_test, tol, direction=dir_)
results.append({
'mr_period': mr_p,
'mr_mult': mr_m,
'dc_period': dc_p,
'bop_smooth': bop_s,
'tolerance': tol,
'direction': dir_,
'final_capital': res['final_capital'],
'total_return': res['total_return'],
})
return pd.DataFrame(results)
По сути, мы здесь просто для каждой комбинации прогоняем бектест и собираем результаты - далее будем их сравнивать и выбирать лучшее.
5. Визуализация: plot_best
Построение графика для лучшей комбинации:
Верхний: Цена + каналы + сигналы (стрелки лонг/шорт).
Нижний: Equity-кривой. Matplotlib генерирует интерактивный plot.
def plot_best(df30, df15, df5, best_row):
df_mr = add_mean_reversion(df15.copy(),
int(best_row['mr_period']),
best_row['mr_mult'])
df_dc = add_donchian(df5.copy(),
int(best_row['dc_period']))
df_bop = add_bop(df30.copy(),
int(best_row['bop_smooth']))
df = sync_timeframes(df_bop, df_mr, df_dc)
# Пересчёт с лучшими параметрами
res = backtest_one(df,
best_row['tolerance'],
direction=best_row['direction'])
# Сигналы
df['signal'] = 0
for i, trade in enumerate(res['trades']):
if 'LONG' in trade or 'SHORT' in trade:
timestamp = df.index[50 + i // 2]
df.loc[timestamp, 'signal'] = 1 if 'LONG' in trade else -1
plt.figure(figsize=(16, 10))
ax1 = plt.subplot(3, 1, 1)
ax1.plot(df.index, df['close'], label='BTC/USDT', color='steelblue')
ax1.plot(df.index, df['lower'], '--', color='green', label='Lower')
ax1.plot(df.index, df['upper'], '--', color='red', label='Upper')
ax1.scatter(df.index[df['signal'] == 1], df['close'][df['signal'] == 1],
marker='^', color='lime', s=100, label='LONG')
ax1.scatter(df.index[df['signal'] == -1], df['close'][df['signal'] == -1],
marker='v', color='red', s=100, label='SHORT')
ax1.set_title(f'Bomberman | {best_row["direction"].upper()} | '
f'Годовых: {best_row["annualized"]*100:.1f}%')
ax1.legend()
ax3 = plt.subplot(3, 1, 3)
ax3.plot(res['equity'], color='gold')
ax3.set_title(f'Капитал: ${res["final_capital"]:,.0f}')
plt.tight_layout()
plt.show()
Запустим нашу стратегию
Запустим подгрузку свечей по всем тф и используем наши функции:
if __name__ == "__main__":
print("Загрузка данных...")
df_30m = fetch_binance_ohlcv('BTCUSDT', '30m', 5000)
df_15m = fetch_binance_ohlcv('BTCUSDT', '15m', 10000)
df_5m = fetch_binance_ohlcv('BTCUSDT', '5m', 30000)
print(f" 30m: {len(df_30m)} | 15m: {len(df_15m)} | 5m: {len(df_5m)}")
results_df = grid_search(df_30m, df_15m, df_5m)
top5 = results_df.sort_values('annualized', ascending=False).head(5)
print("\n" + "="*80)
print("ТОП-5 ПАРАМЕТРОВ (с LONG/SHORT/BOTH)")
print("="*80)
print(top5[['direction', 'mr_period', 'mr_mult', 'dc_period',
'bop_smooth', 'tolerance', 'annualized', 'final_capital']])
best = top5.iloc[0]
print(f"\nВизуализация лучшего: {best['direction']} | "
f"Годовых: {best['annualized']*100:.1f}%")
plot_best(df_30m, df_15m, df_5m, best)
Результаты
На исторических данных BTC/USDT бот показал солидные цифры. В полном грид-серче (576 runs) лучшая комбинация дала 38% прибыли за период в 30000 5m свечей, что равно приблизительно 104 дням. Это отличный результат за такой небольшой период.

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