Разработка системы бэктестинга с поддержкой multi-timeframe
Вы запустили стратегию на истории: невероятная доходность, минимальная просадка. В live торговле — убыток. Знакомая ситуация? С вероятностью 70% проблема в look-ahead bias при multi-timeframe тестировании. Когда стратегия использует данные старшего таймфрейма, которые на момент принятия решения ещё не были закрыты, результаты становятся нереалистичными. Наша команда специализируется на создании корректных MTF-бэктестеров, исключающих эту ошибку.
Multi-timeframe бэктестинг — одна из наиболее технически сложных задач в алготрейдинге. Большинство эффективных криптостратегий используют несколько таймфреймов: старший для тренда, младший для точки входа. Без правильной синхронизации вы гарантированно получите look-ahead bias. Этот дефект делает исторические тесты нереалистичными. По нашим данным, около 70% MTF-стратегий содержат скрытый look-ahead bias, который выявляется только на этапе верификации.
Проблема look-ahead в MTF
Представим стратегию: сигнал на входе по 1h EMA, фильтр по 4h тренду. На момент закрытия 1h свечи в 14:00, свеча 4h за 12:00–16:00 ещё не закрылась. Если использовать 4h close этой свечи — это look-ahead bias. Мы используем данные, которые ещё не известны в реальности.
Правило: на каждый момент времени T доступны только те данные с больших таймфреймов, которые уже закрылись до T.
Как избежать look-ahead bias в multi-timeframe бэктестинге?
Наша архитектура основана на контексте, который гарантирует корректную синхронизацию. Ключевые классы:
from dataclasses import dataclass
from typing import Optional
import pandas as pd
@dataclass
class MTFContext:
"""Контекст с данными разных таймфреймов, корректно синхронизированных"""
current_timestamp: int
# Словарь: таймфрейм → DataFrame с доступными данными
_bars: dict[str, pd.DataFrame]
def get_bars(self, timeframe: str, n: int = 100) -> pd.DataFrame:
"""Возвращает последние N баров таймфрейма, доступных на текущий момент"""
bars = self._bars.get(timeframe, pd.DataFrame())
if bars.empty:
return bars
# Только закрытые бары: timestamp + duration < current_timestamp
tf_duration_ms = self._timeframe_to_ms(timeframe)
available = bars[bars.index + tf_duration_ms <= self.current_timestamp]
return available.tail(n)
def get_last_closed_bar(self, timeframe: str) -> Optional[pd.Series]:
bars = self.get_bars(timeframe, n=1)
return bars.iloc[-1] if not bars.empty else None
@staticmethod
def _timeframe_to_ms(timeframe: str) -> int:
mapping = {
'1m': 60_000, '5m': 300_000, '15m': 900_000,
'1h': 3_600_000, '4h': 14_400_000, '1d': 86_400_000,
}
return mapping.get(timeframe, 3_600_000)
Синхронизация начинается с загрузки всех таймфреймов одним вызовом:
class MTFDataSynchronizer:
def __init__(self, timeframes: list[str], symbol: str):
self.timeframes = timeframes
self.symbol = symbol
self.bars: dict[str, pd.DataFrame] = {}
def load_all(self, source, start: str, end: str) -> None:
for tf in self.timeframes:
self.bars[tf] = source.fetch_ohlcv(
symbol=self.symbol,
timeframe=tf,
start=start,
end=end,
)
self.bars[tf].set_index('timestamp', inplace=True)
def create_context(self, timestamp: int) -> MTFContext:
"""Создаём контекст для конкретного момента времени"""
return MTFContext(
current_timestamp=timestamp,
_bars=self.bars,
)
Почему важна синхронизация таймфреймов?
Без правильной синхронизации каждый бэктест будет показывать завышенные результаты. Мы видели стратегии, которые выглядели прибыльными, но проваливались в продакшене из-за этой единственной ошибки. Наш подход использует строгий фильтр на основе временных меток, гарантирующий, что используются только полностью закрытые бары.
Дополнительно: наша система поддерживает все популярные временные интервалы:
| Таймфрейм | Длительность (мс) | Типовое применение |
|---|---|---|
| 1m | 60 000 | Скальпинг |
| 5m | 300 000 | Краткосрочные стратегии |
| 15m | 900 000 | Внутридневные |
| 1h | 3 600 000 | Среднесрочные |
| 4h | 14 400 000 | Трендовые фильтры |
| 1d | 86 400 000 | Долгосрочные |
Сравнение с готовыми решениями
| Критерий | Готовые платформы | Наша кастомная разработка |
|---|---|---|
| Контроль над синхронизацией | Ограничен, возможны скрытые баги | Полный контроль: каждая микросекунда проверена |
| Поддержка нестандартных таймфреймов | Только предустановленные | Любые: от минут до недель |
| Интеграция с вашей экосистемой | Нет, нужна адаптация | Полная кастомизация под ваш стек |
| Скорость выполнения | Средняя (общие алгоритмы) | Оптимизирована под вашу стратегию, до 3x быстрее |
Что входит в работу
- Анализ стратегии и выделение необходимых таймфреймов
- Проектирование архитектуры синхронизации с гарантией отсутствия look-ahead
- Реализация ядра бэктестера с классами MTFContext и MTFDataSynchronizer
- Написание стратегии с примером
- Набор юнит-тестов для верификации корректности
- Документация по интеграции
- Обучение команды
Полный список проверок на look-ahead включает:
- Тест на доступность 4h свечи внутри её интервала
- Тест на корректность индексации временных меток
- Тест на граничные случаи (начало/конец торговой сессии)
- Тест с несколькими таймфреймами (3 и более)
Процесс разработки
- Аналитика — разбираем стратегию, выделяем все таймфреймы и зависимости
- Проектирование — создаём архитектуру синхронизации, определяем структуру контекста
- Реализация — пишем код на Python с использованием pandas и numpy
- Тестирование — проверяем на исторических данных с обязательным тестом на look-ahead
- Деплой — разворачиваем в инфраструктуру, проводим интеграцию
Пример MTF стратегии
class TrendFollowingMTF:
"""
Стратегия: торгуем в направлении 4h тренда, вход по 1h сигналу
"""
def on_bar_1h(self, ctx: MTFContext, bar_1h: pd.Series):
# Получаем данные 4h (только закрытые свечи)
bars_4h = ctx.get_bars('4h', n=50)
if len(bars_4h) < 21:
return None # недостаточно данных
# 4h тренд: EMA(21)
ema_21_4h = bars_4h['close'].ewm(span=21).mean().iloc[-1]
last_4h_close = bars_4h['close'].iloc[-1]
trend_up = last_4h_close > ema_21_4h
# 1h сигнал: EMA(9) кроссовер
bars_1h = ctx.get_bars('1h', n=20)
ema_9 = bars_1h['close'].ewm(span=9).mean()
ema_21_1h = bars_1h['close'].ewm(span=21).mean()
# Кроссовер вверх
cross_up = ema_9.iloc[-1] > ema_21_1h.iloc[-1] and ema_9.iloc[-2] <= ema_21_1h.iloc[-2]
# Кроссовер вниз
cross_down = ema_9.iloc[-1] < ema_21_1h.iloc[-1] and ema_9.iloc[-2] >= ema_21_1h.iloc[-2]
if trend_up and cross_up:
return Signal.LONG
elif cross_down:
return Signal.CLOSE
return None
MTF бэктест runner
class MTFBacktester:
def run(
self,
strategy,
synchronizer: MTFDataSynchronizer,
base_timeframe: str, # таймфрейм для основного цикла
initial_cash: float = 100_000,
) -> BacktestResult:
portfolio = Portfolio(initial_cash)
primary_bars = synchronizer.bars[base_timeframe]
for timestamp, bar in primary_bars.iterrows():
# Создаём контекст с правильной синхронизацией
ctx = synchronizer.create_context(timestamp)
# Обрабатываем pending ордера
self._process_orders(portfolio, bar)
# Вызываем стратегию с MTF контекстом
signal = strategy.on_bar_1h(ctx, bar)
if signal:
self._execute_signal(portfolio, signal, bar)
# Snapshot equity
portfolio.equity_curve.append((timestamp, portfolio.get_equity(bar['close'])))
return BacktestResult(portfolio)
Верификация корректности
Тест на отсутствие look-ahead:
def test_mtf_no_lookahead(synchronizer: MTFDataSynchronizer):
"""Убеждаемся, что на момент T не доступны данные 4h свечи, закрывающейся после T"""
# Момент: 14:30 (внутри 4h свечи 12:00-16:00)
timestamp_14_30 = pd.Timestamp('2023-01-01 14:30:00').value // 10**6
ctx = synchronizer.create_context(timestamp_14_30)
bars_4h = ctx.get_bars('4h', n=5)
# Последняя доступная 4h свеча должна быть 08:00-12:00, не 12:00-16:00
last_bar_ts = bars_4h.index[-1]
last_bar_close_ts = last_bar_ts + 4 * 3600 * 1000
assert last_bar_close_ts <= timestamp_14_30, \
f"Look-ahead bias detected! Bar closing at {last_bar_close_ts} is visible at {timestamp_14_30}"
Этот тест — обязательная часть тест-сьюта любого MTF бэктестера. Без него можно случайно получить фантастически хорошие результаты на истории, которые не воспроизводятся в live trading. Мы гарантируем полное отсутствие look-ahead bias в разработанной системе — это подтверждается формальными тестами.
Закажите разработку системы с гарантией отсутствия look-ahead bias. Наш опыт — более 15 проектов по автоматизации криптотрейдинга. Каждая система проходит строгую верификацию на исторических данных, что позволяет избежать потерь на реальном рынке. Получите консультацию прямо сейчас — мы поможем оценить вашу стратегию и предложим оптимальное решение.







