Backtesting Guide¶
Learn how to backtest trading strategies with historical data using dgbit's backtesting engine.
Overview¶
Backtesting simulates how a strategy would have performed on historical data. This helps you:
- Evaluate strategy performance before risking real capital
- Compare different strategies objectively
- Optimize strategy parameters
- Understand risk characteristics
Basic Backtesting¶
Step 1: Fetch Historical Data¶
from dgbit_core.data.data_fetcher import BybitDataFetcher
fetcher = BybitDataFetcher(api_key="", api_secret="", testnet=True)
data = fetcher.get_kline_data(
symbol="BTCUSDT",
interval="15", # 15-minute candles
limit=1000, # Bybit caps `limit` at 1000 per request
)
print(f"Data range: {data['timestamp'].min()} to {data['timestamp'].max()}")
Step 2: Configure the Backtest¶
from dgbit_core.backtesting import BacktestConfig
config = BacktestConfig(
initial_capital=10000.0, # Starting capital in USDT
transaction_fee=0.001, # 0.1% trading fee
train_split=0.7, # Use 70% for training, 30% for testing
report_dir="reports", # Where to save reports
)
Step 3: Select a Strategy¶
from dgbit_core.trading.strategy import WaveletReversalStrategy
strategy = WaveletReversalStrategy(
min_signal_threshold=0.75,
take_profit_pct=0.02,
stop_loss_pct=0.01,
)
Step 4: Run the Backtest¶
from dgbit_core.backtesting import Backtester
backtester = Backtester(config=config)
backtester.strategy = strategy
result = backtester.run(data)
Step 5: Analyze Results¶
# Performance metrics
print(f"Total Trades: {result.metrics['total_trades']}")
print(f"Win Rate: {result.metrics['win_rate']:.2%}")
print(f"Total Return: {result.metrics['total_return']:.2%}")
print(f"Max Drawdown: {result.metrics['max_drawdown']:.2%}")
print(f"Profit Factor: {result.metrics['profit_factor']:.2f}")
print(f"Avg Trade Duration: {result.metrics['avg_duration']:.1f} minutes")
# Trade details
for trade in result.trades[:5]:
print(f"{trade.timestamp}: {trade.action} @ {trade.price:.2f}")
Understanding Metrics¶
Win Rate¶
Percentage of profitable trades.
Good values: > 50% for trend following, > 55% for mean reversion
Total Return¶
Overall percentage gain/loss on initial capital.
Maximum Drawdown¶
Largest peak-to-trough decline during the backtest.
Good values: < 20% for conservative, < 40% for aggressive strategies
Profit Factor¶
Ratio of gross profits to gross losses.
Good values: > 1.5 (profitable), > 2.0 (excellent)
Sharpe ratio is not computed
The current Backtester._calculate_metrics() returns total_trades, win_rate, avg_return, avg_duration, final_capital, total_return, max_drawdown, profit_factor, wins, and losses. Sharpe ratio and other risk-adjusted measures are not included; compute them from result.equity_curve if required.
Advanced Backtesting¶
Parameter Optimization¶
Test different parameter combinations:
from dgbit_core.backtesting import Backtester, BacktestConfig
from dgbit_core.trading.strategy import WaveletReversalStrategy
results = []
# Grid search over parameters
for threshold in [0.6, 0.7, 0.8, 0.9]:
for tp in [0.015, 0.02, 0.025, 0.03]:
for sl in [0.005, 0.01, 0.015]:
strategy = WaveletReversalStrategy(
min_signal_threshold=threshold,
take_profit_pct=tp,
stop_loss_pct=sl,
)
backtester = Backtester(config=config)
backtester.strategy = strategy
result = backtester.run(data)
results.append({
'threshold': threshold,
'take_profit': tp,
'stop_loss': sl,
'return': result.metrics['total_return'],
'win_rate': result.metrics['win_rate'],
'max_drawdown': result.metrics['max_drawdown'],
})
# Find best parameters
import pandas as pd
df = pd.DataFrame(results)
best = df.loc[df['return'].idxmax()]
print(f"Best parameters: {best}")
Walk-Forward Analysis¶
Test strategy robustness with rolling windows:
from datetime import timedelta
def walk_forward_backtest(data, strategy_class, window_size=500, step_size=100):
"""
Walk-forward backtesting with rolling training/testing windows.
"""
results = []
for i in range(0, len(data) - window_size, step_size):
window_data = data.iloc[i:i + window_size]
backtester = Backtester(config=config)
backtester.strategy = strategy_class()
result = backtester.run(window_data)
results.append({
'start': window_data['timestamp'].iloc[0],
'end': window_data['timestamp'].iloc[-1],
'return': result.metrics['total_return'],
'trades': result.metrics['total_trades'],
})
return pd.DataFrame(results)
# Run walk-forward analysis
wf_results = walk_forward_backtest(data, WaveletReversalStrategy)
print(f"Average return across windows: {wf_results['return'].mean():.2%}")
print(f"Consistency: {(wf_results['return'] > 0).mean():.2%} profitable windows")
Multi-Asset Backtesting¶
Test across multiple trading pairs:
symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"]
portfolio_results = {}
for symbol in symbols:
data = fetcher.get_kline_data(symbol, interval="15", limit=1000)
backtester = Backtester(config=config)
backtester.strategy = WaveletReversalStrategy()
result = backtester.run(data)
portfolio_results[symbol] = result.metrics
# Compare performance
for symbol, metrics in portfolio_results.items():
print(f"{symbol}: Return={metrics['total_return']:.2%}, "
f"Win Rate={metrics['win_rate']:.2%}")
Backtest Reports¶
Backtester.run(...) does not write any files. To render the interactive Plotly HTML reports, call plot_results after the run:
result = backtester.run(data)
files = backtester.plot_results(data, result)
print(f"Chart: {files['chart']}")
print(f"Metrics: {files['metrics']}")
Two HTML files are written into config.report_dir (default reports), timestamped per run:
- A candlestick chart with entry/exit markers and an account balance subplot.
- A standalone Plotly table of
Win Rate,Avg Return,Total Return,Max Drawdown,Total Trades, andProfit Factor.
Via the API¶
Schedule backtests through the REST API:
import httpx
import time
# Schedule backtest
response = httpx.post(
"http://localhost:8000/api/backtests",
json={
"symbol": "BTCUSDT",
"interval": "15",
"limit": 1000,
"initial_capital": 10000.0,
"transaction_fee": 0.001,
}
)
job = response.json()
job_id = job['job_id']
# Poll for completion
while True:
status = httpx.get(f"http://localhost:8000/api/jobs/{job_id}").json()
if status['status'] in ['completed', 'failed']:
break
time.sleep(1)
# Get results
if status['status'] == 'completed':
print(f"Results: {status['result']}")
Best Practices¶
Avoid Overfitting¶
- Use out-of-sample testing (train/test split)
- Perform walk-forward analysis
- Don't optimize too many parameters
- Test on multiple market conditions
Account for Realistic Costs¶
- Include transaction fees
- Account for slippage (especially for larger orders)
- Consider funding rates for perpetuals
Validate Data Quality¶
# Check for missing data
print(f"Missing values: {data.isnull().sum().sum()}")
# Check for gaps
data['time_diff'] = data['timestamp'].diff()
gaps = data[data['time_diff'] > pd.Timedelta(minutes=20)]
print(f"Data gaps: {len(gaps)}")
Document Your Tests¶
Keep records of:
- Strategy parameters tested
- Data ranges used
- Performance metrics
- Observations and insights
Next Steps¶
- Custom Strategies - Build your own strategies
- Live Trading - Move from backtest to live
- Strategy Reference - All strategy parameters