"""Basic tests for backtest engine. This module contains tests for BacktestEngine initialization, TradeRecord creation, and BacktestResult creation. """ from datetime import datetime, timedelta import pytest from openclaw.backtest.engine import ( BacktestEngine, BacktestEvent, BacktestResult, CommissionModel, EventType, FixedCommissionModel, FixedSlippageModel, PercentageCommissionModel, PercentageSlippageModel, Position, TradeRecord, VolatilitySlippageModel, ) class TestBacktestEngine: """Tests for BacktestEngine class.""" def test_engine_initialization(self): """Test BacktestEngine initialization with default parameters.""" start_date = datetime(2024, 1, 1) end_date = datetime(2024, 12, 31) initial_capital = 100000.0 engine = BacktestEngine( initial_capital=initial_capital, start_date=start_date, end_date=end_date, ) assert engine.initial_capital == initial_capital assert engine.current_equity == initial_capital assert engine.start_date == start_date assert engine.end_date == end_date assert engine.positions == {} assert engine.trades == [] assert engine.equity_curve == [initial_capital] def test_engine_initialization_with_custom_models(self): """Test BacktestEngine initialization with custom slippage and commission models.""" start_date = datetime(2024, 1, 1) end_date = datetime(2024, 12, 31) slippage_model = FixedSlippageModel(fixed_amount=0.02) commission_model = FixedCommissionModel(fixed_amount=10.0) engine = BacktestEngine( initial_capital=50000.0, start_date=start_date, end_date=end_date, slippage_model=slippage_model, commission_model=commission_model, ) assert isinstance(engine.slippage_model, FixedSlippageModel) assert isinstance(engine.commission_model, FixedCommissionModel) assert engine.slippage_model.fixed_amount == 0.02 assert engine.commission_model.fixed_amount == 10.0 def test_engine_reset(self): """Test BacktestEngine reset functionality.""" start_date = datetime(2024, 1, 1) end_date = datetime(2024, 12, 31) engine = BacktestEngine( initial_capital=100000.0, start_date=start_date, end_date=end_date, ) # Modify state engine.current_equity = 50000.0 engine.equity_curve.append(105000.0) # Reset engine.reset() assert engine.current_equity == engine.initial_capital assert engine.positions == {} assert engine.trades == [] assert engine.equity_curve == [engine.initial_capital] def test_get_results_without_data(self): """Test getting results without running backtest.""" engine = BacktestEngine( initial_capital=100000.0, start_date=datetime(2024, 1, 1), end_date=datetime(2024, 12, 31), ) # Engine has initial equity_curve, so it can generate results # Results will just show no change result = engine.get_results() assert result.total_return == 0.0 assert result.total_trades == 0 class TestTradeRecord: """Tests for TradeRecord dataclass.""" def test_trade_record_creation(self): """Test creating a TradeRecord with all fields.""" entry_time = datetime(2024, 1, 1, 10, 0) exit_time = datetime(2024, 1, 2, 10, 0) trade = TradeRecord( symbol="AAPL", entry_time=entry_time, exit_time=exit_time, entry_price=150.0, exit_price=155.0, quantity=100.0, side="long", pnl=500.0, commission=5.0, slippage=2.0, ) assert trade.symbol == "AAPL" assert trade.entry_time == entry_time assert trade.exit_time == exit_time assert trade.entry_price == 150.0 assert trade.exit_price == 155.0 assert trade.quantity == 100.0 assert trade.side == "long" assert trade.pnl == 500.0 assert trade.commission == 5.0 assert trade.slippage == 2.0 def test_trade_record_total_cost(self): """Test TradeRecord total_cost property.""" trade = TradeRecord( symbol="AAPL", entry_time=datetime(2024, 1, 1), exit_time=datetime(2024, 1, 2), entry_price=150.0, exit_price=155.0, quantity=100.0, side="long", pnl=500.0, commission=5.0, slippage=2.0, ) assert trade.total_cost == 7.0 # commission + slippage def test_trade_record_net_pnl(self): """Test TradeRecord net_pnl property.""" trade = TradeRecord( symbol="AAPL", entry_time=datetime(2024, 1, 1), exit_time=datetime(2024, 1, 2), entry_price=150.0, exit_price=155.0, quantity=100.0, side="long", pnl=500.0, commission=5.0, slippage=2.0, ) assert trade.net_pnl == 493.0 # pnl - total_cost def test_trade_record_short_position(self): """Test TradeRecord with short position.""" trade = TradeRecord( symbol="TSLA", entry_time=datetime(2024, 1, 1), exit_time=datetime(2024, 1, 2), entry_price=200.0, exit_price=190.0, quantity=50.0, side="short", pnl=500.0, commission=5.0, slippage=1.0, ) assert trade.side == "short" assert trade.net_pnl == 494.0 class TestBacktestResult: """Tests for BacktestResult class.""" def test_backtest_result_creation(self): """Test creating a BacktestResult with all required fields.""" start_date = datetime(2024, 1, 1) end_date = datetime(2024, 12, 31) result = BacktestResult( start_date=start_date, end_date=end_date, initial_capital=100000.0, final_equity=110000.0, total_return=10.0, total_trades=50, winning_trades=30, losing_trades=20, win_rate=60.0, avg_win=500.0, avg_loss=-200.0, profit_factor=2.5, sharpe_ratio=1.2, max_drawdown=5.0, max_drawdown_duration=10, volatility=15.0, calmar_ratio=2.0, equity_curve=[100000.0, 101000.0, 110000.0], ) assert result.start_date == start_date assert result.end_date == end_date assert result.initial_capital == 100000.0 assert result.final_equity == 110000.0 assert result.total_return == 10.0 assert result.total_trades == 50 assert result.winning_trades == 30 assert result.losing_trades == 20 assert result.win_rate == 60.0 def test_backtest_result_to_dict(self): """Test BacktestResult to_dict method.""" result = BacktestResult( start_date=datetime(2024, 1, 1), end_date=datetime(2024, 12, 31), initial_capital=100000.0, final_equity=110000.0, total_return=10.0, total_trades=50, winning_trades=30, losing_trades=20, win_rate=60.0, avg_win=500.0, avg_loss=-200.0, profit_factor=2.5, sharpe_ratio=1.2, max_drawdown=5.0, max_drawdown_duration=10, volatility=15.0, calmar_ratio=2.0, ) result_dict = result.to_dict() assert isinstance(result_dict, dict) assert result_dict["initial_capital"] == 100000.0 assert result_dict["total_return"] == "10.00%" assert result_dict["win_rate"] == "60.00%" assert "start_date" in result_dict assert "end_date" in result_dict def test_backtest_result_with_trades(self): """Test BacktestResult with trade records.""" trade1 = TradeRecord( symbol="AAPL", entry_time=datetime(2024, 1, 1), exit_time=datetime(2024, 1, 2), entry_price=150.0, exit_price=155.0, quantity=100.0, side="long", pnl=500.0, commission=5.0, slippage=2.0, ) result = BacktestResult( start_date=datetime(2024, 1, 1), end_date=datetime(2024, 12, 31), initial_capital=100000.0, final_equity=110000.0, total_return=10.0, total_trades=1, winning_trades=1, losing_trades=0, win_rate=100.0, avg_win=493.0, avg_loss=0.0, profit_factor=float("inf"), sharpe_ratio=1.5, max_drawdown=0.0, max_drawdown_duration=0, volatility=10.0, calmar_ratio=float("inf"), trades=[trade1], ) assert len(result.trades) == 1 assert result.trades[0].symbol == "AAPL" class TestPosition: """Tests for Position dataclass.""" def test_position_creation(self): """Test creating a Position.""" position = Position( symbol="AAPL", quantity=100.0, entry_price=150.0, entry_time=datetime(2024, 1, 1, 10, 0), side="long", ) assert position.symbol == "AAPL" assert position.quantity == 100.0 assert position.entry_price == 150.0 assert position.side == "long" def test_position_market_value(self): """Test Position market_value property.""" position = Position( symbol="AAPL", quantity=100.0, entry_price=150.0, entry_time=datetime(2024, 1, 1), side="long", ) # market_value is a property that needs current_price # Looking at the source, it's defined with @property but takes current_price # This is a method-style property - need to access differently result = position.market_value(160.0) assert result == 16000.0 def test_position_unrealized_pnl_long(self): """Test Position unrealized_pnl for long position.""" position = Position( symbol="AAPL", quantity=100.0, entry_price=150.0, entry_time=datetime(2024, 1, 1), side="long", ) result1 = position.unrealized_pnl(160.0) result2 = position.unrealized_pnl(140.0) assert result1 == 1000.0 assert result2 == -1000.0 def test_position_unrealized_pnl_short(self): """Test Position unrealized_pnl for short position.""" position = Position( symbol="AAPL", quantity=100.0, entry_price=150.0, entry_time=datetime(2024, 1, 1), side="short", ) result1 = position.unrealized_pnl(140.0) result2 = position.unrealized_pnl(160.0) assert result1 == 1000.0 assert result2 == -1000.0 class TestBacktestEvent: """Tests for BacktestEvent dataclass.""" def test_event_creation(self): """Test creating a BacktestEvent.""" timestamp = datetime(2024, 1, 1, 10, 0) data = {"price": 150.0, "volume": 1000} event = BacktestEvent( event_type=EventType.BAR_OPEN, timestamp=timestamp, data=data, ) assert event.event_type == EventType.BAR_OPEN assert event.timestamp == timestamp assert event.data == data def test_event_types(self): """Test all event types exist.""" assert EventType.BAR_OPEN.name == "BAR_OPEN" assert EventType.BAR_CLOSE.name == "BAR_CLOSE" assert EventType.SIGNAL.name == "SIGNAL" assert EventType.ORDER.name == "ORDER" assert EventType.TRADE.name == "TRADE" assert EventType.END_OF_DATA.name == "END_OF_DATA" class TestSlippageModels: """Tests for slippage models.""" def test_fixed_slippage_model(self): """Test FixedSlippageModel calculation.""" model = FixedSlippageModel(fixed_amount=0.01) slippage = model.calculate_slippage( price=100.0, quantity=100.0, side="buy", volatility=0.02, volume=100000.0, ) assert slippage == 1.0 # 0.01 * 100 def test_percentage_slippage_model(self): """Test PercentageSlippageModel calculation.""" model = PercentageSlippageModel(percentage=0.001) # 0.1% slippage = model.calculate_slippage( price=100.0, quantity=100.0, side="buy", volatility=0.02, volume=100000.0, ) expected = 100.0 * 100.0 * 0.001 # 10.0 assert slippage == expected def test_volatility_slippage_model(self): """Test VolatilitySlippageModel calculation.""" model = VolatilitySlippageModel( base_percentage=0.0005, volatility_multiplier=1.0, ) slippage = model.calculate_slippage( price=100.0, quantity=100.0, side="buy", volatility=0.02, volume=100000.0, ) # trade_value * base * (1 + vol * multiplier) expected = 10000.0 * 0.0005 * (1 + 0.02 * 1.0) assert abs(slippage - expected) < 0.0001 # Floating point comparison class TestCommissionModels: """Tests for commission models.""" def test_fixed_commission_model(self): """Test FixedCommissionModel calculation.""" model = FixedCommissionModel(fixed_amount=5.0) commission = model.calculate_commission(price=100.0, quantity=100.0) assert commission == 5.0 def test_percentage_commission_model(self): """Test PercentageCommissionModel calculation.""" model = PercentageCommissionModel( percentage=0.001, # 0.1% min_commission=1.0, ) commission = model.calculate_commission(price=100.0, quantity=10.0) expected = max(100.0 * 10.0 * 0.001, 1.0) # max(1.0, 1.0) assert commission == expected def test_percentage_commission_with_max(self): """Test PercentageCommissionModel with maximum.""" model = PercentageCommissionModel( percentage=0.01, # 1% min_commission=1.0, max_commission=50.0, ) commission = model.calculate_commission(price=1000.0, quantity=100.0) # trade_value = 100000, 1% = 1000, but max is 50 assert commission == 50.0