Бэктестинг Мартингейл-стратегий: Расчет Risk of Ruin на Python

Бэктестинг Мартингейл-стратегий: Расчет Risk of Ruin на Python Анализ данных и Бэктесты
Подробное руководство по созданию Python-скрипта для бэктестинга Мартингейл-стратегий, включая расчет вероятности разорения (Risk of Ruin) на исторических данных с использованием Monte Carlo симуляции.
Суть: Скрипт на Python для симуляции Мартингейл-стратегий на исторических данных, позволяющий оценить их эффективность и рассчитать вероятность разорения (Risk of Ruin) с учетом заданных параметров и Monte Carlo симуляции.

Исходный код

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

Представленный ниже Python-скрипт позволяет провести бэктестинг Мартингейл-стратегии на исторических данных. Он симулирует последовательность сделок, отслеживает изменение капитала и, что критически важно, рассчитывает вероятность разорения (Risk of Ruin, RoR) с использованием метода Монте-Карло. Это дает более реалистичную оценку рисков, чем просто теоретические формулы, поскольку учитывает наблюдаемые характеристики доходности на исторических данных.

Для демонстрации работы скрипта используются сгенерированные случайные исторические данные. В реальных условиях вы должны будете загрузить свои собственные данные (например, OHLCV из CSV-файла).


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random

# --- Конфигурация для бэктеста ---
# Для демонстрации генерируем фиктивные данные.
# В реальном сценарии вы бы загружали данные из CSV.
# data_path = 'your_historical_data.csv'
# Если используете реальные данные, убедитесь, что есть столбец 'Close'.

# Параметры стратегии
INITIAL_CAPITAL = 10000.0
INITIAL_BET_SIZE = 10.0
MARTINGALE_MULTIPLIER = 2.0 # Во сколько раз увеличивать ставку после проигрыша
MAX_CONSECUTIVE_LOSSES = 7 # Максимальное количество последовательных проигрышей до сброса ставки
PROFIT_TARGET_PCT = 0.01 # 1% прибыли от размера ставки при выигрыше
STOP_LOSS_PCT = 0.01 # 1% убытка от размера ставки при проигрыше

# Параметры симуляции Risk of Ruin (RoR)
NUM_ROR_SIMULATIONS = 10000
MAX_TRADES_PER_ROR_SIM = 1000 # Максимальное количество сделок в одной симуляции RoR

# --- Загрузка данных (генерация фиктивных данных для демонстрации) ---
def generate_dummy_data(num_days=252, start_price=100):
    """Генерирует фиктивные дневные цены закрытия."""
    np.random.seed(42)
    returns = np.random.normal(0.0005, 0.01, num_days) # Среднее 0.05%, Ст.откл. 1%
    prices = start_price * np.exp(np.cumsum(returns))
    df = pd.DataFrame({'Close': prices})
    return df

