Как делать грамотный бэктест и анализ торговой стратегии: метрики, сигналы, сделки и выводы в алготрейдинге

от автора

📜 Введение

В мире алготрейдинга многие уверены: «если стратегия показывает хорошие сигналы — значит она прибыльна». Увы, это заблуждение. Чтобы стратегия действительно была рабочей, нужно грамотно провести бэктест — не просто «посчитать винрейт», а рассчитать ключевые метрики: Profit Factor, Sharpe Ratio, Max Drawdown, и лишь потом делать выводы.

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

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

📦 1. Получаем исторические данные и сигналы

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

Начнём с импорта, инициализируем клиент биржи:

from binance.client import Client import pandas as pd  client = Client()

Теперь нам нужно подгрузить исторические свечи, чтобы понять как стратегия отрабатывала на истории. В нашем бектесте будем анализировать 100000 5м свечей. Это около 350 дней, этого хватит для тестирования скальп стратегии.

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

def fetch_klines_paged(symbol='BTCUSDT', interval='5m', total_bars=100000, client=None):     if client is None:         client = Client()      limit = 1000     data = []     end_time = None  # самый последний бар (новейшая точка)      while len(data) < total_bars:         bars_to_fetch = min(limit, total_bars - len(data))          try:             klines = client.futures_klines(                 symbol=symbol,                 interval=interval,                 limit=bars_to_fetch,                 endTime=end_time             )         except Exception as e:             print("Ошибка Binance API:", e)             break          if not klines:             break          data = klines + data  # prepend! — старые свечи добавляем в начало         end_time = klines[0][0] - 1  # сдвиг назад по времени         time.sleep(0.2)      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')     df = df.sort_values('timestamp').reset_index(drop=True)     return df

Функция вернет нам dataframe со всеми нужными свечами, с ним мы и будем работать.

2. Создаём условия для входа в позицию и начинаем бектест

Начнём с создания условий для входа:

def check_long_condition(row):     return (         row['CSI'] > 0 and         row['CSI'] > row['CSI_prev'] and         isinstance(row['cluster_id'], str) and row['cluster_id'].startswith('bull') and         row['close'] > row['ema_fast'] > row['ema_slow']         )      def check_short_condition(row):     return (         row['CSI'] < 0 and         row['CSI'] < row['CSI_prev'] and         isinstance(row['cluster_id'], str) and row['cluster_id'].startswith('bear') and         row['close'] < row['ema_fast'] < row['ema_slow']         )

Они могут быть любые, я для примера вставил часть условий для своего бота.

Перейдём к основной логике запуска. Для начала получим наш df (массив 100000 свечей) и добавим на него наши нужные индикаторы:

if __name__ == "__main__":     client = Client()     df = fetch_klines_paged('BTCUSDT', '5m', total_bars=100000, client=client)     print(f"Получено свечей: {len(df)}")     df = compute_indicators(df) #функция вида  df['RS'] = 1+rs ;  return df

3. Добавляем нашу логику к свечам

Теперь нужно рассмотреть все свечи и проверять на них наши сигналы. Разделим long и short сигналы в разные блоки, чтобы потом было удобнее анализировать статистику. Также сохраним все наши сделки в .csv таблицу. При необходимости можно будет сделать анализ конкретных сигналов.

    df['long_signal'] = df.apply(check_long_condition, axis=1)     df['short_signal'] = df.apply(check_short_condition, axis=1)      long_signals = df[df['long_signal']]     short_signals = df[df['short_signal']]      # 👇 Сохраняем сделки в CSV     long_signals.to_csv('long_signals.csv', sep=';', index=False)     short_signals.to_csv('short_signals.csv', sep=';', index=False)     df.to_csv('all_data.csv', sep=';', index=False)

🧪 4. Симулируем сделки и считаем прибыль

Теперь нужно понять: какую прибыль дали сигналы. Для простоты мы входим по цене close на сигнальной свече и выходим через 3 свечи (т.е. 15 минут спустя). Выход через 15минут был выведен на основе большого опыта работы через 5м сигналы путём смены интервала через циклы.

Для начала напишем функцию счёта профита:

