325 lines
10 KiB
Python
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
|