# --- Функция бэктестинга Мартингейл-стратегии ---
def run_martingale_backtest(
    data: pd.DataFrame,
    initial_capital: float,
    initial_bet_size: float,
    martingale_multiplier: float,
    max_consecutive_losses: int,
    profit_target_pct: float,
    stop_loss_pct: float
):
    """
    Выполняет бэктестинг Мартингейл-стратегии на исторических данных.
    Результаты сделок определяются простым движением цены:
    если цена закрытия следующего дня > цены закрытия текущего дня, это выигрыш;
    в противном случае - проигрыш. Это упрощенная 'стратегия'.
    """
    capital = initial_capital
    current_bet = initial_bet_size
    consecutive_losses = 0
    equity_curve = [capital]
    trade_outcomes = [] # True для выигрыша, False для проигрыша
    trade_profits = []

    # Вычисляем дневные доходности для симуляции результатов сделок.
    # Предполагаем, что сделка открывается по цене Close[i] и закрывается по Close[i+1].
    data['Daily_Return'] = data['Close'].pct_change().shift(-1)

    # Итерируем по данным для симуляции сделок
    # Итерируем до len(data) - 1, так как нам нужна доходность следующего дня.
    for i in range(len(data) - 1):
        if capital <= 0:
            print(f"Капитал исчерпан на точке данных {i}. Разорение!")
            break

        daily_return = data['Daily_Return'].iloc[i]
        
        if pd.isna(daily_return):
            continue # Пропускаем, если нет доходности следующего дня

        is_win = daily_return > 0

        if is_win:
            # Прибыль составляет процент от текущей ставки
            profit = current_bet * profit_target_pct
            capital += profit
            consecutive_losses = 0
            current_bet = initial_bet_size # Сброс ставки после выигрыша
            trade_outcomes.append(True)
            trade_profits.append(profit)
        else:
            # Убыток составляет процент от текущей ставки
            loss = current_bet * stop_loss_pct
            capital -= loss
            consecutive_losses += 1
            trade_outcomes.append(False)
            trade_profits.append(-loss)

            if consecutive_losses >= max_consecutive_losses:
                # Остановка или сброс стратегии после максимального количества последовательных проигрышей
                current_bet = initial_bet_size # Сброс ставки
                consecutive_losses = 0
            else:
                current_bet *= martingale_multiplier # Удвоение ставки для следующей сделки
                # Важно: убедитесь, что размер ставки не превышает доступный капитал
                # или разумный лимит, чтобы предотвратить немедленное разорение при большом убытке.
                if current_bet > capital * 0.8: # Пример: не ставить более 80% капитала
                    current_bet = capital * 0.8
                if current_bet < initial_bet_size: # Убедитесь, что не опускается ниже начальной ставки
                    current_bet = initial_bet_size

        equity_curve.append(capital)

    final_capital = capital
    total_trades = len(trade_outcomes)
    total_wins = sum(trade_outcomes)
    total_losses = total_trades - total_wins
    win_rate_observed = total_wins / total_trades if total_trades > 0 else 0

    # Расчет максимальной просадки
    equity_series = pd.Series(equity_curve)
    peak = equity_series.expanding(min_periods=1).max()
    drawdown = (equity_series - peak) / peak
    max_drawdown = drawdown.min() if not drawdown.empty else 0

    # Расчет средних значений выигрышей/проигрышей для симуляции RoR
    avg_win_amount = np.mean([p for p in trade_profits if p > 0]) if any(p > 0 for p in trade_profits) else 0
    avg_loss_amount = np.mean([abs(p) for p in trade_profits if p < 0]) if any(p < 0 for p in trade_profits) else 0

    print("\n--- Результаты бэктеста ---")
    print(f"Начальный капитал: ${initial_capital:,.2f}")
    print(f"Конечный капитал: ${final_capital:,.2f}")
    print(f"Всего сделок: {total_trades}")
    print(f"Всего выигрышей: {total_wins}")
    print(f"Всего проигрышей: {total_losses}")
    print(f"Наблюдаемый процент выигрышей: {win_rate_observed:.2%}")
    print(f"Максимальная просадка: {max_drawdown:.2%}")
    print(f"Разорение: {'Да' if final_capital <= 0 else 'Нет'}")

    return {
        'equity_curve': equity_curve,
        'final_capital': final_capital,
        'total_trades': total_trades,
        'total_wins': total_wins,
        'total_losses': total_losses,
        'win_rate_observed': win_rate_observed,
        'max_drawdown': max_drawdown,
        'ruined': final_capital <= 0,
        'avg_win_amount': avg_win_amount,
        'avg_loss_amount': avg_loss_amount
    }