def calculate_profits(df, signal_col='long_signal', direction='long', exit_after=3):     trades = []     for i in range(len(df) - exit_after):         if df.iloc[i][signal_col]:             entry_price = df.iloc[i]['close']             exit_price = df.iloc[i + exit_after]['close']             if direction == 'long':                 profit = exit_price - entry_price                 profit_pct = (exit_price / entry_price - 1) * 100             else:                 profit = entry_price - exit_price                 profit_pct = (entry_price / exit_price - 1) * 100             trades.append({                 'entry_time': df.iloc[i]['timestamp'],                 'exit_time': df.iloc[i + exit_after]['timestamp'],                 'entry_price': entry_price,                 'exit_price': exit_price,                 'profit': profit,                 'profit_%': profit_pct                 'close': entry_price  # ← добавляем сюда цену входа             })     return pd.DataFrame(trades) 

И применим это к нашим сигналам в основном блоке:

long_trades = calculate_profits(df, signal_col='long_signal', direction='long', exit_after=3) short_trades = calculate_profits(df, signal_col='short_signal', direction='short', exit_after=3)  long_trades.to_csv('long_trades.csv', sep=';', index=False) short_trades.to_csv('short_trades.csv', sep=';', index=False)

📊 5. Анализ метрик стратегии

И вот теперь — самое важное: какие метрики мы можем рассчитать и что они значат?

✅ Win Rate

Процент прибыльных сделок: насколько стратегия вообще угадывает?

win_rate = len(df[df['profit'] > 0]) / len(df) * 100

✅ Profit Factor

Сумма всей прибыли / сумма всех убытков. Главное — выше 1.

profit_factor = df[df['profit'] > 0]['profit'].sum() / abs(df[df['profit'] < 0]['profit'].sum())

✅ Sharpe Ratio

Стабильность стратегии: насколько равномерно зарабатываем?

sharpe = df['profit'].mean() / df['profit'].std()

✅ Max Drawdown

Максимальное падение капитала — нужен для оценки риска.

equity = df['profit'].cumsum() rolling_max = equity.cummax() drawdown = equity - rolling_max max_dd = drawdown.min()

Также добавим классическую статистику в виде дельты и профита:

    total_profit = trades_df['profit'].sum()     avg_price = trades_df['close'].mean()     delta = total_profit / avg_price if avg_price != 0 else float('nan')

✅ Всё вместе

Функция анализа:

def analyze_trades(trades_df):     if trades_df.empty:         print("Нет сделок для анализа.")         return      win_trades = trades_df[trades_df['profit'] > 0]     loss_trades = trades_df[trades_df['profit'] <= 0]      win_rate = len(win_trades) / len(trades_df) * 100     profit_factor = win_trades['profit'].sum() / abs(loss_trades['profit'].sum()) if not loss_trades.empty else float('inf')      # Sharpe Ratio (annualized)     daily_returns = trades_df['profit']     mean_return = daily_returns.mean()     std_return = daily_returns.std()     sharpe_daily = mean_return / std_return if std_return != 0 else 0     sharpe_annual = sharpe_daily * np.sqrt(252)      # Equity и просадка     equity_curve = trades_df['profit'].cumsum()     rolling_max = equity_curve.cummax()     drawdown = equity_curve - rolling_max     max_drawdown = drawdown.min()      # Общий профит и дельта     total_profit = trades_df['profit'].sum()     avg_price = trades_df['close'].mean()     delta = total_profit / avg_price if avg_price != 0 else float('nan')      print("\n📊 Результаты анализа сделок:")     print(f"Всего сделок: {len(trades_df)}")     print(f"Win Rate: {win_rate:.2f}%")     print(f"Profit Factor: {profit_factor:.2f}")     print(f"Sharpe Ratio (annualized): {sharpe_annual:.2f}")     print(f"Max Drawdown: {max_drawdown:.2f}")     print(f"📈 Общий профит: {total_profit:.2f}")     print(f"⚖️ Дельта (profit / avg BTC): {delta:.4f}")

Ну и теперь вызовем нашу функцию:

print("LONG TRADES:") analyze_trades(long_trades)  print("\nSHORT TRADES:") analyze_trades(short_trades)

Всю логику можно оформить как модуль, а сами сигналы и сделки анализировать и визуализировать, например, в Jupyter, Excel или Python-дашборде.

Давайте теперь посмотрим какие данные я получил проанализировав моего бота:

📈 Анализ лонг-сделок:

📊 Результаты анализа сделок:
Всего сделок: 417
Win Rate: 76.98%
Profit Factor: 6.89
Sharpe Ratio (annualized): 3.13
Max Drawdown: -1313.60
📈 Общий профит: 98254.50
⚖️ Дельта (profit / avg BTC): 1.1034

📉 Анализ шорт-сделок:

📊 Результаты анализа сделок:
Всего сделок: 336
Win Rate: 75.00%
Profit Factor: 8.02
Sharpe Ratio (annualized): 3.09
Max Drawdown: -1442.50
📈 Общий профит: 86625.40
⚖️ Дельта (profit / avg BTC): 0.9710

Всего сделок: 753 (417 лонгов + 336 шортов)

Разбор показателей

1. Win Rate: качество сигналов

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

2. Profit Factor (PF): соотношение прибыли к убыткам

PF = 6.89 (лонг) и 8.02 (шорт) — очень высокие значения. В индустрии PF > 2 уже считается хорошим. Значение около 7–8 говорит о том, что суммарный профит почти в 7–8 раз превышает убытки.

Вывод: Выдерживается дисциплина по стоп-лоссам и грамотный выбор целей для тейк-профитов.

3. Sharpe Ratio: риск и доходность

Sharpe Ratio около 3— это очень хороший результат, обычно фондовые стратегии имеют значения 1–2, а 3–4 считаются очень хорошими. Высокий Sharpe говорит о низкой волатильности дохода и стабильности.

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

4. Max Drawdown: максимальное проседание капитала

Дровдаун в районе 1300–1400 условных единиц. Это менее 2% чистого движения на фиксированную позицию, тоесть 20% на всю позицию с плечом. (Т.к. средняя цена биткойна около 80к). Это отличный показатель для этой тс.

Общий профит и дельта

  • Общий профит порядка 98 000 для лонга и 86 000 для шорта — отличные результаты.

  • Дельта (profit / avg BTC) около 1 для каждого направлению — говорит о том, что на каждую среднюю единицу базового актива стратегия приносит примерно такую же сумму прибыли, что свидетельствует о высокой доходности позиции. Но не надо забывать, что прибыль идёт на полную позицию. Поэтому вложив 100$ выход будет 1000$ (на байбит фикс. плечо 10х).

Комиссии

Часть важность коммисий и проскальзывания опускается при анализе статистики. Но в HTF и скальп стратегии без этого никуда. Покажу анализ нашей стратегии:

📌 Условия:

  • Входной капитал: $1,000

  • Плечо: 10x → значит, ты торгуешь как будто бы $10,000

  • Комиссия на сделку (открытие + закрытие): 0.1% = 0.001

  • Сделок всего: 753

🔢 Расчёт:

1. Объём одной сделки (с плечом):

1000*10 = 10000 USDT

2. Комиссия с одной сделки:

10,000×0.1% = 10$

3. Общая комиссия за 753 сделки:

10×753=7530 USDT

Так что видим что комиссионные сьели достаточно сильную часть прибыли. От этого не уйти, к сожалению, но всё же даже с учётом коммиссий видим хороший результат — 200% прибыли, т.е. 20000$-7530$=12470$ прибыли за год со вложенных всего 1000$ на позицию. (т.е. на счёте нужно иметь хотя бы чуть более 2к$, так как позиции редко, но бывает, что открываются подряд)

Заключение

Разобрали как рассчитать все необходимые показатели для понимания эффективность торговых алгоритмических систем.

Представленная торговая система демонстрирует впечатляющие показатели — высокие Win Rate, Profit Factor и Sharpe Ratio, а также управляемый Max Drawdown. Это результат продуманного алгоритма, который успешно балансирует между агрессивной доходностью и контролем риска.

Без аналитики полученной после бектеста статистика запуск ботов по трейдингу — чистое казино. Используйте стратегии с умом и всегда проводите тщательное тестирование.


ссылка на оригинал статьи https://habr.com/ru/articles/935196/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *