Walk-Forward анализ представляет собой методологию тестирования, которая моделирует реальные условия торговли, где мы постоянно получаем новые данные и должны адаптировать наши модели. Этот подход особенно важен в контексте финансовых рынков, где временная структура данных имеет критическое значение, а традиционные методы кросс-валидации могут давать обманчиво оптимистичные результаты.
Фундаментальные проблемы традиционного тестирования
Проблема временной зависимости в финансовых данных
Финансовые временные ряды обладают рядом специфических характеристик, которые делают применение стандартных методов машинного обучения проблематичным. Основная проблема заключается в том, что случайное разделение данных на обучающую и тестовую выборки нарушает временную структуру. В реальной торговле мы никогда не имеем доступа к будущим данным для принятия решений в прошлом, однако традиционные методы кросс-валидации могут неявно использовать эту информацию.
Представьте ситуацию: вы тестируете стратегию, используя K-fold кросс-валидацию на данных за 2020-2024 годы. Алгоритм может случайно попасть в ситуацию, когда данные за март 2024 года используются для обучения модели, которая затем тестируется на данных за январь 2022 года. Такая ситуация создает временную утечку (temporal data leakage), которая приводит к переоценке производительности стратегии.
Нестационарность и изменчивость рыночных режимов
Финансовые рынки демонстрируют высокую степень нестационарности. Это означает, что статистические свойства данных изменяются во времени. Периоды низкой волатильности сменяются периодами высокой волатильности, корреляционные структуры между активами меняются, а макроэкономические факторы влияют на поведение цен непредсказуемым образом. Стратегия, которая показывала отличные результаты в период растущего рынка 2024 года, может оказаться совершенно неэффективной в условиях высокой волатильности 2025 года.
Давайте рассмотрим следующий пример:
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error
import warnings
warnings.filterwarnings('ignore')
# Загрузка исторических данных для демонстрации нестационарности
tickers = ['SPY', 'QQQ', 'IWM']
data = yf.download(tickers, start='2015-06-01', end='2025-06-01')['Close']
# Вычисление скользящих характеристик волатильности
def calculate_rolling_volatility(prices, window=252):
returns = prices.pct_change().dropna()
rolling_vol = returns.rolling(window=window).std() * np.sqrt(252)
return rolling_vol
# Анализ изменения волатильности во времени
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
for ticker in tickers:
volatility = calculate_rolling_volatility(data[ticker])
axes[0].plot(volatility.index, volatility, label=f'{ticker} Volatility', linewidth=1.5)
axes[0].set_title('Изменение волатильности во времени', fontsize=14)
axes[0].set_ylabel('Годовая волатильность', fontsize=12)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Анализ корреляционной структуры
def calculate_rolling_correlation(data, window=252):
corr_spy_qqq = data['SPY'].rolling(window).corr(data['QQQ'])
corr_spy_iwm = data['SPY'].rolling(window).corr(data['IWM'])
return corr_spy_qqq, corr_spy_iwm
corr_spy_qqq, corr_spy_iwm = calculate_rolling_correlation(data)
axes[1].plot(corr_spy_qqq.index, corr_spy_qqq, label='SPY-QQQ Correlation',
color='darkblue', linewidth=2)
axes[1].plot(corr_spy_iwm.index, corr_spy_iwm, label='SPY-IWM Correlation',
color='darkred', linewidth=2)
axes[1].set_title('Изменение корреляций между активами', fontsize=14)
axes[1].set_ylabel('Корреляция', fontsize=12)
axes[1].set_xlabel('Дата', fontsize=12)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Статистический тест на стационарность
from statsmodels.tsa.stattools import adfuller
def test_stationarity(timeseries, title):
"""Тест Дики-Фуллера на стационарность"""
result = adfuller(timeseries.dropna())
print(f'\n{title}:')
print(f'ADF Statistic: {result[0]:.6f}')
print(f'p-value: {result[1]:.6f}')
print(f'Critical Values:')
for key, value in result[4].items():
print(f'\t{key}: {value:.3f}')
if result[1] <= 0.05:
print("Результат: Ряд стационарен (отвергаем H0)")
else:
print("Результат: Ряд не является стационарным (не отвергаем H0)")
# Тестирование стационарности цен и доходностей
for ticker in tickers:
test_stationarity(data[ticker], f'{ticker} - Цены')
returns = data[ticker].pct_change().dropna()
test_stationarity(returns, f'{ticker} - Доходности')
Рис. 1: Динамика волатильности и корреляции индексов SP500, Nasdaq, Russell2000 за последние 10 лет
SPY - Цены:
ADF Statistic: 0.231746
p-value: 0.973987
Critical Values:
1%: -3.433
5%: -2.863
10%: -2.567
Результат: Ряд не является стационарным (не отвергаем H0)
SPY - Доходности:
ADF Statistic: -16.197818
p-value: 0.000000
Critical Values:
1%: -3.433
5%: -2.863
10%: -2.567
Результат: Ряд стационарен (отвергаем H0)
QQQ - Цены:
ADF Statistic: 0.229176
p-value: 0.973851
Critical Values:
1%: -3.433
5%: -2.863
10%: -2.567
Результат: Ряд не является стационарным (не отвергаем H0)
QQQ - Доходности:
ADF Statistic: -16.641723
p-value: 0.000000
Critical Values:
1%: -3.433
5%: -2.863
10%: -2.567
Результат: Ряд стационарен (отвергаем H0)
IWM - Цены:
ADF Statistic: -1.495916
p-value: 0.535531
Critical Values:
1%: -3.433
5%: -2.863
10%: -2.567
Результат: Ряд не является стационарным (не отвергаем H0)
IWM - Доходности:
ADF Statistic: -16.398977
p-value: 0.000000
Critical Values:
1%: -3.433
5%: -2.863
10%: -2.567
Результат: Ряд стационарен (отвергаем H0)
Этот код демонстрирует ключевую проблему финансовых временных рядов — их нестационарную природу. Анализ показывает, как волатильность и корреляционная структура между активами изменяются во времени. Тест Дики-Фуллера подтверждает, что цены активов не являются стационарными, в то время как доходности часто проходят тест на стационарность, но все равно демонстрируют изменяющуюся во времени условную волатильность.
Этот анализ подчеркивает важность использования методов тестирования, которые учитывают временную структуру данных. Walk-Forward анализ позволяет моделировать процесс адаптации стратегии к изменяющимся рыночным условиям, что делает его более реалистичным подходом к оценке производительности торговых алгоритмов.
Концептуальные основы Walk-Forward анализа
Принципы временного моделирования
Walk-Forward анализ основывается на простом, но мощном принципе: тестирование стратегии должно максимально точно воспроизводить условия реальной торговли. В реальной торговле мы используем исторические данные для обучения модели, затем применяем ее к новым, неизвестным данным, получаем результаты, и только после этого можем использовать эти новые данные для дальнейшего обучения.
Основная идея заключается в последовательном движении через временной ряд с фиксированным или расширяющимся окном обучения. На каждом шаге мы обучаем модель на доступных исторических данных, делаем прогноз на следующий период (или несколько периодов вперед), фиксируем результат, а затем «идем вперед» во времени, добавляя новые данные к обучающему набору.
Архитектура Walk-Forward тестирования
Классическая схема Walk-Forward анализа включает несколько ключевых компонентов. Определение размера обучающего окна (training window) критически важно — слишком маленькое окно может не захватить важные паттерны, в то время как слишком большое может включать устаревшую информацию, которая уже не релевантна для текущих рыночных условий.
Размер тестового периода (out-of-sample period) также требует тщательного рассмотрения. Более длинные тестовые периоды обеспечивают более стабильную оценку производительности, но могут привести к устареванию модели. Более короткие периоды позволяют чаще переобучать модель, но могут давать более шумные оценки производительности.
Ниже представлен код на Python для выполнения Walk-Forward анализа для тестирования квантовых торговых стратегий на исторических данных SPY ETF за период 2022-2025. Анализ включает три стратегии (Statistical Arbitrage, Cross-Sectional Momentum, VPT + Volatility Regime), правильное разбиение данных на обучающие и тестовые периоды, учет комиссий и детальную оценку эффективности с визуализацией результатов.
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
from scipy.stats import zscore
import warnings
warnings.filterwarnings('ignore')
class ProfessionalWalkForward:
def __init__(self, train_window=150, test_window=50, initial_capital=10000, commission=0.001):
self.train_window = train_window
self.test_window = test_window
self.initial_capital = initial_capital
self.commission = commission
self.results = {}
def download_data(self):
print("Загружаем данные SPY...")
self.data = yf.download('SPY', start='2022-06-01', end='2025-06-01')
self.close_prices = self.data['Close'].squeeze()
self.high_prices = self.data['High'].squeeze()
self.low_prices = self.data['Low'].squeeze()
self.volume = self.data['Volume'].squeeze()
print(f"Загружено {len(self.close_prices)} торговых дней")
print(f"Диапазон дат: {self.close_prices.index[0].strftime('%Y-%m-%d')} - {self.close_prices.index[-1].strftime('%Y-%m-%d')}")
def create_walk_forward_splits(self):
splits = []
start_idx = self.train_window
while start_idx + self.test_window <= len(self.close_prices): train_start = start_idx - self.train_window train_end = start_idx test_start = start_idx test_end = start_idx + self.test_window splits.append({ 'train_start': train_start, 'train_end': train_end, 'test_start': test_start, 'test_end': test_end }) start_idx += self.test_window return splits def statistical_arbitrage_strategy(self, prices, window=60, entry_threshold=2.0, exit_threshold=0.5): signals = np.zeros(len(prices)) price_values = prices.values if hasattr(prices, 'values') else np.array(prices) for i in range(window, len(price_values)): window_data = price_values[i-window:i] current_price = price_values[i] window_mean = np.mean(window_data) window_std = np.std(window_data) if window_std > 0:
z_score = (current_price - window_mean) / window_std
if z_score > entry_threshold:
signals[i] = -1
elif z_score < -entry_threshold:
signals[i] = 1
elif abs(z_score) < exit_threshold: signals[i] = 0 return pd.Series(signals, index=prices.index) def cross_sectional_momentum_strategy(self, prices, short_window=21, long_window=126): signals = np.zeros(len(prices)) price_values = prices.values if hasattr(prices, 'values') else np.array(prices) for i in range(long_window, len(price_values)): if i >= short_window:
short_return = (price_values[i] - price_values[i-short_window]) / price_values[i-short_window]
else:
short_return = 0
long_return = (price_values[i] - price_values[i-long_window]) / price_values[i-long_window]
combined_momentum = short_return * 0.3 + long_return * 0.7
if combined_momentum > 0.02:
signals[i] = 1
elif combined_momentum < -0.02: signals[i] = -1 else: signals[i] = 0 return pd.Series(signals, index=prices.index) def vpt_volatility_strategy(self, prices, volume, vpt_window=20, vol_window=30): signals = np.zeros(len(prices)) price_values = prices.values if hasattr(prices, 'values') else np.array(prices) volume_values = volume.values if hasattr(volume, 'values') else np.array(volume) if len(price_values.shape) > 1:
price_values = price_values.flatten()
if len(volume_values.shape) > 1:
volume_values = volume_values.flatten()
if len(price_values) != len(volume_values):
return pd.Series(signals, index=prices.index)
vpt = np.zeros(len(prices))
for i in range(1, len(price_values)):
if price_values[i-1] != 0:
price_change = (price_values[i] - price_values[i-1]) / price_values[i-1]
vpt[i] = vpt[i-1] + price_change * volume_values[i]
returns = np.diff(price_values) / price_values[:-1]
for i in range(max(vpt_window, vol_window), len(price_values)):
vpt_current = vpt[i]
vpt_avg = np.mean(vpt[i-vpt_window:i])
if i-vol_window >= 0:
start_idx = max(0, i-vol_window-1)
end_idx = min(i-1, len(returns))
if end_idx > start_idx:
recent_returns = returns[start_idx:end_idx]
if len(recent_returns) > 0:
current_vol = np.std(recent_returns) * np.sqrt(252)
else:
current_vol = 0
else:
current_vol = 0
else:
current_vol = 0
if i >= vol_window * 3:
hist_start = max(0, i-vol_window*3-1)
hist_end = max(0, i-vol_window-1)
if hist_end > hist_start and hist_end <= len(returns): hist_returns = returns[hist_start:hist_end] if len(hist_returns) >= vol_window:
vol_windows = []
for j in range(0, len(hist_returns)-vol_window+1, vol_window):
window_vol = np.std(hist_returns[j:j+vol_window]) * np.sqrt(252)
vol_windows.append(window_vol)
if vol_windows:
vol_percentile = np.percentile(vol_windows, 50)
else:
vol_percentile = current_vol
else:
vol_percentile = current_vol
else:
vol_percentile = current_vol
else:
vol_percentile = current_vol
if vol_percentile > 0 and current_vol < vol_percentile * 0.8: if vpt_current > vpt_avg:
signals[i] = 1
else:
signals[i] = -1
elif vol_percentile > 0 and current_vol > vol_percentile * 1.2:
if vpt_current > vpt_avg:
signals[i] = -1
else:
signals[i] = 1
else:
signals[i] = 0
return pd.Series(signals, index=prices.index)
def backtest_strategy(self, prices, signals, strategy_name):
returns = prices.pct_change().fillna(0)
portfolio_value = self.initial_capital
portfolio_returns = []
trades = 0
winning_trades = 0
current_position = 0
trade_start_value = self.initial_capital
for i in range(len(signals)):
signal = signals.iloc[i]
if hasattr(returns.iloc[i], 'item'):
period_return = returns.iloc[i].item()
else:
period_return = float(returns.iloc[i])
if signal != current_position:
if current_position != 0:
# Закрываем позицию - считаем была ли сделка прибыльной
trades += 1
if portfolio_value > trade_start_value:
winning_trades += 1
commission_cost = self.commission * portfolio_value
portfolio_value -= commission_cost
if signal != 0:
# Открываем новую позицию
commission_cost = self.commission * portfolio_value
portfolio_value -= commission_cost
trade_start_value = portfolio_value # Запоминаем стартовое значение сделки
current_position = signal
if current_position != 0:
strategy_return = current_position * period_return
portfolio_value *= (1 + strategy_return)
else:
strategy_return = 0
portfolio_returns.append(strategy_return)
portfolio_returns = np.array(portfolio_returns)
total_return = (portfolio_value - self.initial_capital) / self.initial_capital
if len(portfolio_returns) > 0 and np.std(portfolio_returns) > 0:
sharpe_ratio = np.mean(portfolio_returns) / np.std(portfolio_returns) * np.sqrt(252)
else:
sharpe_ratio = 0
if len(portfolio_returns) > 0:
cumulative_returns = np.cumprod(1 + portfolio_returns)
running_max = np.maximum.accumulate(cumulative_returns)
drawdowns = (cumulative_returns - running_max) / running_max
max_drawdown = np.min(drawdowns)
else:
max_drawdown = 0
win_rate = winning_trades / trades if trades > 0 else 0
return {
'total_return': total_return,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown,
'win_rate': win_rate,
'total_trades': trades,
'portfolio_value': portfolio_value
}
def run_walkforward_analysis(self):
print("WALK-FORWARD АНАЛИЗ")
print("Период анализа: 01.06.2022 - 01.06.2025")
print("Актив: SPY ETF")
self.download_data()
splits = self.create_walk_forward_splits()
print(f"Конфигурация Walk-Forward:")
print(f"- Количество периодов: {len(splits)}")
print(f"- Обучающее окно: {self.train_window} дней")
print(f"- Тестовое окно: {self.test_window} дней")
print(f"- Начальный капитал: ${self.initial_capital:,}")
print(f"- Комиссия: {self.commission:.1%}")
strategies = {
'Statistical Arbitrage': self.statistical_arbitrage_strategy,
'Cross-Sectional Momentum': self.cross_sectional_momentum_strategy,
'VPT + Volatility': self.vpt_volatility_strategy
}
for strategy_name in strategies:
self.results[strategy_name] = []
for period_idx, split in enumerate(splits):
train_data = self.close_prices.iloc[split['train_start']:split['train_end']]
test_data = self.close_prices.iloc[split['test_start']:split['test_end']]
test_volume = self.volume.iloc[split['test_start']:split['test_end']]
print(f"Период {period_idx + 1}/{len(splits)}:")
print(f" Обучение: {train_data.index[0].strftime('%Y-%m-%d')} - {train_data.index[-1].strftime('%Y-%m-%d')}")
print(f" Тестирование: {test_data.index[0].strftime('%Y-%m-%d')} - {test_data.index[-1].strftime('%Y-%m-%d')}")
for strategy_name, strategy_func in strategies.items():
try:
if strategy_name == 'VPT + Volatility':
extended_prices = self.close_prices.iloc[:split['test_end']]
extended_volume = self.volume.iloc[:split['test_end']]
all_signals = strategy_func(extended_prices, extended_volume)
test_signals = all_signals.iloc[split['test_start']:split['test_end']]
else:
extended_prices = self.close_prices.iloc[:split['test_end']]
all_signals = strategy_func(extended_prices)
test_signals = all_signals.iloc[split['test_start']:split['test_end']]
result = self.backtest_strategy(test_data, test_signals, strategy_name)
result.update({
'period': period_idx + 1,
'start_date': test_data.index[0],
'end_date': test_data.index[-1]
})
self.results[strategy_name].append(result)
print(f" {strategy_name}: доходность {result['total_return']:.2%}, Шарп {result['sharpe_ratio']:.2f}, просадка {result['max_drawdown']:.2%}")
except Exception as e:
print(f" ОШИБКА {strategy_name}: {e}")
self.results[strategy_name].append({
'period': period_idx + 1,
'start_date': test_data.index[0],
'end_date': test_data.index[-1],
'total_return': 0,
'sharpe_ratio': 0,
'max_drawdown': 0,
'win_rate': 0,
'total_trades': 0,
'portfolio_value': self.initial_capital
})
return self.results
def analyze_results(self):
print("СВОДНЫЕ РЕЗУЛЬТАТЫ WALK-FORWARD АНАЛИЗА")
print("="*70)
summary_data = []
for strategy_name, results in self.results.items():
if not results:
continue
df = pd.DataFrame(results)
avg_return = df['total_return'].mean()
avg_sharpe = df['sharpe_ratio'].mean()
avg_drawdown = df['max_drawdown'].mean()
avg_winrate = df['win_rate'].mean()
std_return = df['total_return'].std()
positive_periods = (df['total_return'] > 0).mean()
summary_data.append({
'Стратегия': strategy_name,
'Средняя доходность': f"{avg_return:.2%}",
'Std доходности': f"{std_return:.2%}",
'Средний Sharpe': f"{avg_sharpe:.2f}",
'Средняя просадка': f"{avg_drawdown:.2%}",
'Средний Win Rate': f"{avg_winrate:.1%}",
'Прибыльных периодов': f"{positive_periods:.1%}"
})
print(f"{strategy_name}:")
print(f" Средняя доходность за период: {avg_return:.2%}")
print(f" Стандартное отклонение: {std_return:.2%}")
print(f" Средний коэффициент Шарпа: {avg_sharpe:.2f}")
print(f" Средняя максимальная просадка: {avg_drawdown:.2%}")
print(f" Средний Win Rate: {avg_winrate:.1%}")
print(f" Доля прибыльных периодов: {positive_periods:.1%}")
summary_df = pd.DataFrame(summary_data)
print("ИТОГОВАЯ ТАБЛИЦА:")
print(summary_df.to_string(index=False))
self.create_visualizations()
return summary_df
def create_visualizations(self):
print("Создаем графики...")
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
fig.suptitle('Walk-Forward Анализ 3 стратегий торговли SPY, 2022-2025)', fontsize=16, fontweight='bold')
colors = ['purple', 'orange', 'darkgreen']
strategies = list(self.results.keys())
for i, (strategy_name, color) in enumerate(zip(strategies, colors)):
if self.results[strategy_name]:
df = pd.DataFrame(self.results[strategy_name])
axes[0, 0].plot(df['period'], df['total_return'] * 100, marker='o', label=strategy_name, color=color, linewidth=2)
axes[0, 0].set_title('Доходность по периодам (%)')
axes[0, 0].set_xlabel('Период')
axes[0, 0].set_ylabel('Доходность (%)')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].axhline(y=0, color='black', linestyle='--', alpha=0.5)
for i, (strategy_name, color) in enumerate(zip(strategies, colors)):
if self.results[strategy_name]:
df = pd.DataFrame(self.results[strategy_name])
axes[0, 1].plot(df['period'], df['sharpe_ratio'], marker='s', label=strategy_name, color=color, linewidth=2)
axes[0, 1].set_title('Коэффициент Шарпа')
axes[0, 1].set_xlabel('Период')
axes[0, 1].set_ylabel('Sharpe Ratio')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].axhline(y=0, color='black', linestyle='--', alpha=0.5)
for i, (strategy_name, color) in enumerate(zip(strategies, colors)):
if self.results[strategy_name]:
df = pd.DataFrame(self.results[strategy_name])
axes[0, 2].plot(df['period'], df['max_drawdown'] * 100, marker='^', label=strategy_name, color=color, linewidth=2)
axes[0, 2].set_title('Максимальная просадка (%)')
axes[0, 2].set_xlabel('Период')
axes[0, 2].set_ylabel('Просадка (%)')
axes[0, 2].legend()
axes[0, 2].grid(True, alpha=0.3)
for i, (strategy_name, color) in enumerate(zip(strategies, colors)):
if self.results[strategy_name]:
df = pd.DataFrame(self.results[strategy_name])
axes[1, 0].plot(df['period'], df['win_rate'] * 100, marker='d', label=strategy_name, color=color, linewidth=2)
axes[1, 0].set_title('Win Rate (%)')
axes[1, 0].set_xlabel('Период')
axes[1, 0].set_ylabel('Win Rate (%)')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
strategy_names = []
avg_returns = []
strategy_colors = []
for i, (strategy_name, color) in enumerate(zip(strategies, colors)):
if self.results[strategy_name]:
df = pd.DataFrame(self.results[strategy_name])
strategy_names.append(strategy_name.replace(' ', '\n'))
avg_returns.append(df['total_return'].mean() * 100)
strategy_colors.append(color)
if strategy_names:
bars = axes[1, 1].bar(strategy_names, avg_returns, color=strategy_colors, alpha=0.7)
axes[1, 1].set_title('Средняя доходность стратегий')
axes[1, 1].set_ylabel('Доходность (%)')
axes[1, 1].grid(True, alpha=0.3)
for bar, value in zip(bars, avg_returns):
axes[1, 1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05, f'{value:.2f}%', ha='center', va='bottom', fontweight='bold')
if strategy_names:
stability = []
for i, (strategy_name, color) in enumerate(zip(strategies, colors)):
if self.results[strategy_name]:
df = pd.DataFrame(self.results[strategy_name])
stability.append(df['total_return'].std() * 100)
bars2 = axes[1, 2].bar(strategy_names, stability, color=strategy_colors, alpha=0.7)
axes[1, 2].set_title('Стабильность (σ доходности)')
axes[1, 2].set_ylabel('Стандартное отклонение (%)')
axes[1, 2].grid(True, alpha=0.3)
for bar, value in zip(bars2, stability):
axes[1, 2].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02, f'{value:.2f}%', ha='center', va='bottom', fontweight='bold')
plt.tight_layout()
plt.show()
def main():
print("Запуск Walk-Forward анализа")
print("="*60)
try:
analyzer = ProfessionalWalkForward(train_window=150, test_window=50, initial_capital=10000, commission=0.001)
results = analyzer.run_walkforward_analysis()
summary = analyzer.analyze_results()
print("Анализ успешно завершен!")
if results:
best_returns = {}
stability = {}
for strategy_name, strategy_results in results.items():
if strategy_results:
df = pd.DataFrame(strategy_results)
best_returns[strategy_name] = df['total_return'].mean()
stability[strategy_name] = df['total_return'].std()
if best_returns:
best_strategy = max(best_returns, key=best_returns.get)
most_stable = min(stability, key=stability.get)
print(f"Лучшая стратегия по доходности: {best_strategy}")
print(f"Самая стабильная стратегия: {most_stable}")
return results, summary
except Exception as e:
print(f"Ошибка при выполнении анализа: {e}")
import traceback
traceback.print_exc()
return None, None
if __name__ == "__main__":
results, summary = main()
WALK-FORWARD АНАЛИЗ
Период анализа: 01.06.2022 - 01.06.2025
Актив: SPY ETF
Загружаем данные SPY...
Загружено 752 торговых дней
Диапазон дат: 2022-06-01 - 2025-05-30
Конфигурация Walk-Forward:
- Количество периодов: 12
- Обучающее окно: 150 дней
- Тестовое окно: 50 дней
- Начальный капитал: $10,000
- Комиссия: 0.1%
Период 1/12:
Обучение: 2022-06-01 - 2023-01-04
Тестирование: 2023-01-05 - 2023-03-17
Statistical Arbitrage: доходность -3.14%, Шарп -2.55, просадка -2.76%
Cross-Sectional Momentum: доходность 9.60%, Шарп 4.90, просадка -1.67%
VPT + Volatility: доходность 9.46%, Шарп 3.04, просадка -2.77%
Период 2/12:
Обучение: 2022-08-12 - 2023-03-17
Тестирование: 2023-03-20 - 2023-05-30
Statistical Arbitrage: доходность 0.00%, Шарп 0.00, просадка 0.00%
Cross-Sectional Momentum: доходность 7.05%, Шарп 3.00, просадка -2.60%
VPT + Volatility: доходность 11.43%, Шарп 7.32, просадка -1.23%
Период 3/12:
Обучение: 2022-10-24 - 2023-05-30
Тестирование: 2023-05-31 - 2023-08-10
Statistical Arbitrage: доходность -8.43%, Шарп -6.27, просадка -7.32%
Cross-Sectional Momentum: доходность 7.00%, Шарп 3.61, просадка -2.63%
VPT + Volatility: доходность 6.96%, Шарп 4.89, просадка -2.16%
Период 4/12:
Обучение: 2023-01-05 - 2023-08-10
Тестирование: 2023-08-11 - 2023-10-20
Statistical Arbitrage: доходность -4.70%, Шарп -3.35, просадка -4.41%
Cross-Sectional Momentum: доходность -3.76%, Шарп -1.45, просадка -5.28%
VPT + Volatility: доходность 1.14%, Шарп 1.91, просадка -1.23%
Период 5/12:
Обучение: 2023-03-20 - 2023-10-20
Тестирование: 2023-10-23 - 2024-01-03
Statistical Arbitrage: доходность -5.10%, Шарп -3.79, просадка -4.53%
Cross-Sectional Momentum: доходность 11.44%, Шарп 5.32, просадка -1.66%
VPT + Volatility: доходность -6.64%, Шарп -2.69, просадка -7.93%
Период 6/12:
Обучение: 2023-05-31 - 2024-01-03
Тестирование: 2024-01-04 - 2024-03-15
Statistical Arbitrage: доходность -4.15%, Шарп -3.54, просадка -3.57%
Cross-Sectional Momentum: доходность 9.34%, Шарп 4.12, просадка -1.71%
VPT + Volatility: доходность 1.71%, Шарп 2.47, просадка -2.10%
Период 7/12:
Обучение: 2023-08-11 - 2024-03-15
Тестирование: 2024-03-18 - 2024-05-28
Statistical Arbitrage: доходность -1.73%, Шарп -2.71, просадка -1.53%
Cross-Sectional Momentum: доходность 3.20%, Шарп 1.55, просадка -5.35%
VPT + Volatility: доходность -1.03%, Шарп -1.66, просадка -0.84%
Период 8/12:
Обучение: 2023-10-23 - 2024-05-28
Тестирование: 2024-05-29 - 2024-08-08
Statistical Arbitrage: доходность -3.21%, Шарп -4.21, просадка -2.82%
Cross-Sectional Momentum: доходность 4.40%, Шарп 1.94, просадка -5.66%
VPT + Volatility: доходность -0.11%, Шарп 0.31, просадка -6.31%
Период 9/12:
Обучение: 2024-01-04 - 2024-08-08
Тестирование: 2024-08-09 - 2024-10-18
Statistical Arbitrage: доходность 0.00%, Шарп 0.00, просадка 0.00%
Cross-Sectional Momentum: доходность 9.91%, Шарп 4.10, просадка -4.14%
VPT + Volatility: доходность 3.78%, Шарп 2.43, просадка -1.92%
Период 10/12:
Обучение: 2024-03-18 - 2024-10-18
Тестирование: 2024-10-21 - 2024-12-31
Statistical Arbitrage: доходность -4.48%, Шарп -3.49, просадка -4.10%
Cross-Sectional Momentum: доходность 0.66%, Шарп 0.36, просадка -3.57%
VPT + Volatility: доходность 6.52%, Шарп 4.57, просадка -1.41%
Период 11/12:
Обучение: 2024-05-29 - 2024-12-31
Тестирование: 2025-01-02 - 2025-03-17
Statistical Arbitrage: доходность -5.00%, Шарп -2.41, просадка -6.55%
Cross-Sectional Momentum: доходность 0.54%, Шарп 0.60, просадка -4.76%
VPT + Volatility: доходность -5.00%, Шарп -3.32, просадка -5.32%
Период 12/12:
Обучение: 2024-08-09 - 2025-03-17
Тестирование: 2025-03-18 - 2025-05-28
Statistical Arbitrage: доходность -18.56%, Шарп -4.89, просадка -17.91%
Cross-Sectional Momentum: доходность 4.35%, Шарп 0.99, просадка -13.24%
VPT + Volatility: доходность -35.04%, Шарп -6.10, просадка -38.20%
СВОДНЫЕ РЕЗУЛЬТАТЫ WALK-FORWARD АНАЛИЗА
======================================================================
Statistical Arbitrage:
Средняя доходность за период: -4.88%
Стандартное отклонение: 4.90%
Средний коэффициент Шарпа: -3.10
Средняя максимальная просадка: -4.63%
Средний Win Rate: 0.0%
Доля прибыльных периодов: 0.0%
Cross-Sectional Momentum:
Средняя доходность за период: 5.31%
Стандартное отклонение: 4.59%
Средний коэффициент Шарпа: 2.42
Средняя максимальная просадка: -4.35%
Средний Win Rate: 30.7%
Доля прибыльных периодов: 91.7%
VPT + Volatility:
Средняя доходность за период: -0.57%
Стандартное отклонение: 12.15%
Средний коэффициент Шарпа: 1.10
Средняя максимальная просадка: -5.95%
Средний Win Rate: 61.9%
Доля прибыльных периодов: 58.3%
ИТОГОВАЯ ТАБЛИЦА:
Стратегия Средняя доходность Std доходности Средний Sharpe Средняя просадка Средний Win Rate Прибыльных периодов
Statistical Arbitrage -4.88% 4.90% -3.10 -4.63% 0.0% 0.0%
Cross-Sectional Momentum 5.31% 4.59% 2.42 -4.35% 30.7% 91.7%
VPT + Volatility -0.57% 12.15% 1.10 -5.95% 61.9% 58.3%
Анализ успешно завершен!
Лучшая стратегия по доходности: Cross-Sectional Momentum
Самая стабильная стратегия: Cross-Sectional Momentum
Создаем графики...
Рис. 2: Графики результатов Walk-Forward анализа стратегий торговли индекса SP500
Код получится довольно объемным, давайте рассмотрим его основные шаги:
- Загрузка данных: Скачивание исторических данных SPY (цены и объемы) за период с июня 2022 по июнь 2025 через библиотеку yfinance;
- Walk-Forward разбиение: Создание последовательных периодов с обучающим окном 150 дней и тестовым окном 50 дней, всего получается 12 периодов тестирования;
- Функция со стратегией Statistical Arbitrage: Вычисление z-score цены относительно скользящего среднего, вход в позицию при отклонении >2σ, выход при возврате к среднему;
- Функция со стратегией Cross-Sectional Momentum: Комбинирование краткосрочного (21 день) и долгосрочного (126 дней) momentum с весами 30%/70%, сигналы при превышении 2% порога;
- Функция со стратегией VPT + Volatility Regime: Расчет Volume-Price Trend и анализ режимов волатильности, адаптация сигналов в зависимости от текущей волатильности рынка;
- Бэктестинг: Для каждого тестового периода применение стратегий с учетом комиссии 0.1%, расчет доходности портфеля и торговых метрик;
- Расчет метрик: Вычисление для каждой стратегии общей доходности, коэффициента Шарпа, максимальной просадки, win rate и количества сделок;
- Агрегация результатов: Сбор статистики по всем периодам, вычисление средних значений и стабильности стратегий;
- Анализ эффективности: Сравнение стратегий по доходности и стабильности, определение лучшей и наиболее стабильной стратегии;
- Визуализация: Создание 6 графиков для анализа динамики доходности, Sharpe ratio, просадок, win rate и сравнительных характеристик стратегий.
Ключевой особенностью кода является корректная обработка данных pandas Series с автоматическим извлечением скалярных значений, что исключает типичные ошибки форматирования при бэктестинге. Система учета транзакционных издержек интегрирована непосредственно в торговый цикл с правильным расчетом win rate только при закрытии позиций, а не по дневной доходности. Анализатор автоматически генерирует комплексную визуализацию с шестью графиками для сравнительного анализа эффективности стратегий и определения оптимального подхода.
Мета-оптимизация в Walk-Forward анализе
Мета-оптимизация представляет собой процесс оптимизации самих параметров оптимизации. В контексте Walk-Forward анализа это означает динамическую адаптацию не только параметров стратегии, но и параметров самого процесса тестирования. Этот подход особенно важен для профессиональных квантовых фондов, где требуется максимальная адаптивность к изменяющимся рыночным условиям.
Основная идея мета-оптимизации заключается в том, что оптимальные параметры процесса тестирования (размер обучающего окна, частота переобучения, критерии остановки) сами могут изменяться во времени в зависимости от характеристик рынка. Например, в периоды высокой волатильности может быть оптимальным использовать более короткие обучающие окна и чаще переобучать модели, в то время как в стабильные периоды предпочтительнее более длинные окна для повышения статистической значимости.
class MetaOptimizedWalkForward:
def __init__(self, base_train_window=150, base_test_window=50, initial_capital=10000, commission=0.001):
self.base_train_window = base_train_window
self.base_test_window = base_test_window
self.initial_capital = initial_capital
self.commission = commission
self.meta_parameters = {
'train_window_range': [100, 150, 200, 250],
'test_window_range': [20, 30, 50, 70],
'rebalance_frequencies': [10, 20, 30, 50]
}
self.meta_optimization_history = []
self.current_meta_params = {
'train_window': base_train_window,
'test_window': base_test_window,
'rebalance_frequency': 30
}
def download_data(self):
print("Загружаем данные для мета-оптимизации...")
self.data = yf.download('SPY', start='2020-01-01', end='2025-06-01')
self.close_prices = self.data['Close'].squeeze()
self.volume = self.data['Volume'].squeeze()
print(f"Загружено {len(self.close_prices)} торговых дней")
def calculate_market_regime_features(self, prices, lookback=60):
"""Вычисляем характеристики рыночного режима для мета-оптимизации"""
returns = prices.pct_change().dropna()
# Волатильность
volatility = returns.rolling(lookback).std() * np.sqrt(252)
# Автокорреляция доходностей (мера трендовости)
autocorr = returns.rolling(lookback).apply(lambda x: x.autocorr(lag=1) if len(x) > 1 else 0)
# Максимальная просадка за период
cumulative = (1 + returns).cumprod()
rolling_max = cumulative.rolling(lookback).max()
drawdown = (cumulative - rolling_max) / rolling_max
max_drawdown = drawdown.rolling(lookback).min()
# Коэффициент асимметрии (skewness)
skewness = returns.rolling(lookback).skew()
# Коэффициент эксцесса (kurtosis)
kurtosis = returns.rolling(lookback).kurt()
return {
'volatility': volatility.iloc[-1] if len(volatility) > 0 else 0,
'autocorr': autocorr.iloc[-1] if len(autocorr) > 0 else 0,
'max_drawdown': abs(max_drawdown.iloc[-1]) if len(max_drawdown) > 0 else 0,
'skewness': skewness.iloc[-1] if len(skewness) > 0 else 0,
'kurtosis': kurtosis.iloc[-1] if len(kurtosis) > 0 else 0
}
def evaluate_meta_parameter_combination(self, prices, train_window, test_window, rebalance_freq,
evaluation_periods=5):
"""Оценка эффективности конкретной комбинации мета-параметров"""
try:
# Создаем мини walk-forward для оценки
results = []
start_idx = train_window + test_window
periods_tested = 0
while start_idx + test_window <= len(prices) and periods_tested < evaluation_periods: train_start = start_idx - train_window train_end = start_idx test_start = start_idx test_end = start_idx + test_window train_data = prices.iloc[train_start:train_end] test_data = prices.iloc[test_start:test_end] # Применяем простую momentum стратегию для оценки signals = self.simple_momentum_strategy(train_data, test_data) result = self.backtest_strategy(test_data, signals) if result['total_return'] is not None: results.append({ 'return': result['total_return'], 'sharpe': result['sharpe_ratio'], 'drawdown': result['max_drawdown'] }) start_idx += rebalance_freq periods_tested += 1 if not results: return 0 # Комбинированная метрика качества avg_return = np.mean([r['return'] for r in results]) avg_sharpe = np.mean([r['sharpe'] for r in results]) avg_drawdown = np.mean([r['drawdown'] for r in results]) # Штрафуем за высокую просадку combined_score = avg_sharpe * (1 - abs(avg_drawdown)) + avg_return * 0.5 return combined_score except Exception as e: print(f"Ошибка в оценке мета-параметров: {e}") return 0 def optimize_meta_parameters(self, prices, current_position): """Оптимизация мета-параметров на основе текущих рыночных условий""" print(f"Мета-оптимизация на позиции {current_position}") # Вычисляем характеристики текущего рыночного режима regime_features = self.calculate_market_regime_features( prices.iloc[:current_position], lookback=60 ) best_score = -np.inf best_params = self.current_meta_params.copy() # Перебираем комбинации мета-параметров for train_window in self.meta_parameters['train_window_range']: for test_window in self.meta_parameters['test_window_range']: for rebalance_freq in self.meta_parameters['rebalance_frequencies']: # Проверяем валидность параметров if train_window + test_window > current_position:
continue
# Адаптация под рыночный режим
score = self.evaluate_meta_parameter_combination(
prices.iloc[:current_position],
train_window, test_window, rebalance_freq
)
# Бонус за адаптацию к режиму
if regime_features['volatility'] > 0.25: # Высокая волатильность
if train_window < self.base_train_window: # Короткое окно score *= 1.1 else: # Низкая волатильность if train_window > self.base_train_window: # Длинное окно
score *= 1.1
if score > best_score:
best_score = score
best_params = {
'train_window': train_window,
'test_window': test_window,
'rebalance_frequency': rebalance_freq
}
# Сохраняем историю мета-оптимизации
self.meta_optimization_history.append({
'position': current_position,
'date': prices.index[current_position] if current_position < len(prices) else None,
'regime_features': regime_features,
'old_params': self.current_meta_params.copy(),
'new_params': best_params.copy(),
'score_improvement': best_score
})
self.current_meta_params = best_params
print(f"Новые мета-параметры: {best_params}")
return best_params
def simple_momentum_strategy(self, train_data, test_data, lookback=20):
"""Простая momentum стратегия для демонстрации"""
signals = np.zeros(len(test_data))
for i in range(lookback, len(test_data)):
if i < len(test_data): recent_return = (test_data.iloc[i] - test_data.iloc[i-lookback]) / test_data.iloc[i-lookback] signals[i] = 1 if recent_return > 0.02 else (-1 if recent_return < -0.02 else 0) return pd.Series(signals, index=test_data.index) def backtest_strategy(self, prices, signals): """Бэктест стратегии (упрощенная версия)""" returns = prices.pct_change().fillna(0) portfolio_value = self.initial_capital portfolio_returns = [] for i in range(len(signals)): signal = signals.iloc[i] if hasattr(signals.iloc[i], 'item') else float(signals.iloc[i]) period_return = returns.iloc[i] if hasattr(returns.iloc[i], 'item') else float(returns.iloc[i]) strategy_return = signal * period_return portfolio_value *= (1 + strategy_return) portfolio_returns.append(strategy_return) portfolio_returns = np.array(portfolio_returns) total_return = (portfolio_value - self.initial_capital) / self.initial_capital sharpe_ratio = 0 if len(portfolio_returns) > 0 and np.std(portfolio_returns) > 0:
sharpe_ratio = np.mean(portfolio_returns) / np.std(portfolio_returns) * np.sqrt(252)
max_drawdown = 0
if len(portfolio_returns) > 0:
cumulative_returns = np.cumprod(1 + portfolio_returns)
running_max = np.maximum.accumulate(cumulative_returns)
drawdowns = (cumulative_returns - running_max) / running_max
max_drawdown = np.min(drawdowns)
return {
'total_return': total_return,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown,
'portfolio_value': portfolio_value
}
def run_meta_optimized_walkforward(self, meta_optimization_frequency=200):
"""Запуск Walk-Forward анализа с мета-оптимизацией"""
self.download_data()
results = []
current_position = self.current_meta_params['train_window'] + self.current_meta_params['test_window']
last_meta_optimization = 0
while current_position + self.current_meta_params['test_window'] <= len(self.close_prices): # Проверяем, нужна ли мета-оптимизация if (current_position - last_meta_optimization >= meta_optimization_frequency or
last_meta_optimization == 0):
self.optimize_meta_parameters(self.close_prices, current_position)
last_meta_optimization = current_position
# Выполняем обычный Walk-Forward с текущими мета-параметрами
train_window = self.current_meta_params['train_window']
test_window = self.current_meta_params['test_window']
train_start = current_position - train_window
train_end = current_position
test_start = current_position
test_end = current_position + test_window
train_data = self.close_prices.iloc[train_start:train_end]
test_data = self.close_prices.iloc[test_start:test_end]
# Применяем стратегию
signals = self.simple_momentum_strategy(train_data, test_data)
result = self.backtest_strategy(test_data, signals)
result.update({
'period_start': test_data.index[0],
'period_end': test_data.index[-1],
'meta_params_used': self.current_meta_params.copy()
})
results.append(result)
print(f"Период {len(results)}: доходность {result['total_return']:.2%}")
# Переходим к следующему периоду
current_position += self.current_meta_params['rebalance_frequency']
return results
def analyze_meta_optimization_effectiveness(self, results):
"""Анализ эффективности мета-оптимизации"""
print("\nАНАЛИЗ МЕТА-ОПТИМИЗАЦИИ")
if not self.meta_optimization_history:
print("Нет данных о мета-оптимизации")
return
# Статистика изменений параметров
train_window_changes = []
test_window_changes = []
rebalance_freq_changes = []
for i, opt in enumerate(self.meta_optimization_history):
if i > 0:
prev_opt = self.meta_optimization_history[i-1]
train_window_changes.append(
opt['new_params']['train_window'] - prev_opt['new_params']['train_window']
)
test_window_changes.append(
opt['new_params']['test_window'] - prev_opt['new_params']['test_window']
)
rebalance_freq_changes.append(
opt['new_params']['rebalance_frequency'] - prev_opt['new_params']['rebalance_frequency']
)
print(f"Количество мета-оптимизаций: {len(self.meta_optimization_history)}")
print(f"Среднее изменение train_window: {np.mean(np.abs(train_window_changes)):.1f} дней")
print(f"Среднее изменение test_window: {np.mean(np.abs(test_window_changes)):.1f} дней")
print(f"Среднее изменение rebalance_frequency: {np.mean(np.abs(rebalance_freq_changes)):.1f} дней")
# Связь между режимом и параметрами
high_vol_periods = [opt for opt in self.meta_optimization_history
if opt['regime_features']['volatility'] > 0.25]
low_vol_periods = [opt for opt in self.meta_optimization_history
if opt['regime_features']['volatility'] <= 0.25] if high_vol_periods and low_vol_periods: avg_train_high_vol = np.mean([opt['new_params']['train_window'] for opt in high_vol_periods]) avg_train_low_vol = np.mean([opt['new_params']['train_window'] for opt in low_vol_periods]) print(f"\nАдаптация к волатильности:") print(f"Среднее окно обучения при высокой волатильности: {avg_train_high_vol:.0f} дней") print(f"Среднее окно обучения при низкой волатильности: {avg_train_low_vol:.0f} дней") # Демонстрация мета-оптимизации def demonstrate_meta_optimization(): """Демонстрация мета-оптимизированного Walk-Forward анализа""" meta_analyzer = MetaOptimizedWalkForward( base_train_window=150, base_test_window=50, initial_capital=10000, commission=0.001 ) print("Запуск мета-оптимизированного Walk-Forward анализа...") meta_results = meta_analyzer.run_meta_optimized_walkforward(meta_optimization_frequency=150) if meta_results: df_meta = pd.DataFrame(meta_results) print(f"\nРЕЗУЛЬТАТЫ МЕТА-ОПТИМИЗАЦИИ") print(f"Количество периодов: {len(df_meta)}") print(f"Средняя доходность: {df_meta['total_return'].mean():.2%}") print(f"Средний Sharpe: {df_meta['sharpe_ratio'].mean():.2f}") print(f"Стандартное отклонение доходности: {df_meta['total_return'].std():.2%}") print(f"Процент прибыльных периодов: {(df_meta['total_return'] > 0).mean():.1%}")
meta_analyzer.analyze_meta_optimization_effectiveness(meta_results)
return meta_results
if __name__ == "__main__":
meta_results = demonstrate_meta_optimization()
Запуск мета-оптимизированного Walk-Forward анализа...
Загружаем данные для мета-оптимизации...
Загружено 1360 торговых дней
Мета-оптимизация на позиции 200
Новые мета-параметры: {'train_window': 100, 'test_window': 30, 'rebalance_frequency': 50}
Период 1: доходность 2.96%
Период 2: доходность 3.68%
Период 3: доходность 1.28%
Мета-оптимизация на позиции 350
Новые мета-параметры: {'train_window': 200, 'test_window': 70, 'rebalance_frequency': 10}
Период 4: доходность 6.00%
Период 5: доходность 4.06%
Период 6: доходность 6.54%
Период 7: доходность 4.69%
Период 8: доходность 5.85%
Период 9: доходность 6.45%
Период 10: доходность 7.81%
Период 11: доходность 6.58%
Период 12: доходность 7.24%
Период 13: доходность 6.89%
Период 14: доходность 7.72%
Период 15: доходность 4.43%
Период 16: доходность 7.11%
Период 17: доходность 12.97%
Период 18: доходность 8.55%
Мета-оптимизация на позиции 500
Новые мета-параметры: {'train_window': 200, 'test_window': 30, 'rebalance_frequency': 20}
Период 19: доходность -1.84%
Период 20: доходность 6.59%
Период 21: доходность 0.32%
Период 22: доходность 1.63%
Период 23: доходность -5.25%
Период 24: доходность -1.33%
Период 25: доходность 8.70%
Период 26: доходность -2.18%
Мета-оптимизация на позиции 660
Новые мета-параметры: {'train_window': 200, 'test_window': 30, 'rebalance_frequency': 20}
Период 27: доходность 7.61%
Период 28: доходность 0.47%
Период 29: доходность 4.69%
Период 30: доходность 1.16%
Период 31: доходность 0.53%
Период 32: доходность -0.63%
Период 33: доходность 0.62%
Период 34: доходность 1.34%
Мета-оптимизация на позиции 820
Новые мета-параметры: {'train_window': 200, 'test_window': 30, 'rebalance_frequency': 20}
Период 35: доходность 0.00%
Период 36: доходность 4.93%
Период 37: доходность 1.71%
Период 38: доходность 0.30%
Период 39: доходность 0.00%
Период 40: доходность -2.04%
Период 41: доходность 0.44%
Период 42: доходность 0.33%
Мета-оптимизация на позиции 980
Новые мета-параметры: {'train_window': 200, 'test_window': 30, 'rebalance_frequency': 20}
Период 43: доходность -0.07%
Период 44: доходность 1.88%
Период 45: доходность 2.10%
Период 46: доходность 1.36%
Период 47: доходность -0.03%
Период 48: доходность -1.35%
Период 49: доходность 0.65%
Период 50: доходность -0.97%
Мета-оптимизация на позиции 1140
Новые мета-параметры: {'train_window': 200, 'test_window': 30, 'rebalance_frequency': 20}
Период 51: доходность -1.59%
Период 52: доходность 1.99%
Период 53: доходность 1.77%
Период 54: доходность 3.55%
Период 55: доходность 0.04%
Период 56: доходность 3.36%
Период 57: доходность 2.14%
Период 58: доходность 2.60%
Мета-оптимизация на позиции 1300
Новые мета-параметры: {'train_window': 200, 'test_window': 30, 'rebalance_frequency': 20}
Период 59: доходность 17.61%
Период 60: доходность 5.73%
РЕЗУЛЬТАТЫ МЕТА-ОПТИМИЗАЦИИ
Количество периодов: 60
Средняя доходность: 2.99%
Средний Sharpe: 1.96
Стандартное отклонение доходности: 3.97%
Процент прибыльных периодов: 78.3%
АНАЛИЗ МЕТА-ОПТИМИЗАЦИИ
Количество мета-оптимизаций: 8
Среднее изменение train_window: 14.3 дней
Среднее изменение test_window: 11.4 дней
Среднее изменение rebalance_frequency: 7.1 дней
Представленная реализация мета-оптимизации демонстрирует ключевые принципы адаптивного управления параметрами Walk-Forward анализа. Система динамически адаптирует размер обучающего окна, тестового периода и частоту переобучения на основе текущих характеристик рыночного режима.
Особенность подхода заключается в использовании комбинированной метрики качества, которая учитывает не только доходность и коэффициент Шарпа, но и максимальную просадку. Это позволяет системе находить оптимальный баланс между агрессивностью и стабильностью стратегии в различных рыночных условиях.
Практические аспекты реализации в production среде
Вычислительная сложность и оптимизация
Walk-Forward анализ, особенно в его продвинутых формах, может потребовать значительных вычислительных ресурсов. В профессиональных квантовых фондах эта проблема решается несколькими способами:
- Распараллеливание вычислений, где каждый период тестирования обрабатывается независимо на отдельном вычислительном ядре или даже сервере.
- Кэширование промежуточных результатов. Многие вычисления, такие как технические индикаторы или статистические характеристики, могут быть рассчитаны один раз и переиспользованы в различных периодах тестирования. Профессиональные системы часто используют специализированные базы данных временных рядов, такие как InfluxDB или TimescaleDB, для эффективного хранения и извлечения исторических данных.
- Использование приближенных методов для ускорения оптимизации параметров. Вместо полного перебора всех возможных комбинаций параметров часто применяются эвристические алгоритмы, такие как генетические алгоритмы или байесовская оптимизация, которые могут найти близкие к оптимальным решения за значительно меньшее время.
Обработка корпоративных действий и дивидендов
Одной из практических сложностей, которая часто недооценивается в академической литературе, является корректная обработка корпоративных действий. Сплиты акций, выплата дивидендов, спин-оффы и другие корпоративные события могут существенно исказить результаты тестирования, если не учитываются должным образом.
В профессиональных системах используется сложная логика корректировки цен, которая учитывает не только факт события, но и его влияние на торговые сигналы. Например, при дроблении акций 2:1 не только удваивается количество акций в портфеле, но и соответствующим образом корректируются все исторические цены и объемы, используемые для расчета индикаторов.
Особого внимания требует обработка дивидендов в стратегиях, использующих деривативы или короткие позиции. В таких случаях дивиденды могут представлять дополнительные расходы, которые должны быть точно учтены в расчетах доходности.
Интеграция с системами управления рисками
Walk-Forward анализ не существует в вакууме — он должен быть интегрирован с общей системой управления рисками фонда. Это означает, что результаты тестирования должны учитывать не только индивидуальную производительность стратегии, но и ее влияние на общий портфель.
Современные системы управления рисками используют многофакторные модели для оценки вклада каждой стратегии в общий риск портфеля. Walk-Forward анализ должен предоставлять не только показатели доходности, но и корреляционные матрицы, бета-коэффициенты по отношению к различным факторам риска, и оценки хвостовых рисков (Value at Risk, Expected Shortfall).
Кроме того, результаты тестирования должны включать анализ концентрации рисков. Стратегия может показывать отличные результаты в Walk-Forward тестировании, но если она генерирует прибыль только в определенных секторах или при определенных рыночных условиях, это может создать нежелательную концентрацию рисков в портфеле.
Особое внимание уделяется вопросам data mining bias — систематической ошибки, возникающей при многократном тестировании различных стратегий на одних и тех же данных. Для решения этой проблемы многие фонды используют строгое разделение данных на периоды разработки и валидации, где окончательная валидация проводится только один раз на данных, которые никогда не использовались в процессе разработки.
Альтернативные подходы к валидации стратегий
Monte Carlo симуляции в контексте Walk-Forward
Хотя Walk-Forward анализ предоставляет реалистичную оценку производительности стратегии, он все же ограничен конкретной исторической последовательностью событий. Monte Carlo симуляции позволяют дополнить этот анализ, генерируя тысячи альтернативных сценариев развития рынка, основанных на статистических характеристиках исторических данных.
В профессиональной практике часто используется гибридный подход, где Walk-Forward анализ определяет базовую производительность стратегии, а Monte Carlo симуляции оценивают ее устойчивость к различным рыночным сценариям. Это особенно важно для стратегий, которые демонстрируют высокую доходность, но могут быть уязвимы к редким, но разрушительным рыночным событиям.
Современные Monte Carlo симуляции учитывают не только изменения цен, но и эволюцию волатильности, корреляционных структур между активами, и даже вероятность структурных изменений в рыночных режимах. Такой многомерный подход позволяет получить более полную картину потенциальных рисков и возможностей стратегии.
Синтетические данные и стресс-тестирование
Еще одним важным дополнением к Walk-Forward анализу является использование синтетических данных для стресс-тестирования. Исторические данные могут не содержать всех возможных экстремальных сценариев, которые могут произойти в будущем. Синтетические данные позволяют смоделировать такие сценарии и оценить поведение стратегии в условиях, которые не наблюдались в прошлом.
Профессиональные системы часто используют комбинацию исторических кризисов (1987, 1998, 2008, 2020 годы) для создания синтетических кризисных сценариев. Эти сценарии могут комбинировать характеристики различных исторических кризисов или усиливать их для моделирования еще более экстремальных условий.
Особенно важным является тестирование стратегий на синтетических данных, моделирующих одновременное нарушение нескольких рыночных предположений. Например, сценарий, где одновременно происходит резкое увеличение волатильности, нарушение исторических корреляций между активами, и значительное снижение ликвидности рынка.
Кросс-валидация во временных рядах
Традиционная кросс-валидация неприменима к временным рядам из-за нарушения временного порядка данных. Однако существуют специализированные методы кросс-валидации, адаптированные для финансовых временных рядов, которые могут дополнить Walk-Forward анализ:
- Time Series Cross-Validation представляет собой обобщение Walk-Forward подхода, где используется несколько различных схем разбиения данных одновременно. Это позволяет оценить устойчивость результатов к конкретному выбору размеров обучающих и тестовых окон.
- Purged Cross-Validation — еще более продвинутый метод, который учитывает возможное «загрязнение» тестовых данных информацией из обучающей выборки через сложные зависимости в финансовых временных рядах. Этот метод особенно важен для высокочастотных стратегий, где даже небольшие временные утечки могут существенно исказить результаты.
Заключение
Walk-Forward анализ представляет собой мощный инструмент для реалистичного тестирования торговых стратегий, который позволяет моделировать условия реальной торговли с учетом временной структуры финансовых данных. В отличие от традиционных методов машинного обучения, этот подход строго соблюдает принцип каузальности, где решения принимаются только на основе информации, доступной на момент принятия решения.
Ключевые особенности правильного Walk-Forward анализа включают динамическую адаптацию к изменяющимся рыночным режимам, многоуровневую структуру тестирования с разделением оптимизации и валидации, а также интеграцию с системами управления рисками.
Walk-Forward анализ сегодня крайне популярен для валидации количественных стратегий в биржевой торговле. Правильное применение этой методологии позволяет избежать множества ловушек, характерных для традиционных подходов к тестированию, и создавать торговые системы, способные генерировать стабильную прибыль в реальных рыночных условиях.