# --- Функция симуляции Risk of Ruin (RoR) ---
def simulate_martingale_ror(
    initial_capital: float,
    initial_bet_size: float,
    martingale_multiplier: float,
    trade_signal_win_rate: float, # Используйте наблюдаемый процент выигрышей из бэктеста или предполагаемый
    avg_win_amount: float,
    avg_loss_amount: float,
    max_consecutive_losses: int,
    num_simulations: int,
    max_trades_per_sim: int
):
    """
    Оценивает Risk of Ruin для Мартингейл-стратегии с использованием симуляции Монте-Карло.
    Результаты сделок основаны на заданном проценте выигрышей и средних суммах выигрышей/проигрышей,
    а не напрямую на исторических движениях цен.
    """
    ruin_count = 0

    for _ in range(num_simulations):
        capital = initial_capital
        current_bet = initial_bet_size
        consecutive_losses = 0
        
        for trade_num in range(max_trades_per_sim):
            if capital <= 0:
                ruin_count += 1
                break

            is_win = random.random() < trade_signal_win_rate

            if is_win:
                capital += avg_win_amount # Используем среднюю сумму выигрыша
                consecutive_losses = 0
                current_bet = initial_bet_size
            else:
                capital -= avg_loss_amount # Используем среднюю сумму проигрыша
                consecutive_losses += 1

                if consecutive_losses >= max_consecutive_losses:
                    current_bet = initial_bet_size # Сброс ставки
                    consecutive_losses = 0
                else:
                    current_bet *= martingale_multiplier
                    # Убедитесь, что ставка не превышает доступный капитал или разумный лимит
                    if current_bet > capital * 0.5: # Пример: не ставить более 50% капитала
                        current_bet = capital * 0.5
                    if current_bet < initial_bet_size: # Убедитесь, что не опускается ниже начальной ставки
                        current_bet = initial_bet_size

        if capital <= 0: # Проверяем еще раз в конце симуляции, если не было выхода раньше
            ruin_count += 1

    ror = ruin_count / num_simulations
    return ror

# --- Основное выполнение ---
if __name__ == "__main__":
    # 1. Загрузка или генерация данных
    # Для реальных данных:
    # try:
    #     historical_data = pd.read_csv(data_path)
    #     historical_data['Close'] = pd.to_numeric(historical_data['Close'], errors='coerce')
    #     historical_data.dropna(subset=['Close'], inplace=True)
    # except FileNotFoundError:
    #     print(f"Ошибка: Файл данных не найден по пути {data_path}. Генерируем фиктивные данные.")
    #     historical_data = generate_dummy_data()
    # else:
    #     print(f"Данные успешно загружены из {data_path}.")

    # Используем фиктивные данные для демонстрации
    historical_data = generate_dummy_data(num_days=500)
    print("Сгенерированы фиктивные исторические данные.")

    # 2. Запуск бэктеста Мартингейл-стратегии
    backtest_results = run_martingale_backtest(
        data=historical_data.copy(), # Передаем копию, чтобы избежать изменения исходного DataFrame
        initial_capital=INITIAL_CAPITAL,
        initial_bet_size=INITIAL_BET_SIZE,
        martingale_multiplier=MARTINGALE_MULTIPLIER,
        max_consecutive_losses=MAX_CONSECUTIVE_LOSSES,
        profit_target_pct=PROFIT_TARGET_PCT,
        stop_loss_pct=STOP_LOSS_PCT
    )

    # 3. Построение графика кривой капитала
    plt.figure(figsize=(12, 6))
    plt.plot(backtest_results['equity_curve'], label='Кривая капитала')
    plt.title('Кривая капитала Мартингейл-стратегии')
    plt.xlabel('Номер сделки')
    plt.ylabel('Капитал ($)')
    plt.grid(True)
    plt.axhline(y=INITIAL_CAPITAL, color='r', linestyle='--', label='Начальный капитал')
    plt.legend()
    plt.show()

    # 4. Расчет Risk of Ruin (RoR)
    # Используем наблюдаемый процент выигрышей и средние суммы выигрышей/проигрышей из бэктеста для симуляции RoR.
    # Если бэктест сам по себе привел к разорению или было слишком мало сделок, используем начальные предположения.
    if backtest_results['ruined'] or backtest_results['total_trades'] == 0:
        print("\nВнимание: Бэктест привел к разорению или не имел сделок. Расчет RoR может быть менее надежным на основе наблюдаемой статистики.")
        print("Используем начальные предположения для параметров симуляции RoR.")
        # Возвращаемся к начальным предположениям для симуляции RoR, если бэктест был проблематичным
        ror_win_rate = 0.5 # Распространенное предположение для простых стратегий
        ror_avg_win = INITIAL_BET_SIZE * PROFIT_TARGET_PCT
        ror_avg_loss = INITIAL_BET_SIZE * STOP_LOSS_PCT
    else:
        ror_win_rate = backtest_results['win_rate_observed']
        ror_avg_win = backtest_results['avg_win_amount']
        ror_avg_loss = backtest_results['avg_loss_amount']

    risk_of_ruin = simulate_martingale_ror(
        initial_capital=INITIAL_CAPITAL,
        initial_bet_size=INITIAL_BET_SIZE,
        martingale_multiplier=MARTINGALE_MULTIPLIER,
        trade_signal_win_rate=ror_win_rate,
        avg_win_amount=ror_avg_win,
        avg_loss_amount=ror_avg_loss,
        max_consecutive_losses=MAX_CONSECUTIVE_LOSSES,
        num_simulations=NUM_ROR_SIMULATIONS,
        max_trades_per_sim=MAX_TRADES_PER_ROR_SIM
    )

    print(f"\n--- Результаты симуляции Risk of Ruin (RoR) ({NUM_ROR_SIMULATIONS} запусков) ---")
    print(f"Оценочный Risk of Ruin: {risk_of_ruin:.2%}")

