В этой статье я предлагаю рассмотреть профессиональный подход к расчету прибыльности торговых стратегий с помощью Python. Правильная оценка эффективности стратегий является краеугольным камнем успешного алгоритмического трейдинга. Но не все знают как это готовить.
Большинство начинающих трейдеров ограничиваются расчетом общей доходности и процента прибыльных сделок, однако такой подход игнорирует временную структуру доходности и риски. Стратегия может показывать впечатляющую годовую доходность в 50%, но если она достигается ценой 40% просадки, то risk-adjusted доходность окажется неприемлемой для профессиональных стандартов.
Почему общая доходность и процент прибыльности сделок могут давать обманчивые результаты? Основная проблема заключается в том, что традиционные метрики не учитывают распределение доходности во времени. Стратегия, которая генерирует стабильную прибыль каждый месяц, кардинально отличается от стратегии с такой же годовой доходностью, но с большими колебаниями результатов. Временная структура доходности влияет на психологическую устойчивость трейдера, возможности реинвестирования и риск банкротства.
Современные институциональные инвесторы и хедж-фонды используют сложные метрики для оценки стратегий, которые учитывают не только абсолютную доходность, но и риски, просадки, временные характеристики и устойчивость результатов. В этой статье я поделюсь практическим опытом расчета и интерпретации ключевых метрик прибыльности, которые действительно работают в реальных условиях торговли.
Фундаментальные принципы оценки торговых стратегий
Профессиональные управляющие активами руководствуются несколькими ключевыми принципами при оценке стратегий:
- Доходность всегда рассматривается в контексте принятого риска. Стратегия с доходностью 15% годовых и волатильностью 8% предпочтительнее стратегии с доходностью 20% и волатильностью 18%;
- Устойчивость результатов важнее кратковременных всплесков прибыльности. Лучше иметь стратегию с умеренной, но стабильной доходностью, чем стратегию с высокой доходностью, которая может внезапно перестать работать;
- Стратегия должна быть масштабируемой. Она должна сохранять эффективность при увеличении объема торгуемого капитала. Многие арбитражные стратегии показывают отличные результаты на небольших суммах, но их прибыльность быстро снижается при росте объемов из-за влияния на рынок и ограничений ликвидности.
Давайте рассмотрим как можно посчитать ключевые метрики с помощью Python. Для корректного расчета прибыльности необходимо корректно загрузить и предобработать данные. Основой служит временной ряд доходности, который может быть представлен как на уровне отдельных сделок, так и в виде агрегированных периодических данных.
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
pd.set_option('display.expand_frame_repr', False)
import warnings
warnings.filterwarnings('ignore')
# Загрузка данных для демонстрации
ticker = "IEMG" # iShares Core MSCI Emerging Markets IMI Index ETF
start_date = "2022-07-01"
end_date = "2025-07-01"
# Получение данных
data = yf.download(ticker, start=start_date, end=end_date)
# Проверка на MultiIndex и обработка
if isinstance(data.columns, pd.MultiIndex):
data = data.droplevel(1, axis=1)
# Расчет дневной доходности
daily_returns = data['Close'].pct_change().dropna()
# Создание синтетической торговой стратегии для демонстрации
np.random.seed(79)
# Простой random BUY/SELL
strategy_returns = daily_returns * np.random.choice([1, -1], size=len(daily_returns), p=[0.6, 0.4])
# Кладем результаты стратегий в датафрейм
trading_data = pd.DataFrame({
'date': daily_returns.index,
'strategy_returns': strategy_returns.values,
'benchmark_returns': daily_returns.values
})
print("Структура данных для анализа:")
print(trading_data.tail(6))
print(f"\nОбщий период: {trading_data['date'].min()} - {trading_data['date'].max()}")
Структура данных для анализа:
date strategy_returns benchmark_returns
744 2025-06-23 0.008338 0.008338
745 2025-06-24 0.025151 0.025151
746 2025-06-25 0.000672 0.000672
747 2025-06-26 0.006885 0.006885
748 2025-06-27 0.003502 -0.003502
749 2025-06-30 -0.004686 0.004686
Общий период: 2022-07-05 00:00:00 - 2025-06-30 00:00:00
Данный код демонстрирует подготовку базовой структуры данных для анализа торговой стратегии. Я использую ETF на развивающиеся рынки как более интересный пример по сравнению с банальными индексами развитых стран. Важно отметить обработку MultiIndex, которая часто возникает при работе с yfinance для множественных тикеров.
Синтетическая стратегия создается путем случайного изменения знака доходности базового актива, что позволяет продемонстрировать расчет метрик без привязки к конкретному торговому алгоритму. В реальных условиях здесь будут находиться фактические результаты торговли, полученные от брокера или торговой системы.
Базовые метрики доходности и их ограничения
Абсолютная и относительная доходность
Начнем с фундаментальных показателей, которые формируют основу для более сложных метрик. Абсолютная доходность показывает общее изменение стоимости портфеля, в то время как относительная доходность сравнивает результаты стратегии с бенчмарком.
def calculate_basic_returns(returns_series, benchmark_returns=None):
# Кумулятивная доходность
cumulative_returns = (1 + returns_series).cumprod() - 1
total_return = cumulative_returns.iloc[-1]
# Аннуализированная доходность
trading_days = len(returns_series)
years = trading_days / 252 # Приблизительное количество торговых дней в году
annualized_return = (1 + total_return) ** (1/years) - 1
# Средняя дневная доходность
mean_daily_return = returns_series.mean()
# Относительная доходность к бенчмарку
if benchmark_returns is not None:
benchmark_cumulative = (1 + benchmark_returns).cumprod() - 1
benchmark_total = benchmark_cumulative.iloc[-1]
benchmark_annualized = (1 + benchmark_total) ** (1/years) - 1
excess_return = annualized_return - benchmark_annualized
active_return = returns_series - benchmark_returns
tracking_error = active_return.std() * np.sqrt(252)
else:
excess_return = None
tracking_error = None
return {
'total_return': total_return,
'annualized_return': annualized_return,
'mean_daily_return': mean_daily_return,
'excess_return': excess_return,
'tracking_error': tracking_error
}
# Расчет базовых метрик
basic_metrics = calculate_basic_returns(
trading_data['strategy_returns'],
trading_data['benchmark_returns']
)
print("Базовые метрики доходности:")
for metric, value in basic_metrics.items():
if value is not None:
print(f"{metric}: {value:.4f}")
Базовые метрики доходности:
total_return: 0.2589
annualized_return: 0.0804
mean_daily_return: 0.0004
excess_return: -0.0239
tracking_error: 0.2203
Этот код демонстрирует расчет фундаментальных метрик доходности с учетом аннуализации и сравнения с бенчмарком. Вот как их можно интерпретировать:
- Total return (25.89%) — стратегия принесла 26% за весь период;
- Annualized return (8.04%) — среднегодовая доходность 8%;
- Mean daily return (0.04%) — в среднем +0.04% в день;
- Excess return (-2.39%) — стратегия отстает от бенчмарка на 2.4% годовых;
- Tracking error (22.03%) — стратегия сильно отклоняется от бенчмарка (высокая волатильность относительно него)
Вывод: Стратегия показывает положительную доходность (8% годовых), но работает хуже бенчмарка и имеет высокую волатильность. Для окончательной оценки нужно также посмотреть остальные метрики.
Как видите даже такая несложная функция дает нам множество полезной информации об эффективности биржевой стратегии. Важно отметить использование 252 торговых дней для аннуализации, что является стандартом в индустрии. Tracking error измеряет волатильность активной доходности относительно бенчмарка, что позволяет оценить консистентность стратегии.
Мы посчитали 5 метрик, но этого недостаточно. Основное ограничение этих метрик заключается в том, что они не учитывают риски и временную структуру доходности. Высокая аннуализированная доходность может скрывать периоды значительных убытков, которые делают стратегию неприемлемой для практического использования.
Расчет других показателей эффективности
Важно не ограничивать себя только метриками доходности и процентом прибыльных сделок. Стратегия может иметь 70% прибыльных сделок, но если средний убыток значительно превышает среднюю прибыль, общий результат будет отрицательным. Это особенно характерно для стратегий типа «carry trade» или продажи опционов, где небольшие постоянные прибыли перекрываются редкими, но крупными убытками.
def analyze_trade_characteristics(returns_series):
"""
Анализ характеристик отдельных торговых периодов
"""
# Разделение на прибыльные и убыточные периоды
profitable_periods = returns_series[returns_series > 0]
losing_periods = returns_series[returns_series < 0] # Базовые статистики win_rate = len(profitable_periods) / len(returns_series) avg_win = profitable_periods.mean() if len(profitable_periods) > 0 else 0
avg_loss = losing_periods.mean() if len(losing_periods) > 0 else 0
# Profit factor - отношение суммы прибылей к сумме убытков
total_profits = profitable_periods.sum()
total_losses = abs(losing_periods.sum())
profit_factor = total_profits / total_losses if total_losses > 0 else np.inf
# Expectancy - математическое ожидание одного торгового периода
expectancy = win_rate * avg_win + (1 - win_rate) * avg_loss
return {
'win_rate': win_rate,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'expectancy': expectancy,
'total_periods': len(returns_series),
'profitable_periods': len(profitable_periods),
'losing_periods': len(losing_periods)
}
# Анализ характеристик сделок
trade_stats = analyze_trade_characteristics(trading_data['strategy_returns'])
print("Характеристики торговых периодов:")
for metric, value in trade_stats.items():
if isinstance(value, float):
print(f"{metric}: {value:.4f}")
else:
print(f"{metric}: {value}")
Характеристики торговых периодов:
win_rate: 0.5213
avg_win: 0.0081
avg_loss: -0.0081
profit_factor: 1.0953
expectancy: 0.0003
total_periods: 750
profitable_periods: 391
losing_periods: 355
Данный анализ показывает ключевые характеристики торговых результатов, которые помогают понять структуру прибыльности стратегии. Вот как можно интепретировать эти метрики:
- Win rate (52.13%) — больше половины дней прибыльные, что хорошо;
- Avg win (+0.81%) vs Avg loss (-0.81%) — средний выигрыш равен среднему проигрышу;
- Profit factor (1.095) — на каждый доллар убытков приходится $1.095 прибыли (чуть выше точки безубыточности);
- Expectancy (+0.03%) — математическое ожидание слабо положительное;
- Profitable periods (750) vs Losing periods (355): 391 прибыльных vs 355 убыточных дней.
Вывод: Стратегия сбалансированная — чуть больше выигрышей, чем проигрышей, но средние размеры прибыли и убытков почти равны. Это объясняет скромное превосходство над случайным результатом. Стратегия работает, но без явного преимущества — типичный результат для синтетической стратегии со случайными сигналами.
Среди рассмотренных выше метрик я считаю Profit factor наиболее ценной метрикой, поскольку он показывает соотношение между общей прибылью и общими убытками. Значение больше 1 указывает на прибыльность стратегии, но для практического использования желательно иметь profit factor выше 1.5.
Еще одна метрика, на которую я постоянно обращаю внимание, это Expectancy, или по-русски математическое ожидание доходности за один торговый период, что позволяет оценить долгосрочную жизнеспособность стратегии. Положительное значение expectancy является необходимым условием для прибыльной торговли в долгосрочной перспективе.
Риск-адаптированные метрики эффективности
Коэффициент Шарпа и его модификации
Коэффициент Шарпа остается золотым стандартом для оценки риск-адаптированной доходности в количественных финансах. Однако его применение требует понимания ограничений и правильной интерпретации результатов.
def calculate_sharpe_ratio(returns_series, risk_free_rate=0.02):
"""
Расчет коэффициента Шарпа и его модификаций
"""
# Конвертация годовой безрисковой ставки в дневную
daily_risk_free = risk_free_rate / 252
# Избыточная доходность
excess_returns = returns_series - daily_risk_free
# Классический коэффициент Шарпа
sharpe_ratio = excess_returns.mean() / excess_returns.std() * np.sqrt(252)
# Модифицированный коэффициент Шарпа с учетом асимметрии
# Используется для стратегий с несимметричным распределением доходности
downside_returns = excess_returns[excess_returns < 0] if len(downside_returns) > 0:
downside_deviation = downside_returns.std() * np.sqrt(252)
sortino_ratio = excess_returns.mean() * np.sqrt(252) / downside_deviation
else:
sortino_ratio = np.inf
# Коэффициент Калмара (Calmar Ratio)
# Отношение аннуализированной доходности к максимальной просадке
cumulative_returns = (1 + returns_series).cumprod()
running_max = cumulative_returns.expanding().max()
drawdown = (cumulative_returns - running_max) / running_max
max_drawdown = drawdown.min()
annualized_return = returns_series.mean() * 252
calmar_ratio = annualized_return / abs(max_drawdown) if max_drawdown < 0 else np.inf
return {
'sharpe_ratio': sharpe_ratio,
'sortino_ratio': sortino_ratio,
'calmar_ratio': calmar_ratio,
'max_drawdown': max_drawdown,
'annualized_return': annualized_return,
'volatility': returns_series.std() * np.sqrt(252)
}
# Расчет риск-адаптированных метрик
risk_metrics = calculate_sharpe_ratio(trading_data['strategy_returns'])
print("Риск-адаптированные метрики:")
for metric, value in risk_metrics.items():
print(f"{metric}: {value:.4f}")
Риск-адаптированные метрики:
sharpe_ratio: 0.4203
sortino_ratio: 0.0375
calmar_ratio: 0.4252
max_drawdown: -0.2167
annualized_return: 0.0921
volatility: 0.1716
Этот код демонстрирует расчет трех ключевых риск-адаптированных метрик, каждая из которых фокусируется на различных аспектах риска. Вот краткая интерпретация риск-метрик:
- Sharpe ratio (0.42) — умеренное качество (норма >0.5, хорошо >1.0);
- Sortino ratio (0.0375) — очень низкое (стратегия плохо компенсирует негативную волатильность);
- Calmar ratio (0.43) — умеренное соотношение доходности к максимальной просадке;
- Max drawdown (-21.67%) — максимальная просадка довольно высокая;
- Volatility (17.16%) — умеренная волатильность;
- Annualized return (9.21%) — неплохая годовая доходность
Вывод: Стратегия показывает приемлемую доходность (9.2% годовых), но с существенными рисками — высокая просадка (22%) и низкое качество доходности по Sortino (Sortino в 0.0375 указывает на плохое управление downside-риском). Риск-доходность посредственная — стратегию можно рассматривать, но нужно быть готовым к значительным временным убыткам.
Итак, мы получили еще больше информации о потенциальной прибыльности стратегии. Все эти метрики тоже крайне популярны в количественном анализе. Однако важно помнить про их ограничения в применимости:
- Классический коэффициент Шарпа предполагает нормальное распределение доходности, что часто не соответствует реальности финансовых рынков;
- Коэффициент Сортино исправляет этот недостаток, учитывая только негативные отклонения от среднего значения;
- Коэффициент Калмара, на мой взгляд, особенно важен для практического трейдинга, поскольку максимальная просадка непосредственно влияет на психологическую устойчивость трейдера и требования к капиталу. Стратегия с коэффициентом Калмара выше 1.0 считается привлекательной для профессиональных управляющих.
Важно понимать, что все эти метрики основываются на исторических данных и не гарантируют будущих результатов. Однако они позволяют сравнивать стратегии на объективной основе и выявлять потенциальные проблемы до начала реальной торговли.
Анализ просадок и восстановления
Просадки являются неизбежной частью любой торговой стратегии, и их правильный анализ помогает понять устойчивость стратегии к неблагоприятным рыночным условиям.
def analyze_drawdowns(returns_series):
# Расчет кумулятивной доходности
cumulative_returns = (1 + returns_series).cumprod()
# Расчет running maximum
running_max = cumulative_returns.expanding().max()
# Просадка в каждый момент времени
drawdown = (cumulative_returns - running_max) / running_max
# Поиск периодов просадок
drawdown_periods = []
in_drawdown = False
start_idx = None
for i, dd in enumerate(drawdown):
if dd < 0 and not in_drawdown: # Начало просадки in_drawdown = True start_idx = i elif dd >= 0 and in_drawdown:
# Конец просадки
in_drawdown = False
end_idx = i - 1
# Характеристики просадки
period_drawdown = drawdown[start_idx:end_idx+1]
max_dd = period_drawdown.min()
duration = end_idx - start_idx + 1
drawdown_periods.append({
'start': start_idx,
'end': end_idx,
'max_drawdown': max_dd,
'duration': duration
})
# Общие статистики просадок
if drawdown_periods:
max_drawdown = min([dd['max_drawdown'] for dd in drawdown_periods])
avg_drawdown = np.mean([dd['max_drawdown'] for dd in drawdown_periods])
max_duration = max([dd['duration'] for dd in drawdown_periods])
avg_duration = np.mean([dd['duration'] for dd in drawdown_periods])
# Коэффициент восстановления
recovery_factor = returns_series.mean() * 252 / abs(max_drawdown)
else:
max_drawdown = 0
avg_drawdown = 0
max_duration = 0
avg_duration = 0
recovery_factor = np.inf
return {
'max_drawdown': max_drawdown,
'avg_drawdown': avg_drawdown,
'max_drawdown_duration': max_duration,
'avg_drawdown_duration': avg_duration,
'recovery_factor': recovery_factor,
'drawdown_periods_count': len(drawdown_periods),
'drawdown_series': drawdown
}
# Анализ просадок
drawdown_analysis = analyze_drawdowns(trading_data['strategy_returns'])
print("Анализ просадок:")
for metric, value in drawdown_analysis.items():
if metric != 'drawdown_series':
if isinstance(value, float):
print(f"{metric}: {value:.4f}")
else:
print(f"{metric}: {value}")
Анализ просадок:
max_drawdown: -0.2167
avg_drawdown: -0.0409
max_drawdown_duration: 490
avg_drawdown_duration: 39.7778
recovery_factor: 0.4252
drawdown_periods_count: 18
Детальный анализ просадок позволяет понять не только их глубину, но и продолжительность, что крайне важно для оценки практической применимости стратегии. Вот как можно интепретировать результаты последнего расчета:
- Max drawdown (-21.67%) — максимальная потеря от пика составила 22%;
- Avg drawdown (-4.09%) — типичная просадка около 4%;
- Max drawdown duration (490 дней) — самая долгая просадка длилась 1.3 года!
- Avg drawdown duration (39.8 дней) — обычно восстановление занимает ~1.3 месяца;
- Recovery factor (0.43) — слабая способность восстанавливаться после просадок;
- Drawdown periods count (18) — всего было 18 периодов просадок.
Вывод: Стратегия имеет серьезные проблемы с просадками:
- Очень долгое восстановление (до 1.3 года);
- Слабая способность стратегии «отыгрываться»;
- Инвестору нужна высокая психологическая устойчивость
490 дней в просадке — это очень долго для любой стратегии. Многие инвесторы не выдержат такой период без прибыли. Длительные периоды просадок могут привести к психологическому давлению и преждевременному прекращению торговли даже при положительном математическом ожидании.
Коэффициент восстановления (recovery factor) показывает, насколько быстро стратегия способна компенсировать убытки. Высокое значение указывает на устойчивость стратегии и ее способность к быстрому восстановлению после неблагоприятных периодов. Однако здесь мы его тоже не наблюдаем.
Продвинутые метрики для профессиональной оценки
Value at Risk и Expected Shortfall
Value at Risk (VaR) и Expected Shortfall (ES) являются стандартными инструментами риск-менеджмента в институциональных фондах. Эти метрики позволяют количественно оценить потенциальные убытки в экстремальных сценариях.
def calculate_risk_metrics(returns_series, confidence_levels=[0.95, 0.99]):
"""
Расчет VaR и Expected Shortfall для различных уровней доверия
"""
results = {}
for confidence in confidence_levels:
# Historical VaR (квантильный подход)
var_historical = np.percentile(returns_series, (1 - confidence) * 100)
# Expected Shortfall (Conditional VaR)
# Средние убытки, превышающие VaR
tail_losses = returns_series[returns_series <= var_historical] expected_shortfall = tail_losses.mean() if len(tail_losses) > 0 else 0
# Параметрический VaR (предполагает нормальное распределение)
mean_return = returns_series.mean()
std_return = returns_series.std()
z_score = -np.percentile(np.random.normal(0, 1, 10000), (1 - confidence) * 100)
var_parametric = mean_return + z_score * std_return
results[f'VaR_{int(confidence*100)}'] = {
'historical': var_historical,
'parametric': var_parametric,
'expected_shortfall': expected_shortfall
}
return results
# Расчет метрик риска
risk_analysis = calculate_risk_metrics(trading_data['strategy_returns'])
print("Анализ рисков (VaR и Expected Shortfall):")
for confidence, metrics in risk_analysis.items():
print(f"\n{confidence}:")
for metric, value in metrics.items():
print(f" {metric}: {value:.4f}")
Анализ рисков (VaR и Expected Shortfall):
VaR_95:
historical: -0.0173
parametric: 0.0178
expected_shortfall: -0.0241
VaR_99:
historical: -0.0242
parametric: 0.0263
expected_shortfall: -0.0390
VaR показывает максимальные ожидаемые убытки при заданном уровне доверия, но не учитывает размер убытков в хвосте распределения. Expected Shortfall исправляет этот недостаток, показывая средние убытки в наихудших сценариях. Сравнение исторического и параметрического VaR помогает понять, насколько распределение доходности отличается от нормального.
Вот краткая интерпретация рассчитанных VaR и Expected Shortfall:
- VaR 95% (риск в 5% случаев): Historical (-1.73%) — в 5% худших дней потери превышают 1.73%, Parametric (1.78%) — параметрическая модель показывает похожий результат;
- VaR 99% (риск в 1% случаев): Historical (-2.42%) — в 1% худших дней потери превышают 2.42%, Parametric (2.63%) — параметрическая оценка чуть выше;
- Expected Shortfall (хвостовый риск): 95% (-2.41%) — если попали в худшие 5%, средняя потеря составит 2.41%;
- ES 99% (-3.90%) — если попали в худший 1%, средняя потеря составит 3.90%.
Вывод: Стратегия имеет умеренный хвостовой риск:
- В обычные «плохие» дни (5%) потери ~1.7-2.4%;
- В самые худшие дни (1%) потери могут достигать ~4%;
- Каждый месяц можно ожидать 1-2 дня с потерями >1.7%;
- Риски предсказуемы и не экстремальны.
Эти метрики особенно важны для стратегий с асимметричным распределением доходности, таких как стратегии продажи опционов или carry trades, где редкие экстремальные события могут привести к катастрофическим убыткам.
Коэффициент информации и активные риски
Коэффициент информации (Information Ratio) измеряет способность стратегии генерировать избыточную доходность относительно бенчмарка с учетом принятых активных рисков.
def calculate_information_ratio(strategy_returns, benchmark_returns):
"""
Расчет коэффициента информации и связанных метрик
"""
# Активная доходность
active_returns = strategy_returns - benchmark_returns
# Коэффициент информации
information_ratio = active_returns.mean() / active_returns.std() * np.sqrt(252)
# Коэффициент корреляции с бенчмарком
correlation = np.corrcoef(strategy_returns, benchmark_returns)[0, 1]
# Beta относительно бенчмарка
covariance = np.cov(strategy_returns, benchmark_returns)[0, 1]
benchmark_variance = benchmark_returns.var()
beta = covariance / benchmark_variance
# Alpha (избыточная доходность с учетом риска)
strategy_mean = strategy_returns.mean() * 252
benchmark_mean = benchmark_returns.mean() * 252
alpha = strategy_mean - beta * benchmark_mean
# Коэффициент детерминации (R-squared)
r_squared = correlation ** 2
# Активный риск (tracking error)
tracking_error = active_returns.std() * np.sqrt(252)
return {
'information_ratio': information_ratio,
'alpha': alpha,
'beta': beta,
'correlation': correlation,
'r_squared': r_squared,
'tracking_error': tracking_error,
'active_return': active_returns.mean() * 252
}
# Расчет коэффициента информации
info_metrics = calculate_information_ratio(
trading_data['strategy_returns'],
trading_data['benchmark_returns']
)
print("Метрики активного управления:")
for metric, value in info_metrics.items():
print(f"{metric}: {value:.4f}")
Метрики активного управления:
information_ratio: -0.0991
alpha: 0.0721
beta: 0.1754
correlation: 0.1753
r_squared: 0.0307
tracking_error: 0.2203
active_return: -0.0218
Вот как можно интепретировать результаты метрик активного управления:
Качество активного управления:
- Information ratio (-0.099) — плохое качество активного управления (отрицательное значение);
- Alpha (7.21%) — стратегия может генерировать дополнительную доходность независимо от рынка;
- Active return (-2.18%) — стратегия отстает от бенчмарка на 2.18% годовых.
Связь с рынком и волатильность:
- Beta (0.175) — очень низкая чувствительность к рынку (стратегия почти независима);
- Correlation (0.175) — слабая корреляция с бенчмарком;
- R-squared (3.07%) — только 3% движений объясняются рынком;
- Tracking error (22.03%) — высокие отклонения от бенчмарка.
Вывод: Стратегия практически независима от рынка (низкие beta и корреляция), однако это не приносит пользы — она отстает от бенчмарка при высокой волатильности. Отрицательный information ratio показывает, что дополнительный риск не компенсируется доходностью.
Коэффициент информации является одной из наиболее важных метрик для оценки активных стратегий. Значение выше 0.5 считается хорошим результатом, а выше 1.0 — выдающимся. Высокий коэффициент информации указывает на способность стратегии генерировать стабильную избыточную доходность при контролируемом уровне активного риска.
Alpha и beta позволяют понять, насколько стратегия зависит от движений бенчмарка и какую дополнительную стоимость она создает. Низкая корреляция с бенчмарком может быть как преимуществом (диверсификация), так и недостатком (отклонение от мандата управляющего).
Временная стабильность и робастность метрик
Анализ стабильности показателей во времени
Одной из главных проблем оценки торговых стратегий является нестабильность метрик во времени. Стратегия может показывать отличные результаты на определенном временном отрезке, но кардинально менять характеристики при изменении рыночных условий. Профессиональный анализ требует оценки устойчивости метрик к различным временным периодам.
def rolling_metrics_analysis(returns_series, window_size=252):
"""
Анализ скользящих метрик для оценки временной стабильности
"""
# Инициализация результатов
rolling_sharpe = []
rolling_sortino = []
rolling_max_dd = []
rolling_volatility = []
# Расчет скользящих метрик
for i in range(window_size, len(returns_series)):
window_returns = returns_series.iloc[i-window_size:i]
# Коэффициент Шарпа
excess_returns = window_returns - 0.02/252 # 2% годовых безрисковая ставка
sharpe = excess_returns.mean() / excess_returns.std() * np.sqrt(252)
rolling_sharpe.append(sharpe)
# Коэффициент Сортино
downside_returns = excess_returns[excess_returns < 0] if len(downside_returns) > 0:
sortino = excess_returns.mean() / downside_returns.std() * np.sqrt(252)
else:
sortino = np.inf
rolling_sortino.append(sortino)
# Максимальная просадка
cumulative = (1 + window_returns).cumprod()
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
max_dd = drawdown.min()
rolling_max_dd.append(abs(max_dd))
# Волатильность
volatility = window_returns.std() * np.sqrt(252)
rolling_volatility.append(volatility)
# Создание DataFrame с результатами
dates = returns_series.index[window_size:]
rolling_metrics = pd.DataFrame({
'date': dates,
'rolling_sharpe': rolling_sharpe,
'rolling_sortino': rolling_sortino,
'rolling_max_dd': rolling_max_dd,
'rolling_volatility': rolling_volatility
})
# Статистики стабильности
stability_stats = {
'sharpe_std': np.std(rolling_sharpe),
'sharpe_min': np.min(rolling_sharpe),
'sharpe_max': np.max(rolling_sharpe),
'periods_negative_sharpe': sum(1 for x in rolling_sharpe if x < 0),
'max_dd_std': np.std(rolling_max_dd),
'max_dd_worst': np.max(rolling_max_dd)
}
return rolling_metrics, stability_stats
# Анализ временной стабильности
rolling_data, stability = rolling_metrics_analysis(trading_data['strategy_returns'])
print("Статистики временной стабильности:")
for metric, value in stability.items():
if isinstance(value, float):
print(f"{metric}: {value:.4f}")
else:
print(f"{metric}: {value}")
# Показываем последние значения скользящих метрик
print("\nПоследние значения скользящих метрик:")
print(rolling_data.tail())
Статистики временной стабильности:
sharpe_std: 0.7275
sharpe_min: -1.1744
sharpe_max: 1.5318
periods_negative_sharpe: 212
max_dd_std: 0.0547
max_dd_worst: 0.2167
Последние значения скользящих метрик:
date rolling_sharpe rolling_sortino rolling_max_dd rolling_volatility
493 745 0.805268 0.977761 0.177635 0.187222
494 746 0.918937 1.125228 0.177635 0.188807
495 747 0.938446 1.145312 0.177635 0.188770
496 748 0.973427 1.188619 0.177635 0.188867
497 749 1.003858 1.222350 0.177635 0.188862
Данный анализ позволяет выявить периоды, когда стратегия работала неэффективно, и оценить общую стабильность результатов. Интерпретация полученных показателей может быть следующей:
- Sharpe std (0.73) — высокая волатильность качества стратегии;
- Sharpe min (-1.17) vs max (1.53) — огромный разброс от очень плохого до хорошего;
- Periods negative Sharpe (212) — в 212 периодах качество было отрицательным;
- Max DD std (0.055) — умеренная волатильность просадок;
- Max DD worst (21.67%) — подтверждение максимальной просадки
Вывод: Стратегия крайне нестабильна во времени. Ее качество сильно «скачет» — от ужасного до приличного. Почти в трети периодов (212 из ~750) стратегия показывала отрицательный risk-adjusted результат, высокая волатильность Sharpe (0.73) означает, что нельзя полагаться на стабильность результатов. Стратегия непредсказуема — сегодня может быть отличной, завтра провальной.
Высокое стандартное отклонение коэффициента Шарпа указывает на нестабильность стратегии, что может сигнализировать о переоптимизации или зависимости от специфических рыночных условий.
Особое внимание следует обратить на количество периодов с отрицательным коэффициентом Шарпа. Если таких периодов много, это может указывать на фундаментальные проблемы стратегии, которые не очевидны при взгляде на общие результаты. Профессиональные управляющие активами обычно требуют, чтобы стратегия показывала положительный коэффициент Шарпа не менее чем в 70% скользящих окон.
Тестирование на различных рыночных режимах
Эффективная торговая стратегия должна демонстрировать устойчивость к различным рыночным условиям. Анализ эффективности биржевой стратегии в разных рыночных режимах помогает понять ограничения стратегии и потенциальные риски.
def regime_based_analysis(strategy_returns, benchmark_returns, volatility_threshold=0.02):
"""
Анализ эффективности стратегии в различных рыночных режимах
"""
# Расчет скользящей волатильности бенчмарка для определения режимов
rolling_vol = benchmark_returns.rolling(window=20).std() * np.sqrt(252)
# Классификация рыночных режимов
high_vol_periods = rolling_vol > rolling_vol.quantile(0.75)
low_vol_periods = rolling_vol < rolling_vol.quantile(0.25) normal_vol_periods = ~(high_vol_periods | low_vol_periods) # Определение трендовых и боковых рынков rolling_returns = benchmark_returns.rolling(window=20).sum() uptrend_periods = rolling_returns > rolling_returns.quantile(0.7)
downtrend_periods = rolling_returns < rolling_returns.quantile(0.3) sideways_periods = ~(uptrend_periods | downtrend_periods) # Функция для расчета метрик по режиму def calculate_regime_metrics(mask, regime_name): if mask.sum() == 0: return None regime_strategy = strategy_returns[mask] regime_benchmark = benchmark_returns[mask] # Базовые метрики total_return = (1 + regime_strategy).prod() - 1 benchmark_return = (1 + regime_benchmark).prod() - 1 excess_return = total_return - benchmark_return # Риск-адаптированные метрики if regime_strategy.std() > 0:
sharpe = regime_strategy.mean() / regime_strategy.std() * np.sqrt(252)
else:
sharpe = 0
win_rate = (regime_strategy > 0).mean()
return {
'regime': regime_name,
'periods': mask.sum(),
'total_return': total_return,
'benchmark_return': benchmark_return,
'excess_return': excess_return,
'sharpe_ratio': sharpe,
'win_rate': win_rate,
'avg_return': regime_strategy.mean(),
'volatility': regime_strategy.std() * np.sqrt(252)
}
# Анализ по режимам волатильности
vol_results = []
vol_results.append(calculate_regime_metrics(high_vol_periods, 'High Volatility'))
vol_results.append(calculate_regime_metrics(normal_vol_periods, 'Normal Volatility'))
vol_results.append(calculate_regime_metrics(low_vol_periods, 'Low Volatility'))
# Анализ по трендовым режимам
trend_results = []
trend_results.append(calculate_regime_metrics(uptrend_periods, 'Uptrend'))
trend_results.append(calculate_regime_metrics(sideways_periods, 'Sideways'))
trend_results.append(calculate_regime_metrics(downtrend_periods, 'Downtrend'))
return vol_results, trend_results
# Анализ по рыночным режимам
vol_analysis, trend_analysis = regime_based_analysis(
trading_data['strategy_returns'],
trading_data['benchmark_returns']
)
print("Анализ по режимам волатильности:")
for result in vol_analysis:
if result:
print(f"\n{result['regime']}:")
for key, value in result.items():
if key != 'regime' and isinstance(value, (int, float)):
print(f" {key}: {value:.4f}")
elif key != 'regime':
print(f" {key}: {value}")
print("\n" + "="*50)
print("Анализ по трендовым режимам:")
for result in trend_analysis:
if result:
print(f"\n{result['regime']}:")
for key, value in result.items():
if key != 'regime' and isinstance(value, (int, float)):
print(f" {key}: {value:.4f}")
elif key != 'regime':
print(f" {key}: {value}")
Анализ по режимам волатильности:
High Volatility:
periods: 183
total_return: 0.3141
benchmark_return: 0.1613
excess_return: 0.1528
sharpe_ratio: 1.6996
win_rate: 0.5246
avg_return: 0.0016
volatility: 0.2382
Normal Volatility:
periods: 384
total_return: -0.0448
benchmark_return: 0.2297
excess_return: -0.2744
sharpe_ratio: -0.1234
win_rate: 0.5260
avg_return: -0.0001
volatility: 0.1511
Low Volatility:
periods: 183
total_return: 0.0029
benchmark_return: -0.0590
excess_return: 0.0619
sharpe_ratio: 0.0944
win_rate: 0.5082
avg_return: 0.0000
volatility: 0.1260
==================================================
Анализ по трендовым режимам:
Uptrend:
periods: 219
total_return: 0.1974
benchmark_return: 0.7136
excess_return: -0.5162
sharpe_ratio: 1.3950
win_rate: 0.5388
avg_return: 0.0009
volatility: 0.1575
Sideways:
periods: 312
total_return: 0.1676
benchmark_return: 0.1920
excess_return: -0.0244
sharpe_ratio: 0.8740
win_rate: 0.5353
avg_return: 0.0005
volatility: 0.1574
Downtrend:
periods: 219
total_return: -0.0995
benchmark_return: -0.3421
excess_return: 0.2426
sharpe_ratio: -0.4954
win_rate: 0.4840
avg_return: -0.0004
volatility: 0.2021
Этот анализ крайне важен для понимания характера стратегии и ее потенциальных слабых мест. Многие стратегии показывают отличные результаты в определенных рыночных условиях, но терпят крах при изменении режима. Например, momentum стратегии обычно хорошо работают в трендовых рынках, но могут показывать плохие результаты в боковых движениях.
Метрик тут посчитано довольно много. Давайте рассмотрим ключевые по размеру отклонений и что они значат на языке инвестора:
- Normal Volatility Sharpe (-0.12) — в обычных рыночных условиях стратегия дает плохую risk-adjusted доходность, хотя win rate 52.6%;
- Normal Volatility excess return (-27.44%) — в спокойные периоды стратегия отстает от индекса на 27%, то есть простой buy-and-hold индекса дал бы на 27% больше прибыли;
- Uptrend Excess return (-51.62%) — в растущем рынке стратегия недополучает 52% от роста индекса;
- Normal Volatility total return (-4.48%) — в период нормальной волатильности стратегия показала убыток 4.5%;
- High Volatility Sharpe (1.70) — в кризисы стратегия показывает отличную risk-adjusted доходность;
- High Volatility excess return (+15.28%) — в стрессовые периоды опережает индекс на 15%;
- Downtrend Excess return (+24.26%) — в падающем рынке превосходит индекс на 24%;
- Uptrend Performance gap (71% vs 20%) — огромная разница в абсолютной доходности в растущем рынке;
- Downtrend win rate (48.40%) — единственный режим где меньше половины дней прибыльные;
- High vs Normal Volatility gap (31.41% vs -4.48%) — разница в доходности 36% между кризисными и спокойными периодами показывает экстремальную режимную зависимость;
- Volatility clustering effect — стратегия лучше всего работает именно тогда, когда волатильность высокая (238% vs 151-126%).
Если можно сделать вывод в двух словах: Это не стратегия роста, а дорогая страховка от кризисов. Стратегия зарабатывает когда рынки падают/стрессуют и теряет когда рынки растут — классический hedge-профиль.
Профессиональные управляющие особое внимание уделяют поведению стратегии в периоды высокой волатильности, поскольку именно в эти моменты происходят наибольшие потери капитала. Стратегия, которая сохраняет положительную доходность в периоды рыночного стресса, может представлять ценность для диверсификации портфеля.
Сравнение результатов с эталонными бенчмарками
Интерпретация метрик существенно зависит от типа торговой стратегии и рыночного сегмента. То, что считается отличным результатом для одного типа стратегий, может быть неприемлемым для другого.
Для equity long-short стратегий профессиональные стандарты предполагают коэффициент Шарпа выше 1.0, максимальную просадку не более 15%, и коэффициент информации относительно рыночного индекса выше 0.5. Высокочастотные стратегии обычно имеют более высокие коэффициенты Шарпа (2.0+), но требуют учета транзакционных издержек и влияния на рынок.
Арбитражные стратегии должны демонстрировать очень высокую стабильность результатов с коэффициентом Сортино выше 2.0 и минимальными просадками. В то же время, momentum стратегии характеризуются более высокой волатильностью, и для них приемлемы большие просадки при условии высокой долгосрочной доходности.
Используя Python мы можем написать функцию для профессиональной оценки торговых стратегий путем сравнения ключевых метрик с отраслевыми стандартами. Функция будет анализировать 8 важных показателей (Sharpe ratio, максимальная просадка, Information ratio, win rate, Sortino ratio, Calmar ratio, годовая доходность и волатильность) и сопоставлять их с бенчмарками для различных типов стратегий: equity long/short, market neutral, momentum и mean reversion.
def benchmark_strategy_performance(metrics, strategy_type='equity_long_short'):
"""
Сравнение метрик стратегии с профессиональными бенчмарками
"""
# Определение бенчмарков для различных типов стратегий
benchmarks = {
'equity_long_short': {
'sharpe_ratio': {'excellent': 1.5, 'good': 1.0, 'acceptable': 0.5},
'max_drawdown': {'excellent': 0.08, 'good': 0.12, 'acceptable': 0.20},
'information_ratio': {'excellent': 0.8, 'good': 0.5, 'acceptable': 0.2},
'win_rate': {'excellent': 0.60, 'good': 0.55, 'acceptable': 0.50},
'sortino_ratio': {'excellent': 1.5, 'good': 1.0, 'acceptable': 0.7},
'calmar_ratio': {'excellent': 1.2, 'good': 0.8, 'acceptable': 0.5},
'annualized_return': {'excellent': 0.15, 'good': 0.12, 'acceptable': 0.08},
'volatility': {'excellent': 0.12, 'good': 0.18, 'acceptable': 0.25}
},
'market_neutral': {
'sharpe_ratio': {'excellent': 2.0, 'good': 1.5, 'acceptable': 1.0},
'max_drawdown': {'excellent': 0.05, 'good': 0.08, 'acceptable': 0.12},
'correlation': {'excellent': 0.1, 'good': 0.2, 'acceptable': 0.3},
'sortino_ratio': {'excellent': 2.5, 'good': 2.0, 'acceptable': 1.5},
'information_ratio': {'excellent': 1.5, 'good': 1.0, 'acceptable': 0.5},
'volatility': {'excellent': 0.08, 'good': 0.12, 'acceptable': 0.16},
'annualized_return': {'excellent': 0.12, 'good': 0.08, 'acceptable': 0.05}
},
'momentum': {
'sharpe_ratio': {'excellent': 1.2, 'good': 0.8, 'acceptable': 0.4},
'max_drawdown': {'excellent': 0.15, 'good': 0.25, 'acceptable': 0.35},
'calmar_ratio': {'excellent': 1.0, 'good': 0.6, 'acceptable': 0.3},
'win_rate': {'excellent': 0.58, 'good': 0.53, 'acceptable': 0.48},
'sortino_ratio': {'excellent': 1.0, 'good': 0.6, 'acceptable': 0.3},
'volatility': {'excellent': 0.15, 'good': 0.20, 'acceptable': 0.30},
'annualized_return': {'excellent': 0.18, 'good': 0.12, 'acceptable': 0.08}
},
'mean_reversion': {
'sharpe_ratio': {'excellent': 1.8, 'good': 1.2, 'acceptable': 0.8},
'max_drawdown': {'excellent': 0.10, 'good': 0.15, 'acceptable': 0.25},
'win_rate': {'excellent': 0.65, 'good': 0.60, 'acceptable': 0.55},
'sortino_ratio': {'excellent': 2.0, 'good': 1.5, 'acceptable': 1.0},
'calmar_ratio': {'excellent': 1.5, 'good': 1.0, 'acceptable': 0.6},
'volatility': {'excellent': 0.10, 'good': 0.15, 'acceptable': 0.20},
'annualized_return': {'excellent': 0.15, 'good': 0.10, 'acceptable': 0.06}
}
}
if strategy_type not in benchmarks:
print(f"Бенчмарки для типа стратегии '{strategy_type}' не определены")
return
benchmark = benchmarks[strategy_type]
assessment = {}
print(f"\nОЦЕНКА СТРАТЕГИИ ТИПА: {strategy_type.upper()}")
print("="*60)
for metric_name, thresholds in benchmark.items():
if metric_name in metrics:
value = metrics[metric_name]
# Для метрик типа drawdown, volatility, correlation логика обратная (меньше = лучше)
if any(word in metric_name for word in ['drawdown', 'correlation', 'volatility']):
if abs(value) <= thresholds['excellent']:
rating = '🟢 ОТЛИЧНО'
elif abs(value) <= thresholds['good']:
rating = '🟡 ХОРОШО'
elif abs(value) <= thresholds['acceptable']: rating = '🟠 ПРИЕМЛЕМО' else: rating = '🔴 НЕУДОВЛЕТВОРИТЕЛЬНО' else: if value >= thresholds['excellent']:
rating = '🟢 ОТЛИЧНО'
elif value >= thresholds['good']:
rating = '🟡 ХОРОШО'
elif value >= thresholds['acceptable']:
rating = '🟠 ПРИЕМЛЕМО'
else:
rating = '🔴 НЕУДОВЛЕТВОРИТЕЛЬНО'
assessment[metric_name] = rating
if isinstance(value, float):
if any(word in metric_name for word in ['rate', 'return', 'drawdown', 'volatility']):
print(f"{metric_name.ljust(20)}: {value:>8.2%} - {rating}")
else:
print(f"{metric_name.ljust(20)}: {value:>8.3f} - {rating}")
# Общая оценка
ratings_count = {}
for rating in assessment.values():
clean_rating = rating.split(' ', 1)[1]
ratings_count[clean_rating] = ratings_count.get(clean_rating, 0) + 1
print(f"\nСВОДНАЯ ОЦЕНКА:")
print("-" * 30)
for rating, count in sorted(ratings_count.items()):
emoji = '🟢' if rating == 'ОТЛИЧНО' else '🟡' if rating == 'ХОРОШО' else '🟠' if rating == 'ПРИЕМЛЕМО' else '🔴'
print(f"{emoji} {rating}: {count} метрик")
# Итоговый рейтинг
total_metrics = len(assessment)
excellent_count = ratings_count.get('ОТЛИЧНО', 0)
good_count = ratings_count.get('ХОРОШО', 0)
if excellent_count >= total_metrics * 0.6:
overall_rating = "🟢 ВЫСОКИЙ РЕЙТИНГ"
elif (excellent_count + good_count) >= total_metrics * 0.6:
overall_rating = "🟡 СРЕДНИЙ РЕЙТИНГ"
else:
overall_rating = "🔴 НИЗКИЙ РЕЙТИНГ"
print(f"\n ИТОГОВЫЙ РЕЙТИНГ: {overall_rating}")
return assessment
# Пример использования:
strategy_assessment = benchmark_strategy_performance(final_metrics, 'equity_long_short')
ОЦЕНКА СТРАТЕГИИ ТИПА: EQUITY_LONG_SHORT
============================================================
sharpe_ratio : 0.420 - 🔴 НЕУДОВЛЕТВОРИТЕЛЬНО
max_drawdown : -21.67% - 🔴 НЕУДОВЛЕТВОРИТЕЛЬНО
information_ratio : -0.099 - 🔴 НЕУДОВЛЕТВОРИТЕЛЬНО
win_rate : 52.13% - 🟠 ПРИЕМЛЕМО
sortino_ratio : 0.038 - 🔴 НЕУДОВЛЕТВОРИТЕЛЬНО
calmar_ratio : 0.371 - 🔴 НЕУДОВЛЕТВОРИТЕЛЬНО
annualized_return : 8.04% - 🟠 ПРИЕМЛЕМО
volatility : 17.16% - 🟡 ХОРОШО
СВОДНАЯ ОЦЕНКА:
------------------------------
🔴 НЕУДОВЛЕТВОРИТЕЛЬНО: 5 метрик
🟠 ПРИЕМЛЕМО: 2 метрик
🟡 ХОРОШО: 1 метрик
ИТОГОВЫЙ РЕЙТИНГ: 🔴 НИЗКИЙ РЕЙТИНГ
Как видите — стратегия совершенно провалилась в сравнении с эталонными показателями. Отдельно отмечу цветовое кодирование: каждая метрика получает цветовую оценку от «отлично» до «неудовлетворительно», а итоговый рейтинг формируется на основе распределения оценок. Это позволяет управляющим активов и инвесторам быстро определить сильные и слабые стороны стратегии, а также понять, соответствует ли она институциональным стандартам качества для данного типа торговых подходов.
В целом, бенчмаркинг — довольно популярный способ оценки стратегий, поскольку он позволяет объективно оценить качество стратегии в контексте профессиональных стандартов. Хотя тут всегда следует иметь ввиду, что стратегия с «приемлемыми» оценками в принципе может быть коммерчески успешной, особенно если она обладает низкой корреляцией с другими активами в портфеле.
Выводы и практические рекомендации
Итак, полагаю эта статья открыла для вас множество интересных и полезных метрик оценки прибыльности биржевых стратегий. Как правило, начинающие трейдеры и инвесторы совершают одну и ту же ошибку: фокусируются на доходности или % профитных сделок, игнорируя структуру рисков. Это неизбежно приводит к болезненным просадкам, эмоциональному трейдингу и, как результат — преждевременному прекращению торговли.
Согласно моему опыту и best practices надо оценивать стратегии комплексно. Однако также важно понимать что не существует идеальных стратегий. Работающие стратегии редко демонстрируют идеальные метрики. Коэффициент Шарпа в районе 0.8-1.2 с просадками 10-20% часто оказывается более устойчивым, чем стратегии с показателями 2.0+ и минимальными просадками. Последние обычно работают только в определенных рыночных условиях и быстро ломаются при изменении режима.
Если дейтрейдинг — это не про вас, то особое внимание рекомендую уделять анализу tail risk (хвостовых рисков) и поведению стратегии в стрессовые периоды (резких падений). Также следует помнить, что метрика expected shortfall (ожидаемые потери в хвосте распределений) часто говорит больше о реальных рисках, чем классический VaR. А временная стабильность метрик важнее их абсолютных значений — лучше иметь умеренные, но постоянные результаты, чем блестящие показатели, которые резко ухудшаются.
Представленный инструментарий на Python для оценки биржевых стратегий стоит освоить каждому, кто серьезно относится к своим инвестициям. Даже если вы не профессиональный трейдер, автоматизация расчетов поможет избежать эмоциональных решений и объективно оценить результаты. Математика не врет — в отличие от эмоций.