brighter-trading/tests/test_backtest_determinism.py

325 lines
10 KiB
Python

"""
Tests for backtest determinism.
These tests ensure that running the same strategy with the same data
produces identical results every time.
"""
import pytest
from backtest_result import (
BacktestResult, BacktestMetrics, TradeResult,
create_backtest_result
)
class TestBacktestResult:
"""Tests for BacktestResult schema."""
def test_create_backtest_result(self):
"""Test creating a backtest result."""
result = create_backtest_result(
strategy_id='test-strategy-1',
strategy_name='Test Strategy',
user_id=1,
backtest_id='bt-001',
initial_capital=10000.0,
final_value=11500.0,
equity_curve=[10000, 10200, 10100, 10500, 11000, 11500],
trades=[
{'ref': 1, 'pnl': 200, 'side': 'buy'},
{'ref': 2, 'pnl': 300, 'side': 'buy'},
],
stats={
'total_return': 15.0,
'sharpe_ratio': 1.2,
'max_drawdown': -5.0,
'win_rate': 100.0,
'number_of_trades': 2,
},
run_duration=1.5,
)
assert result.success
assert result.initial_capital == 10000.0
assert result.final_portfolio_value == 11500.0
assert len(result.equity_curve) == 6
assert len(result.trades) == 2
assert result.metrics.total_return == 15.0
assert result.metrics.win_rate == 100.0
def test_backtest_result_to_dict(self):
"""Test converting result to dictionary."""
result = create_backtest_result(
strategy_id='test-1',
strategy_name='Test',
user_id=1,
backtest_id='bt-001',
initial_capital=10000,
final_value=10500,
equity_curve=[10000, 10500],
trades=[],
stats={'total_return': 5.0},
run_duration=0.5,
)
d = result.to_dict()
assert isinstance(d, dict)
assert d['strategy_id'] == 'test-1'
assert d['initial_capital'] == 10000
assert isinstance(d['metrics'], dict)
def test_backtest_result_to_json(self):
"""Test JSON serialization."""
result = create_backtest_result(
strategy_id='test-1',
strategy_name='Test',
user_id=1,
backtest_id='bt-001',
initial_capital=10000,
final_value=10500,
equity_curve=[10000, 10500],
trades=[],
stats={},
run_duration=0.5,
)
json_str = result.to_json()
assert isinstance(json_str, str)
assert 'test-1' in json_str
def test_backtest_result_from_dict(self):
"""Test creating result from dictionary."""
data = {
'strategy_id': 'test-1',
'strategy_name': 'Test',
'user_id': 1,
'backtest_id': 'bt-001',
'start_date': '2024-01-01T00:00:00',
'end_date': '2024-01-31T00:00:00',
'run_datetime': '2024-02-01T12:00:00',
'run_duration_seconds': 1.5,
'initial_capital': 10000,
'final_portfolio_value': 10500,
'commission_rate': 0.001,
'success': True,
'equity_curve': [10000, 10250, 10500],
'trades': [],
'metrics': {
'total_return': 5.0,
'number_of_trades': 0,
}
}
result = BacktestResult.from_dict(data)
assert result.strategy_id == 'test-1'
assert result.initial_capital == 10000
assert result.metrics.total_return == 5.0
class TestBacktestDeterminism:
"""Tests for verifying backtest determinism."""
def test_same_inputs_same_hash(self):
"""Test that identical inputs produce the same hash."""
result1 = create_backtest_result(
strategy_id='strategy-abc',
strategy_name='Test Strategy',
user_id=1,
backtest_id='bt-001',
initial_capital=10000.0,
final_value=11000.0,
equity_curve=[10000, 10500, 11000],
trades=[
{'ref': 1, 'pnl': 500, 'side': 'buy'},
{'ref': 2, 'pnl': 500, 'side': 'sell'},
],
stats={
'total_return': 10.0,
'number_of_trades': 2,
'win_rate': 100.0,
},
run_duration=1.0,
)
result2 = create_backtest_result(
strategy_id='strategy-abc',
strategy_name='Test Strategy',
user_id=1,
backtest_id='bt-002', # Different ID
initial_capital=10000.0,
final_value=11000.0,
equity_curve=[10000, 10500, 11000],
trades=[
{'ref': 1, 'pnl': 500, 'side': 'buy'},
{'ref': 2, 'pnl': 500, 'side': 'sell'},
],
stats={
'total_return': 10.0,
'number_of_trades': 2,
'win_rate': 100.0,
},
run_duration=2.0, # Different runtime
)
# Hashes should be identical despite different backtest_id and run_duration
assert result1.get_determinism_hash() == result2.get_determinism_hash()
def test_different_results_different_hash(self):
"""Test that different results produce different hashes."""
result1 = create_backtest_result(
strategy_id='strategy-abc',
strategy_name='Test',
user_id=1,
backtest_id='bt-001',
initial_capital=10000.0,
final_value=11000.0,
equity_curve=[10000, 10500, 11000],
trades=[{'pnl': 1000}],
stats={'total_return': 10.0, 'win_rate': 100.0, 'number_of_trades': 1},
run_duration=1.0,
)
result2 = create_backtest_result(
strategy_id='strategy-abc',
strategy_name='Test',
user_id=1,
backtest_id='bt-001',
initial_capital=10000.0,
final_value=10500.0, # Different final value
equity_curve=[10000, 10250, 10500], # Different curve
trades=[{'pnl': 500}], # Different PnL
stats={'total_return': 5.0, 'win_rate': 100.0, 'number_of_trades': 1},
run_duration=1.0,
)
assert result1.get_determinism_hash() != result2.get_determinism_hash()
def test_verify_determinism(self):
"""Test the verify_determinism method."""
result1 = create_backtest_result(
strategy_id='strategy-1',
strategy_name='Test',
user_id=1,
backtest_id='bt-001',
initial_capital=10000,
final_value=10500,
equity_curve=[10000, 10250, 10500],
trades=[],
stats={'total_return': 5.0, 'number_of_trades': 0, 'win_rate': 0.0},
run_duration=1.0,
)
# Same result
result2 = create_backtest_result(
strategy_id='strategy-1',
strategy_name='Test',
user_id=1,
backtest_id='bt-002',
initial_capital=10000,
final_value=10500,
equity_curve=[10000, 10250, 10500],
trades=[],
stats={'total_return': 5.0, 'number_of_trades': 0, 'win_rate': 0.0},
run_duration=2.0,
)
assert result1.verify_determinism(result2)
def test_floating_point_precision(self):
"""Test that floating point precision doesn't break determinism."""
# Results with slightly different floating point representations
result1 = create_backtest_result(
strategy_id='strategy-1',
strategy_name='Test',
user_id=1,
backtest_id='bt-001',
initial_capital=10000.0,
final_value=10500.123456,
equity_curve=[10000.0, 10500.123456],
trades=[{'pnl': 500.123456}],
stats={'total_return': 5.001234, 'number_of_trades': 1, 'win_rate': 100.0},
run_duration=1.0,
)
result2 = create_backtest_result(
strategy_id='strategy-1',
strategy_name='Test',
user_id=1,
backtest_id='bt-002',
initial_capital=10000.0,
final_value=10500.123456,
equity_curve=[10000.0, 10500.123456],
trades=[{'pnl': 500.123456}],
stats={'total_return': 5.001234, 'number_of_trades': 1, 'win_rate': 100.0},
run_duration=1.0,
)
# Should still be equal due to rounding in hash
assert result1.get_determinism_hash() == result2.get_determinism_hash()
class TestBacktestMetrics:
"""Tests for BacktestMetrics."""
def test_metrics_defaults(self):
"""Test that metrics have sensible defaults."""
metrics = BacktestMetrics()
assert metrics.total_return == 0.0
assert metrics.number_of_trades == 0
assert metrics.win_rate == 0.0
def test_metrics_to_dict(self):
"""Test metrics conversion to dict."""
metrics = BacktestMetrics(
total_return=15.5,
sharpe_ratio=1.2,
number_of_trades=10,
win_rate=60.0,
)
d = metrics.to_dict()
assert d['total_return'] == 15.5
assert d['number_of_trades'] == 10
class TestTradeResult:
"""Tests for TradeResult."""
def test_trade_result_creation(self):
"""Test creating a trade result."""
trade = TradeResult(
ref=1,
symbol='BTC/USDT',
side='buy',
open_datetime='2024-01-01T10:00:00',
close_datetime='2024-01-01T12:00:00',
size=0.1,
open_price=50000,
close_price=51000,
pnl=100,
pnlcomm=99,
commission=1,
)
assert trade.ref == 1
assert trade.symbol == 'BTC/USDT'
assert trade.pnl == 100
def test_trade_result_to_dict(self):
"""Test trade result conversion to dict."""
trade = TradeResult(
ref=1,
symbol='BTC/USDT',
side='buy',
open_datetime='2024-01-01T10:00:00',
close_datetime=None,
size=0.1,
open_price=50000,
close_price=None,
pnl=0,
pnlcomm=0,
)
d = trade.to_dict()
assert isinstance(d, dict)
assert d['ref'] == 1
assert d['close_datetime'] is None