Разбор параметров

  • INITIAL_CAPITAL: Начальный капитал, с которым начинается бэктест и симуляция.
  • INITIAL_BET_SIZE: Размер первой ставки в Мартингейл-стратегии.
  • MARTINGALE_MULTIPLIER: Коэффициент, на который увеличивается ставка после каждого проигрыша (например, 2.0 для удвоения).
  • MAX_CONSECUTIVE_LOSSES: Максимальное количество последовательных проигрышей, после которого стратегия сбрасывает ставку до INITIAL_BET_SIZE. Это мера для ограничения экспоненциального роста ставок.
  • PROFIT_TARGET_PCT: Процент от текущей ставки, который составляет прибыль при выигрыше.
  • STOP_LOSS_PCT: Процент от текущей ставки, который составляет убыток при проигрыше.
  • NUM_ROR_SIMULATIONS:: Количество симуляций Монте-Карло для расчета Risk of Ruin. Чем больше, тем точнее оценка.
  • MAX_TRADES_PER_ROR_SIM: Максимальное количество сделок в каждой отдельной симуляции Risk of Ruin.
  • data: Объект pandas.DataFrame, содержащий исторические данные. Обязательно должен быть столбец 'Close' с ценами закрытия.
  • trade_signal_win_rate: Вероятность выигрыша одной сделки, используемая в симуляции RoR. Обычно это наблюдаемый процент выигрышей из бэктеста.
  • avg_win_amount: Средняя сумма выигрыша за сделку, используемая в симуляции RoR.
  • avg_loss_amount: Средняя сумма проигрыша за сделку, используемая в симуляции RoR.

Как запустить

Для запуска скрипта выполните следующие шаги:

  1. Сохраните код: Сохраните приведенный выше код в файл с расширением .py, например, martingale_backtester.py.

  2. Подготовьте данные (опционально): Если вы хотите использовать свои исторические данные вместо сгенерированных фиктивных, подготовьте CSV-файл с колонкой 'Close' (цена закрытия). Раскомментируйте и настройте секцию загрузки данных в блоке if __name__ == "__main__":, указав путь к вашему файлу в переменной data_path.

  3. Настройте параметры: Измените значения глобальных переменных в начале скрипта (INITIAL_CAPITAL, INITIAL_BET_SIZE, MARTINGALE_MULTIPLIER и т.д.) в соответствии с вашими предпочтениями и условиями тестирования.

  4. Запустите скрипт: Откройте терминал или командную строку, перейдите в директорию, где сохранен файл, и выполните команду:

    
    python martingale_backtester.py
    
  5. Проанализируйте результаты: Скрипт выведет в консоль результаты бэктеста (конечный капитал, процент выигрышей, максимальную просадку) и оценочный Risk of Ruin. Также будет построен график кривой капитала.

Дополнительные рекомендации:

Оцените статью
FinFluct