פיתוח מקצועי של בדיקות היסטוריות (Backtesting) לאסטרטגיות מסחר
לאחרונה, סטארטאפ פנה אלינו עם אסטרטגיית ארביטראז' DeFi. בדיקת ה-backtest העצמאית שלהם ב-Python הראתה תשואה חודשית של 15% עם יחס שארפ של 2.1 — מספרים מרשימים. הרצנו סימולציה שהתחשבה בהחלקה (slippage) ובעמלות (0.3% לעסקה) — וקיבלנו 2%- עם שארפ של 0.1. הסיבה? הם השתמשו בנתונים היסטוריים מ-Binance ללא התאמה לנזילות הבריכה ושכחו את הטיית ההישרדות (survivorship bias) — נכסים שנמחקו מהמסחר שהיו מוחקים את התשואות לא נכללו. זה קורה ב-80% מהבדיקות הביתיות.
רוב הסוחרים מזלזלים בהשפעת ההחלקה והעמלות: 100 עסקאות בחודש בעמלה של 0.3% יכולות לאכול עד 30% מהתשואה השנתית. המנוע שלנו מתחשב בגורמים אלה, ומבטיח דיוק סימולציה שניתן לשחזור בסביבת מסחר חיה.
אנו מתכננים ומיישמים מערכות backtesting לאסטרטגיות DeFi, רובוטים ל-ETF ומסחר אלגוריתמי בקריפטו. נכון להיום, יש לנו 30+ פרויקטים שהושלמו — מממוצעים נעים פשוטים ועד מודלים מורכבים של ML על LSTM. המנועים שלנו מעבדים מיליוני נרות ללא דופי. בממוצע, הלקוחות שלנו חוסכים $5,000 בניפוי באגים בהשוואה לפיתוח פנימי. מנועי ה-backtesting שלנו מתחילים ב-$3,000 ויכולים לחסוך עד $5,000 בעלויות ניפוי באגים.
למה רוב הבדיקות ההיסטוריות אינן אמינות
הטיית מבט קדימה (Lookahead bias) היא הטעות הנפוצה ביותר. האסטרטגיה משתמשת בנתונים עתידיים כדי לייצר אותות בהווה.
# НЕПРАВИЛЬНО: используем current high для входа на current open
signal = df['high'].rolling(20).max() > df['close'] * 1.05 # текущие max и close
# ПРАВИЛЬНО: сигнал формируется на закрытой свече, вход на следующей
signal = df['high'].shift(1).rolling(20).max() > df['close'].shift(1) * 1.05
entry_price = df['open'] # вход по открытию следующей свечиהטיית הישרדות — מלכודת קלאסית: LUNA, FTX token, UST — נכסים שנמחקו מהמסחר אינם כלולים בנתונים ההיסטוריים של ספקים רבים. אנו משתמשים במערכי נתונים מותאמים שמתחשבים במטבעות מתים. מחקרים מראים שהתעלמות מהטיית ההישרדות יכולה לנפח תשואות ב-3-5% בשנה בהתאם לשוק. הטיית הישרדות — חובה לקרוא.
התאמת יתר (Overfitting): האסטרטגיה מותאמת לתקופה היסטורית ספציפית. היא עובדת מצוין על נתוני אימון, ונכשלת על נתונים מחוץ למדגם. לכן ניתוח walk-forward הוא חובה.
איך לארגן בדיקות היסטוריות בצורה נכונה?
נתחיל בארכיטקטורת המנוע. הקוד חייב להתחשב בעמלות, החלקות ועיכובים. אנו כותבים ב-Python באמצעות pandas ו-dataclasses. הנה מימוש מינימלי של BacktestEngine:
לחצו להרחבת דוגמת קוד
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Optional
import pandas as pd
@dataclass
class BacktestConfig:
initial_capital: Decimal = Decimal('10000')
commission_rate: Decimal = Decimal('0.001') # 0.1%
slippage_bps: int = 5 # 5 basis points
position_size_percent: float = 95.0 # % капитала на позицию
allow_short: bool = True
@dataclass
class Trade:
entry_time: pd.Timestamp
exit_time: Optional[pd.Timestamp]
side: str
symbol: str
entry_price: Decimal
exit_price: Optional[Decimal]
quantity: Decimal
commission: Decimal
pnl: Optional[Decimal] = None
class BacktestEngine:
def __init__(self, strategy, config: BacktestConfig):
self.strategy = strategy
self.config = config
self.capital = config.initial_capital
self.position: Optional[Trade] = None
self.completed_trades: list[Trade] = []
self.equity_curve: list[tuple] = []
def apply_slippage(self, price: Decimal, side: str) -> Decimal:
"""Моделируем ухудшение цены исполнения"""
slippage = price * Decimal(self.config.slippage_bps) / Decimal(10000)
if side == 'buy':
return price + slippage # покупаем дороже
else:
return price - slippage # продаём дешевле
def run(self, df: pd.DataFrame) -> 'BacktestResult':
warmup = 50 # свечей для прогрева индикаторов
for i in range(warmup, len(df)):
candle = df.iloc[i]
history = df.iloc[:i]
# Цена исполнения = open следующей свечи (реалистично)
exec_price = Decimal(str(candle['open']))
# Проверяем выход для открытой позиции
if self.position:
exit_signal = self.strategy.should_exit(self.position, history)
if exit_signal:
self.close_position(exec_price, candle.name, exit_signal)
# Проверяем сигнал входа
if not self.position:
signal = self.strategy.generate_signal(history)
if signal in ('BUY', 'SELL') and (signal == 'BUY' or self.config.allow_short):
self.open_position(signal, exec_price, candle.name)
# Записываем equity
current_equity = self.calculate_current_equity(candle['close'])
self.equity_curve.append((candle.name, float(current_equity)))
# Закрываем открытую позицию по последней цене
if self.position:
self.close_position(Decimal(str(df.iloc[-1]['close'])), df.index[-1], 'end_of_data')
return self.build_result()
def open_position(self, signal: str, price: Decimal, timestamp):
exec_price = self.apply_slippage(price, 'buy' if signal == 'BUY' else 'sell')
quantity = self.capital * Decimal(str(self.config.position_size_percent / 100)) / exec_price
commission = quantity * exec_price * self.config.commission_rate
self.capital -= (quantity * exec_price + commission)
self.position = Trade(
entry_time=timestamp,
exit_time=None,
side=signal,
symbol='BTC',
entry_price=exec_price,
exit_price=None,
quantity=quantity,
commission=commission
)
def close_position(self, price: Decimal, timestamp, reason: str):
side = 'sell' if self.position.side == 'BUY' else 'buy'
exec_price = self.apply_slippage(price, side)
commission = self.position.quantity * exec_price * self.config.commission_rate
if self.position.side == 'BUY':
gross_pnl = (exec_price - self.position.entry_price) * self.position.quantity
else:
gross_pnl = (self.position.entry_price - exec_price) * self.position.quantity
net_pnl = gross_pnl - commission - self.position.commission
self.capital += self.position.quantity * exec_price - commission
self.position.exit_time = timestamp
self.position.exit_price = exec_price
self.position.pnl = net_pnl
self.completed_trades.append(self.position)
self.position = None
חשוב: המנוע הזה כבר כולל החלקה (apply_slippage) ומודל עמלות. בלעדיהם, התוצאות לא יתאימו למסחר חי.
ניתוח Walk-Forward
def walk_forward_analysis(
strategy_class, df: pd.DataFrame,
train_size: int = 365, # свечей (дней)
test_size: int = 90,
step_size: int = 30,
param_grid: dict = None
) -> list[dict]:
results = []
n = len(df)
for start in range(0, n - train_size - test_size, step_size):
train_df = df.iloc[start : start + train_size]
test_df = df.iloc[start + train_size : start + train_size + test_size]
# Оптимизация на train данных
if param_grid:
best_params = optimize_params(strategy_class, train_df, param_grid)
else:
best_params = {}
# Тест на out-of-sample данных
strategy = strategy_class(**best_params)
engine = BacktestEngine(strategy, BacktestConfig())
result = engine.run(test_df)
results.append({
'period_start': test_df.index[0],
'period_end': test_df.index[-1],
'params': best_params,
'roi': result.roi,
'sharpe': result.sharpe_ratio,
'max_drawdown': result.max_drawdown,
'win_rate': result.win_rate,
})
return results
איך לפתח בודק היסטורי: שלב אחר שלב
- אנליטיקה: לימוד אסטרטגיית הלקוח, קביעת הנתונים הנדרשים (טווחי זמן, נכסים, תקופות).
- עיצוב: בחירת ארכיטקטורה (מונעת אירועים או שלב-אחר-שלב), עיצוב ממשק האסטרטגיה.
- יישום: כתיבת מנוע עם תמיכה בעמלות, החלקות, ביצועים חלקיים; שילוב מקורות נתונים (Binance, בריכות DEX).
- בדיקות: הרצה על נתונים היסטוריים, ביצוע ניתוח walk-forward, אופטימיזציה של פרמטרים.
- פריסה: מסירת קוד, תיעוד, דוח מדדים; הדרכת הצוות.
פרטי כל שלב נדונים בנפרד ומתועדים במפרט טכני.
השוואת גישות לבדיקות היסטוריות
הגישה שלנו מדויקת בממוצע פי 2 בסימולציה בהשוואה לסקריפטים מותאמים אישית. המנוע שלנו רץ פי 3 מהר יותר מ-backtrader על מערכי נתונים גדולים. הלקוחות שלנו חוסכים בממוצע $5,000 בניפוי באגים בהשוואה לפיתוח פנימי.
| גישה | מהירות | ריאליזם | גמישות |
|---|---|---|---|
| סקריפט מותאם אישית (pandas) | גבוהה | נמוכה (מתעלם מעמלות/החלקות) | גבוהה |
| מנוע מותאם אישית (שלנו) | גבוהה | גבוהה (מתחשב בכל העלויות הנלוות) | בינונית |
| מבוסס ספריות (backtrader, vectorbt) | בינונית | בינונית (התאמה אישית מוגבלת) | נמוכה |
מקורות נתונים והכנה
אנו משתמשים בנתוני מסחר גולמיים מ-Binance, Coinbase ובריכות DEX. הנתונים מנוקים על ידי הסרת חריגים, מילוי חותמות זמן חסרות והתאמה לפיצולים/דיבידנדים. עבור הטיית הישרדות, אנו משלבים מטבעות מתים מ-צילומי מצב היסטוריים של CoinMarketCap. הצינור שלנו מבטיח שלכל סמל יש כיסוי של לפחות 90%; פערים ממולאים קדימה.
ניתוח תוצאות
מדדים מרכזיים
def analyze_results(trades: list[Trade], equity_curve: list, initial_capital: float) -> dict:
pnls = [float(t.pnl) for t in trades]
wins = [p for p in pnls if p > 0]
losses = [p for p in pnls if p <= 0]
# Sharpe Ratio
equity_values = [e[1] for e in equity_curve]
daily_returns = pd.Series(equity_values).pct_change().dropna()
sharpe = daily_returns.mean() / daily_returns.std() * (365 ** 0.5) if daily_returns.std() > 0 else 0
# Max Drawdown
peak = equity_values[0]
max_dd = 0
for val in equity_values:
peak = max(peak, val)
dd = (peak - val) / peak
max_dd = max(max_dd, dd)
return {
'roi_percent': (equity_values[-1] / initial_capital - 1) * 100,
'total_trades': len(trades),
'win_rate': len(wins) / len(trades) * 100 if trades else 0,
'profit_factor': sum(wins) / abs(sum(losses)) if losses else float('inf'),
'sharpe_ratio': sharpe,
'max_drawdown_percent': max_dd * 100,
'avg_win': sum(wins) / len(wins) if wins else 0,
'avg_loss': sum(losses) / len(losses) if losses else 0,
'expectancy': (sum(pnls) / len(pnls)) if pnls else 0, # средний P&L на сделку
} פרשנות תוצאות
| מדד | חלש | סביר | טוב |
|---|---|---|---|
| יחס שארפ | < 0.5 | 0.5-1.5 | > 1.5 |
| גרירה מקסימלית | > 30% | 15-30% | < 15% |
| יחס רווח | < 1.2 | 1.2-2.0 | > 2.0 |
| אחוז זכיות | < 40% | 40-55% | > 55% |
יחס הרווח חשוב יותר מאחוז הזכיות: אסטרטגיה עם 35% זכיות אבל רווח ממוצע פי 3 מהפסד ממוצע היא רווחית. אסטרטגיה עם 65% זכיות אבל רווח ממוצע של 0.5 מההפסד הממוצע היא מפסידה. ניהול סיכונים חייב להיות מובנה באסטרטגיה, לא נוסף בדיעבד.
לעולם אל תריצו אסטרטגיה חיה ללא מעבר ניתוח walk-forward. תוצאות טובות על תקופה אחת עשויות להיות מקריות. עקביות על פני מספר חלונות walk-forward מעידה על יתרון אמיתי.
מה כלול בעבודה שלנו
אנו מספקים:
- קוד מקור של מנוע ה-backtest עם תמיכה בהחלקות, עמלות וביצועים חלקיים.
- אינטגרציה עם נתונים היסטוריים (Binance, Coinbase, בריכות DEX).
- תיעוד המתאר ארכיטקטורה והגדרות.
- דוח עם תוצאות בדיקות (שארפ, גרירה, יחס רווח).
- הדרכה לצוות שלך על שימוש במנוע.
למהנדסים שלנו יש ניסיון רב בפיתוח מערכות מסחר ל-DeFi. אנו מבטיחים שתוצאות ה-backtest יהיו ניתנות לשחזור בסביבת מסחר חיה בדיוק עד לעמלות. עלות הבודק ההיסטורי תלויה בנפח הנתונים ובמורכבות האסטרטגיה, החל מ-$3,000.
צרו קשר כדי לדון בפרויקט שלכם — נעריך את המשימה תוך 2 ימי עסקים. או אם אתם רוצים לבדוק איכות, הזמינו פרויקט פיילוט — תקבלו backtest מלא של אסטרטגיה אחת עם דוח.







