Трейдери, які активно використовують бектестування, часто стикаються з проблемою: після прогону десятків стратегій на історичних даних немає єдиного способу об'єктивно їх порівняти. Кожна стратегія виводить власний набір метрик — одна показує Sharpe ratio, інша тільки максимальну просідання. Вибір найкращої перетворюється на ворожіння. Ми розробили систему, яка стандартизує всі результати: приводить метрики до річних значень, розраховує додаткові показники (Sortino, Calmar, profit factor) та виводить зведену таблицю з ранжуванням за кожним критерієм. Завдяки єдиному класу StrategyResult всі дані зберігаються в одному місці, а компаратор автоматично будує рейтинг та аналізує кореляцію стратегій.
Стандартизація необхідна, тому що метрики, розраховані на різних таймфреймах, непорівнянні. Sharpe на годинних свічках і на денних дає різні значення. Наш інструмент annualizes всі метрики, як того вимагають академічні стандарти. Це дозволяє порівнювати стратегії з різною частотою угод та тривалістю тестових періодів.
Чому стандартизація метрик критична для бектестування?
Без єдиного формату складно зрозуміти, яка стратегія дійсно краща. Sharpe ratio однієї може бути розрахований на денних даних, іншої — на годинних. Наша система приводить всі метрики до річних значень, використовуючи єдиний клас StrategyResult.
from dataclasses import dataclass
import pandas as pd
import numpy as np
@dataclass
class StrategyResult:
name: str
params: dict
equity_curve: pd.Series
trades: pd.DataFrame
# Вирахувані метрики
sharpe_ratio: float
sortino_ratio: float
calmar_ratio: float
annual_return_pct: float
max_drawdown_pct: float
win_rate: float
profit_factor: float
total_trades: int
avg_trade_duration_hours: float
total_commission_pct: float
Клас зберігає всі вирахувані метрики в одному місці. Це дозволяє легко передавати результати в компаратор.
Як ми оцінюємо ризик та дохідність?
Кожна метрика відповідає на своє питання: Sharpe показує надлишкову дохідність на одиницю ризику, Sortino враховує тільки від'ємну волатильність, Calmar — відношення дохідності до максимальної просідання. Ми розраховуємо їх автоматично з equity curve.
| Метрика | Що показує | Формула (annualized) |
|---|---|---|
| Sharpe ratio | Дохідність за ризик | (mean_return - risk_free) / std_return * sqrt(252) |
| Sortino ratio | Дохідність за downside-ризик | (mean_return - risk_free) / downside_std * sqrt(252) |
| Calmar ratio | Дохідність до просідання | annual_return / max_drawdown |
| Profit factor | Виграш/програш | gross_profit / gross_loss |
| Win rate | Частка прибуткових угод | wins / total_trades |
Таблиця допомагає швидко зрозуміти, яка стратегія краща за цікавим критерієм.
Порівняння стратегій з бенчмарком
Ми додаємо Buy & Hold benchmark, щоб оцінити, чи перевершує стратегія пасивне інвестування. Компаратор StrategyComparator ранжує стратегії за кожною метрикою та виводить загальний рейтинг.
class StrategyComparator:
def __init__(self, backtester, benchmark_data: pd.Series = None):
self.backtester = backtester
self.benchmark = benchmark_data # Buy & Hold BTC для порівняння
def compare(self, strategies: list[dict], data: pd.DataFrame) -> ComparisonReport:
results = []
for strategy_config in strategies:
result = self.backtester.run(
strategy_class=strategy_config['class'],
params=strategy_config['params'],
data=data,
name=strategy_config['name'],
)
results.append(result)
if self.benchmark is not None:
bh_return = (self.benchmark.iloc[-1] / self.benchmark.iloc[0] - 1)
results.append(self._create_buyhold_result(self.benchmark))
return self.build_report(results)
def build_report(self, results: list[StrategyResult]) -> ComparisonReport:
comparison_df = pd.DataFrame([{
'Strategy': r.name,
'Annual Return %': round(r.annual_return_pct, 2),
'Sharpe Ratio': round(r.sharpe_ratio, 3),
'Sortino Ratio': round(r.sortino_ratio, 3),
'Max Drawdown %': round(r.max_drawdown_pct, 2),
'Calmar Ratio': round(r.calmar_ratio, 3),
'Win Rate %': round(r.win_rate * 100, 1),
'Profit Factor': round(r.profit_factor, 2),
'Total Trades': r.total_trades,
'Avg Trade Hours': round(r.avg_trade_duration_hours, 1),
'Commission Drag %': round(r.total_commission_pct, 2),
} for r in results])
rankings = self._compute_rankings(comparison_df)
correlations = self._compute_correlations(results)
return ComparisonReport(
summary=comparison_df,
rankings=rankings,
correlations=correlations,
strategies=results,
)
def _compute_rankings(self, df: pd.DataFrame) -> pd.DataFrame:
rankings = pd.DataFrame({'Strategy': df['Strategy']})
for metric, ascending in [
('Annual Return %', False),
('Sharpe Ratio', False),
('Max Drawdown %', True),
('Profit Factor', False),
('Win Rate %', False),
]:
if metric in df.columns:
rankings[f'Rank: {metric}'] = df[metric].rank(ascending=ascending).astype(int)
rank_cols = [c for c in rankings.columns if c.startswith('Rank:')]
rankings['Overall Rank'] = rankings[rank_cols].mean(axis=1).rank().astype(int)
return rankings.sort_values('Overall Rank')
def _compute_correlations(self, results: list[StrategyResult]) -> pd.DataFrame:
returns_dict = {
r.name: r.equity_curve.pct_change().dropna()
for r in results
}
returns_df = pd.DataFrame(returns_dict).dropna()
return returns_df.corr()
Ранжування дозволяє швидко виділити лідера за сукупністю метрик, а матриця кореляції — оцінити ступінь диверсифікації.
Як візуалізація допомагає інтерпретувати результати?
Графік equity curves, scatter plot ризик-дохідність та просідання дають наочне уявлення. Приклад коду з Plotly:
def plot_comparison(report: ComparisonReport):
import plotly.graph_objects as go
from plotly.subplots import make_subplots
fig = make_subplots(
rows=2, cols=2,
subplot_titles=[
'Equity Curves',
'Risk-Return Scatter',
'Monthly Returns Distribution',
'Drawdown Comparison',
]
)
colors = ['#00C853', '#2196F3', '#FF9800', '#E91E63', '#9C27B0']
for i, strat in enumerate(report.strategies):
color = colors[i % len(colors)]
fig.add_trace(go.Scatter(
x=strat.equity_curve.index,
y=strat.equity_curve / strat.equity_curve.iloc[0] * 100,
name=strat.name,
line=dict(color=color),
), row=1, col=1)
fig.add_trace(go.Scatter(
x=[abs(strat.max_drawdown_pct)],
y=[strat.annual_return_pct],
mode='markers+text',
marker=dict(size=12, color=color),
text=[strat.name],
textposition='top center',
showlegend=False,
), row=1, col=2)
rolling_max = strat.equity_curve.cummax()
drawdown = (strat.equity_curve - rolling_max) / rolling_max * 100
fig.add_trace(go.Scatter(
x=drawdown.index,
y=drawdown,
fill='tozeroy',
name=strat.name,
line=dict(color=color),
showlegend=False,
opacity=0.6,
), row=2, col=2)
fig.update_layout(
title='Strategy Comparison Report',
height=900,
template='plotly_dark',
)
return fig
Візуалізація особливо корисна при презентації результатів команді або інвесторам.
Як виконується порівняння: покроково
- Завантажте історичні дані та список конфігурацій стратегій.
- Запустіть бектестування — кожна стратегія проганяється на єдиному наборі даних.
- Система збирає equity curves та списки угод, потім обчислює всі метрики.
- Компаратор формує зведену таблицю, ранжує стратегії та будує матрицю кореляції.
- Звіт експортується в HTML з графіками Plotly.
Весь процес займає від кількох хвилин до години залежно від кількості стратегій та обсягу даних. На практиці, при 20 стратегіях на річному датасеті, розрахунок займає менше 5 хвилин.
Приклад зведення для трьох стратегій
| Стратегія | Annual Return % | Sharpe Ratio | Max Drawdown % | Profit Factor |
|---|---|---|---|---|
| Momentum | 34.2 | 1.85 | -18.3 | 2.10 |
| MeanRev | 18.7 | 1.12 | -25.1 | 1.45 |
| Breakout | 41.5 | 2.10 | -22.7 | 2.55 |
За даними таблиці, Breakout показує найкращий Sharpe та дохідність, але Momentum має менше просідання. Система ранжування з урахуванням всіх метрик віддасть перевагу Breakout, але також вкаже на високу кореляцію між ним та Momentum (якщо вона є).
Як портфельна оптимізація покращує Sharpe?
Якщо стратегії слабко корельовані, їх можна об'єднати в портфель. Ми реалізували оптимізацію за Sharpe ratio:
def analyze_portfolio_combination(
strategy_returns: dict[str, pd.Series],
target_sharpe: float = 2.0,
) -> dict:
from scipy.optimize import minimize
returns_df = pd.DataFrame(strategy_returns).dropna()
mean_returns = returns_df.mean()
cov_matrix = returns_df.cov()
def neg_sharpe(weights):
portfolio_return = np.dot(weights, mean_returns) * 252
portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix * 252, weights)))
return -portfolio_return / portfolio_vol if portfolio_vol > 0 else 999
n = len(strategy_returns)
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = [(0, 1)] * n
x0 = [1/n] * n
result = minimize(neg_sharpe, x0, method='SLSQP', bounds=bounds, constraints=constraints)
optimal_weights = dict(zip(strategy_returns.keys(), result.x))
combined_returns = sum(ret * w for ret, w in zip(returns_df.values.T, result.x))
combined_sharpe = -result.fun
return {
'optimal_weights': optimal_weights,
'combined_sharpe': combined_sharpe,
'improvement_vs_best_single': combined_sharpe - max(
ret.mean() / ret.std() * np.sqrt(252) for ret in strategy_returns.values()
),
}
На практиці комбінація з двох-трьох некорельованих стратегій покращує Sharpe на 30–50% відносно найкращої одиночної. Наш досвід розробки таких систем налічує понад 50 проєктів.
Що входить у розробку системи порівняння?
Ми надаємо:
- Архітектуру та проєктування модуля порівняння
- Реалізацію на Python з використанням pandas, numpy, scipy
- Інтеграцію з існуючим backtester
- Генерацію звіту у вигляді DataFrame або HTML
- Візуалізацію через Plotly
- Документацію та приклади використання
- Підтримку після впровадження
Термін розробки системи під ключ становить від 2 до 4 тижнів, вартість розраховується індивідуально залежно від складності інтеграції. Нова система економить до 80% часу на аналізі результатів ручним способом. Ми гарантуємо, що всі розрахунки відповідають академічним стандартам.
Якщо ви хочете отримати об'єктивну систему порівняння для своїх стратегій — замовте розробку під ваші вимоги. Зв'яжіться з нами для консультації: опишіть задачу, і ми запропонуємо рішення.
Типові помилки при порівнянні стратегій
- Look-ahead bias: використання майбутніх даних при розрахунку метрик. Ми строго розділяємо in-sample та out-of-sample.
- Survivor bias: ігнорування померлих стратегій. Включаємо всі протестовані.
- Неврахування комісій: commission drag може з'їсти до 2% річних. Наш backtester їх враховує.
- Порівняння на різних періодах: всі метрики annualized, тому періоди можуть бути різними.







