В системном трейдинге и количественном анализе классический коэффициент Шарпа часто подвергается критике. Он одинаково штрафует торговую систему как за резкие убытки, так и за резкую прибыль, поскольку использует в знаменателе стандартное отклонение всех доходностей. Для более реалистичной оценки асимметричных профилей риска профессиональные кванты используют альтернативные метрики: Sortino Ratio (коэффициент Сортино) и Calmar Ratio (коэффициент Кальмара).
Коэффициент Сортино оценивает доходность портфеля относительно волатильности только отрицательных результатов (downside deviation). Коэффициент Кальмара сопоставляет среднегодовой темп роста капитала (CAGR) с максимальной исторической просадкой (Max Drawdown). Для более глубокого анализа рисков вы также можете изучить наш материал про Расчет коэффициента Шарпа и просадки в Python с pandas-ta.
Если вы строите портфель из нескольких активов, полезно оценить его чувствительность к рынку, используя Расчет Бета-коэффициента Крипто-портфеля к S&P 500 (SPY) с NumPy Cov. При бэктестинге стратегий крайне важно избегать переобучения, в чем поможет Walk-Forward Оптимизация в Backtrader: Предотвращение Переобучения | FinFluct.
Исходный код
Ниже представлен оптимизированный скрипт на Python, который принимает на вход исторический ряд стоимости чистых активов (Equity Curve) в виде pandas.Series, рассчитывает CAGR, максимальную просадку, а также коэффициенты Сортино и Кальмара.
import numpy as np
import pandas as pd
def calculate_portfolio_metrics(equity_series: pd.Series, risk_free_rate: float = 0.0, periods: int = 252) -> dict:
"""
Расчет финансовых метрик (CAGR, Max Drawdown, Calmar Ratio, Sortino Ratio) на основе Equity Curve.
:param equity_series: pd.Series с исторической стоимостью портфеля (индекс - даты)
:param risk_free_rate: Годовая безрисковая ставка (например, 0.05 для 5%)
:param periods: Количество торговых периодов в году (252 для акций, 365 для крипты)
:return: Словарь с рассчитанными метриками
"""
if len(equity_series) < 2:
raise ValueError("Серия Equity Curve должна содержать как минимум 2 точки данных.")
# 1. Расчет доходностей
returns = equity_series.pct_change().dropna()
# Определение временного горизонта в годах
total_days = (equity_series.index[-1] - equity_series.index[0]).days
years = total_days / 365.25
if years == 0:
years = len(equity_series) / periods
# 2. CAGR (Compound Annual Growth Rate)
initial_val = equity_series.iloc[0]
final_val = equity_series.iloc[-1]
if initial_val <= 0 or final_val <= 0:
cagr = 0.0
else:
cagr = (final_val / initial_val) ** (1 / years) - 1
# 3. Расчет максимальной просадки (Max Drawdown)
cum_max = equity_series.cummax()
drawdowns = (equity_series - cum_max) / cum_max
max_dd = drawdowns.min() # Отрицательное число
# 4. Расчет Calmar Ratio
abs_max_dd = abs(max_dd)
calmar_ratio = cagr / abs_max_dd if abs_max_dd > 0 else np.nan
# 5. Расчет Sortino Ratio
# Переводим годовую безрисковую ставку под шаг расчета
target_return = risk_free_rate / periods
# Вычисляем избыточную доходность
excess_returns = returns - target_return
# Downside Deviation (отклонение вниз)
# Рассматриваем только те периоды, где доходность ниже целевой
downside_diff = returns[returns < target_return] - target_return
if len(downside_diff) > 0:
# Среднеквадратичное отклонение отрицательных доходностей
downside_deviation = np.sqrt(np.mean(downside_diff ** 2)) * np.sqrt(periods)
else:
downside_deviation = 0.0
# Среднегодовая избыточная доходность (арифметическая)
ann_excess_return = returns.mean() * periods - risk_free_rate
sortino_ratio = ann_excess_return / downside_deviation if downside_deviation > 0 else np.nan
return {
"cagr": cagr,
"max_drawdown": max_dd,
"calmar_ratio": calmar_ratio,
"sortino_ratio": sortino_ratio,
"downside_deviation": downside_deviation
}
Разбор параметров
equity_series: Объектpd.Series, представляющий динамику стоимости вашего портфеля во времени. Крайне важно, чтобы индекс содержал валидные даты типаDatetimeIndexдля точного расчета временного интервала в годах.risk_free_rate: Годовая безрисковая процентная ставка (например, доходность гособлигаций). По умолчанию равна0.0. Используется в качестве бенчмарка (минимально допустимой доходности) при расчете Sortino Ratio.periods: Коэффициент годовой сериализации. Для дневных данных традиционного фондового рынка равен252, для круглосуточного криптовалютного рынка —365.
Как запустить
Для демонстрации работы скрипта сгенерируем синтетическую кривую капитала с восходящим трендом и случайным шумом, после чего передадим ее в нашу функцию:
if __name__ == "__main__":
# Создаем синтетические данные (1000 торговых дней)
np.random.seed(42)
dates = pd.date_range(start="2020-01-01", periods=1000, freq="D")
# Генерируем случайные ежедневные доходности со средним плюсом
random_returns = np.random.normal(loc=0.0005, scale=0.01, size=1000)
# Строим Equity Curve, начиная со 100 000 USD
equity_values = 100000 * np.cumprod(1 + random_returns)
equity_curve = pd.Series(equity_values, index=dates)
# Рассчитываем метрики (для крипты periods=365)
metrics = calculate_portfolio_metrics(equity_curve, risk_free_rate=0.02, periods=365)
# Вывод результатов
print(f"CAGR (Среднегодовой темп роста): {metrics['cagr']:.2%}")
print(f"Максимальная просадка (Max DD): {metrics['max_drawdown']:.2%}")
print(f"Коэффициент Кальмара (Calmar Ratio): {metrics['calmar_ratio']:.4f}")
print(f"Коэффициент Сортино (Sortino Ratio): {metrics['sortino_ratio']:.4f}")
print(f"Downside Deviation (Волатильность вниз): {metrics['downside_deviation']:.2%}")




