feat(backtest): add BacktestEngine with slippage and commission models - Issue #42 (57 tests)
Implements comprehensive backtesting framework: - OrderSide, OrderType, FillStatus enums - OHLCV bar data and Signal dataclasses - BacktestConfig, BacktestPosition, BacktestTrade, BacktestResult - BacktestSnapshot for portfolio tracking over time - BacktestEngine main class Slippage Models: - NoSlippage: Zero slippage (ideal execution) - FixedSlippage: Fixed amount per share - PercentageSlippage: Percentage of price - VolumeSlippage: Volume-impact model with participation ratio Commission Models: - NoCommission: Zero commission - FixedCommission: Fixed per trade - PerShareCommission: Per share with min/max - PercentageCommission: Percentage of trade value - TieredCommission: Tiered by trade value thresholds Features: - Historical price data replay with date filtering - Multi-symbol portfolio support - Automatic position sizing (equal weight) - Buy/sell signal execution with slippage - Cash and position tracking - Shorting support (configurable) - Max position percentage limits - Minimum trade value enforcement - Strategy callback for dynamic signals - Portfolio snapshots at each timestamp - Drawdown tracking with peak detection - Sharpe and Sortino ratio calculation - Win rate, profit factor, avg trade P&L - Total commission and slippage tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6eca9ea07
commit
6e52e4190d
|
|
@ -49,8 +49,8 @@
|
|||
"Issue #48: [DOCS-47] Documentation - user guide, developer docs"
|
||||
],
|
||||
"total_features": 45,
|
||||
"current_index": 37,
|
||||
"completed_features": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36],
|
||||
"current_index": 38,
|
||||
"completed_features": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37],
|
||||
"failed_features": [],
|
||||
"context_token_estimate": 0,
|
||||
"auto_clear_count": 0,
|
||||
|
|
@ -60,5 +60,5 @@
|
|||
"source_type": "issues",
|
||||
"feature_order": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44],
|
||||
"started_at": "2025-12-26T12:35:00Z",
|
||||
"notes": "Issue #2 already implemented. Issue #3: 84 tests (d3892b0). Issue #4: 51 tests (0d09f15). Issue #5: 43 tests (1c6c2fa). Issue #6: 87 tests (1ea006e). Issue #7: migrations fixed + README (68be12c). Issue #8: 108 tests FRED API (4d693fb). Issue #9: 42 tests multi-timeframe (19171a4). Issue #10: 35 tests benchmark (bbd85c9). Issue #11: 84 tests vendor routing (2c80264). Issue #12: 41 tests data cache (ae7899a). Issue #13: 47 tests momentum analyst (8522b4b). Issue #14: 57 tests macro analyst (bdff87a). Issue #15: 59 tests correlation analyst (b0140a8). Issue #16: 52 tests position sizing (a17fc1f). Issue #17: 35 tests analyst integration (5a0606b). Issue #18: 71 tests layered memory (d72c214). Issue #19: 51 tests trade history (dbfcea3). Issue #20: 59 tests risk profiles (25c31d5). Issue #21: 26 tests memory integration (4f6f7c1). Issue #22: 71 tests broker base (e4ef947). Issue #23: 57 tests broker router (850346a). Issue #24: 37 tests alpaca broker (593d599). Issue #25: 38 tests ibkr broker (1e32c0e). Issue #26: 63 tests paper broker (834d18f). Issue #27: 47 tests order manager (6863e3e). Issue #28: 45 tests risk controls (9aee433). Issue #29: 68 tests portfolio state (6642047). Issue #31: 63 tests performance metrics (bedb59b). Issue #32: 66 tests CGT calculator (13f2bba). Issue #33: 45 tests scenario runner (e7bff2c). Issue #34: 43 tests strategy comparator (76eac65). Issue #35: 53 tests economic conditions (b54d6ba). Issue #36: 56 tests signal to order (c423c6b). Issue #37: 37 tests strategy executor (ddb12c1). Issue #38: 55 tests alert manager (7ab60eb). Issue #40: 44 tests slack channel (795f970)."
|
||||
"notes": "Issue #2 already implemented. Issue #3: 84 tests (d3892b0). Issue #4: 51 tests (0d09f15). Issue #5: 43 tests (1c6c2fa). Issue #6: 87 tests (1ea006e). Issue #7: migrations fixed + README (68be12c). Issue #8: 108 tests FRED API (4d693fb). Issue #9: 42 tests multi-timeframe (19171a4). Issue #10: 35 tests benchmark (bbd85c9). Issue #11: 84 tests vendor routing (2c80264). Issue #12: 41 tests data cache (ae7899a). Issue #13: 47 tests momentum analyst (8522b4b). Issue #14: 57 tests macro analyst (bdff87a). Issue #15: 59 tests correlation analyst (b0140a8). Issue #16: 52 tests position sizing (a17fc1f). Issue #17: 35 tests analyst integration (5a0606b). Issue #18: 71 tests layered memory (d72c214). Issue #19: 51 tests trade history (dbfcea3). Issue #20: 59 tests risk profiles (25c31d5). Issue #21: 26 tests memory integration (4f6f7c1). Issue #22: 71 tests broker base (e4ef947). Issue #23: 57 tests broker router (850346a). Issue #24: 37 tests alpaca broker (593d599). Issue #25: 38 tests ibkr broker (1e32c0e). Issue #26: 63 tests paper broker (834d18f). Issue #27: 47 tests order manager (6863e3e). Issue #28: 45 tests risk controls (9aee433). Issue #29: 68 tests portfolio state (6642047). Issue #31: 63 tests performance metrics (bedb59b). Issue #32: 66 tests CGT calculator (13f2bba). Issue #33: 45 tests scenario runner (e7bff2c). Issue #34: 43 tests strategy comparator (76eac65). Issue #35: 53 tests economic conditions (b54d6ba). Issue #36: 56 tests signal to order (c423c6b). Issue #37: 37 tests strategy executor (ddb12c1). Issue #38: 55 tests alert manager (7ab60eb). Issue #40: 44 tests slack channel (795f970). Issue #41: 59 tests SMS channel (b6eca9e)."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
feat(alerts): add Slack channel with webhooks and Block Kit - Issue #40 (44 tests)
|
||||
feat(alerts): add SMS channel with Twilio integration - Issue #41 (59 tests)
|
||||
|
||||
Implements Slack integration for alert delivery:
|
||||
- SlackMessageStyle enum (simple, blocks, attachment)
|
||||
- SlackConfig for webhook URL, channel, username, icon
|
||||
- SlackMessageResult for delivery tracking
|
||||
- SlackMessageFormatter with Block Kit support
|
||||
- SlackChannel implementing AlertChannel protocol
|
||||
- create_slack_channel factory function
|
||||
Implements SMS alert delivery using Twilio API:
|
||||
- SMSFormat enum (plain, compact, detailed)
|
||||
- SMSStatus enum (queued, sending, sent, delivered, failed, etc.)
|
||||
- SMSConfig for Twilio credentials and settings
|
||||
- SMSMessageResult for delivery tracking
|
||||
- SMSBatchResult for multi-recipient sends
|
||||
- SMSMessageFormatter with priority indicators
|
||||
- SMSChannel implementing AlertChannel protocol
|
||||
- create_sms_channel factory function
|
||||
|
||||
Features:
|
||||
- Three formatting styles (simple text, blocks, attachments)
|
||||
- Priority-based colors and emojis
|
||||
- Category emojis for visual identification
|
||||
- @mention support for critical alerts
|
||||
- Timestamp and source inclusion options
|
||||
- Data field display in messages
|
||||
- Webhook URL validation (hooks.slack.com)
|
||||
- E.164 phone number validation
|
||||
- Three formatting styles (plain, compact, detailed)
|
||||
- Priority indicators ([!], [!!!]) for high/critical alerts
|
||||
- Category prefixes (TRD, RSK, SYS, MKT, etc.)
|
||||
- SMS segment counting (GSM-7 vs Unicode)
|
||||
- Messaging service SID support
|
||||
- Priority filtering (send only HIGH+ alerts)
|
||||
- Batch sending to multiple recipients
|
||||
- Retry logic with exponential backoff
|
||||
- Rate limit handling (429 status with Retry-After)
|
||||
- Server error retry (500+) vs client error fail-fast (400)
|
||||
- Server error retry vs client error fail-fast
|
||||
- Latency tracking for performance monitoring
|
||||
- Sync and async delivery methods
|
||||
- Test webhook functionality
|
||||
- Status callback URL support
|
||||
- Connection test functionality
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
|
|
|
|||
|
|
@ -2511,3 +2511,9 @@
|
|||
{"timestamp": "2025-12-26T11:46:58.778745Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/alerts", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/alerts", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:47:09.309952Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/alerts/sms_channel.py", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/alerts/sms_channel.py", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:47:28.653880Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/tests/unit/alerts/test_sms_channel.py", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/tests/unit/alerts/test_sms_channel.py", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:47:55.583233Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/.claude/cache/commit_msg.txt", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/.claude/cache/commit_msg.txt", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:48:31.321467Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/.claude/batch_state.json", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/.claude/batch_state.json", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:49:04.090842Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/.claude/batch_state.json", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/.claude/batch_state.json", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:52:02.146237Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/backtest/backtest_engine.py", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/backtest/backtest_engine.py", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:52:24.761044Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/backtest/__init__.py", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/tradingagents/backtest/__init__.py", "test_mode": false}}
|
||||
{"timestamp": "2025-12-26T11:54:45.126921Z", "event_type": "path_validation", "status": "success", "context": {"operation": "validate_tool_auto-approval", "path": "/Users/andrewkaszubski/Dev/TradingAgents/tests/unit/backtest/test_backtest_engine.py", "resolved": "/Users/andrewkaszubski/Dev/TradingAgents/tests/unit/backtest/test_backtest_engine.py", "test_mode": false}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,964 @@
|
|||
"""Tests for Backtest Engine.
|
||||
|
||||
Issue #42: [BT-41] Backtest engine - historical replay, slippage
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
import pytest
|
||||
|
||||
from tradingagents.backtest import (
|
||||
# Enums
|
||||
OrderSide,
|
||||
OrderType,
|
||||
FillStatus,
|
||||
# Data Classes
|
||||
OHLCV,
|
||||
Signal,
|
||||
BacktestConfig,
|
||||
BacktestPosition,
|
||||
BacktestTrade,
|
||||
BacktestSnapshot,
|
||||
BacktestResult,
|
||||
# Slippage Models
|
||||
SlippageModel,
|
||||
NoSlippage,
|
||||
FixedSlippage,
|
||||
PercentageSlippage,
|
||||
VolumeSlippage,
|
||||
# Commission Models
|
||||
CommissionModel,
|
||||
NoCommission,
|
||||
FixedCommission,
|
||||
PerShareCommission,
|
||||
PercentageCommission,
|
||||
TieredCommission,
|
||||
# Main Classes
|
||||
BacktestEngine,
|
||||
# Factory Functions
|
||||
create_backtest_engine,
|
||||
)
|
||||
|
||||
|
||||
ZERO = Decimal("0")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Enum Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestOrderSide:
|
||||
"""Tests for OrderSide enum."""
|
||||
|
||||
def test_values(self):
|
||||
"""Test enum values."""
|
||||
assert OrderSide.BUY.value == "buy"
|
||||
assert OrderSide.SELL.value == "sell"
|
||||
|
||||
|
||||
class TestOrderType:
|
||||
"""Tests for OrderType enum."""
|
||||
|
||||
def test_values(self):
|
||||
"""Test enum values."""
|
||||
assert OrderType.MARKET.value == "market"
|
||||
assert OrderType.LIMIT.value == "limit"
|
||||
assert OrderType.STOP.value == "stop"
|
||||
assert OrderType.STOP_LIMIT.value == "stop_limit"
|
||||
|
||||
|
||||
class TestFillStatus:
|
||||
"""Tests for FillStatus enum."""
|
||||
|
||||
def test_values(self):
|
||||
"""Test enum values."""
|
||||
assert FillStatus.UNFILLED.value == "unfilled"
|
||||
assert FillStatus.FILLED.value == "filled"
|
||||
assert FillStatus.PARTIAL.value == "partial"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Class Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestOHLCV:
|
||||
"""Tests for OHLCV dataclass."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test OHLCV creation."""
|
||||
bar = OHLCV(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
open=Decimal("100"),
|
||||
high=Decimal("105"),
|
||||
low=Decimal("99"),
|
||||
close=Decimal("103"),
|
||||
volume=Decimal("1000000"),
|
||||
symbol="AAPL",
|
||||
)
|
||||
assert bar.open == Decimal("100")
|
||||
assert bar.close == Decimal("103")
|
||||
assert bar.symbol == "AAPL"
|
||||
|
||||
def test_numeric_conversion(self):
|
||||
"""Test numeric types are converted to Decimal."""
|
||||
bar = OHLCV(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
open=100,
|
||||
high=105,
|
||||
low=99,
|
||||
close=103,
|
||||
volume=1000000,
|
||||
)
|
||||
assert isinstance(bar.open, Decimal)
|
||||
assert bar.open == Decimal("100")
|
||||
|
||||
|
||||
class TestSignal:
|
||||
"""Tests for Signal dataclass."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test signal creation."""
|
||||
signal = Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
)
|
||||
assert signal.symbol == "AAPL"
|
||||
assert signal.side == OrderSide.BUY
|
||||
assert signal.quantity == Decimal("100")
|
||||
|
||||
def test_defaults(self):
|
||||
"""Test signal defaults."""
|
||||
signal = Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
)
|
||||
assert signal.quantity == ZERO
|
||||
assert signal.order_type == OrderType.MARKET
|
||||
assert signal.confidence == Decimal("1")
|
||||
|
||||
|
||||
class TestBacktestConfig:
|
||||
"""Tests for BacktestConfig dataclass."""
|
||||
|
||||
def test_defaults(self):
|
||||
"""Test default configuration."""
|
||||
config = BacktestConfig()
|
||||
assert config.initial_capital == Decimal("100000")
|
||||
assert config.allow_shorting is False
|
||||
assert config.max_position_pct == Decimal("20")
|
||||
|
||||
def test_custom_config(self):
|
||||
"""Test custom configuration."""
|
||||
config = BacktestConfig(
|
||||
initial_capital=Decimal("50000"),
|
||||
allow_shorting=True,
|
||||
max_position_pct=Decimal("10"),
|
||||
)
|
||||
assert config.initial_capital == Decimal("50000")
|
||||
assert config.allow_shorting is True
|
||||
|
||||
|
||||
class TestBacktestPosition:
|
||||
"""Tests for BacktestPosition dataclass."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test position creation."""
|
||||
position = BacktestPosition(
|
||||
symbol="AAPL",
|
||||
quantity=Decimal("100"),
|
||||
average_cost=Decimal("150"),
|
||||
current_price=Decimal("155"),
|
||||
)
|
||||
assert position.symbol == "AAPL"
|
||||
assert position.quantity == Decimal("100")
|
||||
|
||||
def test_market_value(self):
|
||||
"""Test market value calculation."""
|
||||
position = BacktestPosition(
|
||||
symbol="AAPL",
|
||||
quantity=Decimal("100"),
|
||||
average_cost=Decimal("150"),
|
||||
current_price=Decimal("160"),
|
||||
)
|
||||
assert position.market_value == Decimal("16000")
|
||||
|
||||
def test_cost_basis(self):
|
||||
"""Test cost basis calculation."""
|
||||
position = BacktestPosition(
|
||||
symbol="AAPL",
|
||||
quantity=Decimal("100"),
|
||||
average_cost=Decimal("150"),
|
||||
)
|
||||
assert position.cost_basis == Decimal("15000")
|
||||
|
||||
def test_is_long(self):
|
||||
"""Test is_long property."""
|
||||
position = BacktestPosition(symbol="AAPL", quantity=Decimal("100"))
|
||||
assert position.is_long is True
|
||||
assert position.is_short is False
|
||||
|
||||
def test_is_short(self):
|
||||
"""Test is_short property."""
|
||||
position = BacktestPosition(symbol="AAPL", quantity=Decimal("-100"))
|
||||
assert position.is_short is True
|
||||
assert position.is_long is False
|
||||
|
||||
def test_update_price(self):
|
||||
"""Test price update."""
|
||||
position = BacktestPosition(
|
||||
symbol="AAPL",
|
||||
quantity=Decimal("100"),
|
||||
average_cost=Decimal("150"),
|
||||
current_price=Decimal("150"),
|
||||
)
|
||||
position.update_price(Decimal("160"), datetime(2023, 1, 4))
|
||||
assert position.current_price == Decimal("160")
|
||||
assert position.unrealized_pnl == Decimal("1000") # (160-150)*100
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Slippage Model Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestNoSlippage:
|
||||
"""Tests for NoSlippage model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test no slippage."""
|
||||
model = NoSlippage()
|
||||
slippage = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
side=OrderSide.BUY,
|
||||
volume=Decimal("1000000"),
|
||||
)
|
||||
assert slippage == ZERO
|
||||
|
||||
|
||||
class TestFixedSlippage:
|
||||
"""Tests for FixedSlippage model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test fixed slippage."""
|
||||
model = FixedSlippage(Decimal("0.01"))
|
||||
slippage = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
side=OrderSide.BUY,
|
||||
volume=Decimal("1000000"),
|
||||
)
|
||||
assert slippage == Decimal("0.01")
|
||||
|
||||
|
||||
class TestPercentageSlippage:
|
||||
"""Tests for PercentageSlippage model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test percentage slippage."""
|
||||
model = PercentageSlippage(Decimal("0.1")) # 0.1%
|
||||
slippage = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
side=OrderSide.BUY,
|
||||
volume=Decimal("1000000"),
|
||||
)
|
||||
assert slippage == Decimal("0.1") # 0.1% of 100
|
||||
|
||||
|
||||
class TestVolumeSlippage:
|
||||
"""Tests for VolumeSlippage model."""
|
||||
|
||||
def test_calculate_low_volume(self):
|
||||
"""Test low volume participation."""
|
||||
model = VolumeSlippage(
|
||||
base_percentage=Decimal("0.05"),
|
||||
volume_impact=Decimal("0.1"),
|
||||
)
|
||||
slippage = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
side=OrderSide.BUY,
|
||||
volume=Decimal("10000"),
|
||||
)
|
||||
# 100/10000 = 1% participation
|
||||
# slippage = 0.05% + (0.01 * 0.1 * 100) = 0.05% + 0.1% = 0.15%
|
||||
assert slippage > ZERO
|
||||
|
||||
def test_calculate_high_volume(self):
|
||||
"""Test high volume participation."""
|
||||
model = VolumeSlippage(
|
||||
base_percentage=Decimal("0.05"),
|
||||
volume_impact=Decimal("0.1"),
|
||||
max_percentage=Decimal("1.0"),
|
||||
)
|
||||
slippage = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("5000"),
|
||||
side=OrderSide.BUY,
|
||||
volume=Decimal("10000"),
|
||||
)
|
||||
# 50% participation - should hit max
|
||||
assert slippage <= Decimal("1.0") # Max 1%
|
||||
|
||||
def test_calculate_no_volume(self):
|
||||
"""Test with no volume data."""
|
||||
model = VolumeSlippage(base_percentage=Decimal("0.05"))
|
||||
slippage = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
side=OrderSide.BUY,
|
||||
volume=ZERO,
|
||||
)
|
||||
# Falls back to base percentage
|
||||
assert slippage == Decimal("0.05")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Commission Model Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestNoCommission:
|
||||
"""Tests for NoCommission model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test no commission."""
|
||||
model = NoCommission()
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
trade_value=Decimal("10000"),
|
||||
)
|
||||
assert commission == ZERO
|
||||
|
||||
|
||||
class TestFixedCommission:
|
||||
"""Tests for FixedCommission model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test fixed commission."""
|
||||
model = FixedCommission(Decimal("9.99"))
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
trade_value=Decimal("10000"),
|
||||
)
|
||||
assert commission == Decimal("9.99")
|
||||
|
||||
def test_minimum(self):
|
||||
"""Test minimum commission."""
|
||||
model = FixedCommission(Decimal("5"), minimum=Decimal("10"))
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
trade_value=Decimal("10000"),
|
||||
)
|
||||
assert commission == Decimal("10")
|
||||
|
||||
|
||||
class TestPerShareCommission:
|
||||
"""Tests for PerShareCommission model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test per-share commission."""
|
||||
model = PerShareCommission(Decimal("0.005")) # $0.005/share
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
trade_value=Decimal("10000"),
|
||||
)
|
||||
assert commission == Decimal("0.5") # 100 * 0.005
|
||||
|
||||
def test_minimum(self):
|
||||
"""Test minimum commission."""
|
||||
model = PerShareCommission(Decimal("0.005"), minimum=Decimal("1.0"))
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("10"), # Only 10 shares
|
||||
trade_value=Decimal("1000"),
|
||||
)
|
||||
assert commission == Decimal("1.0") # Minimum
|
||||
|
||||
def test_maximum(self):
|
||||
"""Test maximum commission."""
|
||||
model = PerShareCommission(
|
||||
Decimal("0.005"),
|
||||
minimum=ZERO,
|
||||
maximum=Decimal("10"),
|
||||
)
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("10000"), # Many shares
|
||||
trade_value=Decimal("1000000"),
|
||||
)
|
||||
assert commission == Decimal("10") # Maximum
|
||||
|
||||
|
||||
class TestPercentageCommission:
|
||||
"""Tests for PercentageCommission model."""
|
||||
|
||||
def test_calculate(self):
|
||||
"""Test percentage commission."""
|
||||
model = PercentageCommission(Decimal("0.1")) # 0.1%
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("100"),
|
||||
trade_value=Decimal("10000"),
|
||||
)
|
||||
assert commission == Decimal("10") # 0.1% of 10000
|
||||
|
||||
def test_minimum(self):
|
||||
"""Test minimum commission."""
|
||||
model = PercentageCommission(Decimal("0.1"), minimum=Decimal("5"))
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("1"),
|
||||
trade_value=Decimal("100"),
|
||||
)
|
||||
assert commission == Decimal("5") # Minimum
|
||||
|
||||
|
||||
class TestTieredCommission:
|
||||
"""Tests for TieredCommission model."""
|
||||
|
||||
def test_calculate_low_tier(self):
|
||||
"""Test low tier commission."""
|
||||
model = TieredCommission([
|
||||
(Decimal("0"), Decimal("0.2")),
|
||||
(Decimal("10000"), Decimal("0.15")),
|
||||
(Decimal("50000"), Decimal("0.1")),
|
||||
])
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("50"),
|
||||
trade_value=Decimal("5000"),
|
||||
)
|
||||
assert commission == Decimal("10") # 0.2% of 5000
|
||||
|
||||
def test_calculate_mid_tier(self):
|
||||
"""Test middle tier commission."""
|
||||
model = TieredCommission([
|
||||
(Decimal("0"), Decimal("0.2")),
|
||||
(Decimal("10000"), Decimal("0.15")),
|
||||
(Decimal("50000"), Decimal("0.1")),
|
||||
])
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("200"),
|
||||
trade_value=Decimal("20000"),
|
||||
)
|
||||
assert commission == Decimal("30") # 0.15% of 20000
|
||||
|
||||
def test_calculate_high_tier(self):
|
||||
"""Test high tier commission."""
|
||||
model = TieredCommission([
|
||||
(Decimal("0"), Decimal("0.2")),
|
||||
(Decimal("10000"), Decimal("0.15")),
|
||||
(Decimal("50000"), Decimal("0.1")),
|
||||
])
|
||||
commission = model.calculate(
|
||||
price=Decimal("100"),
|
||||
quantity=Decimal("1000"),
|
||||
trade_value=Decimal("100000"),
|
||||
)
|
||||
assert commission == Decimal("100") # 0.1% of 100000
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BacktestEngine Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestBacktestEngine:
|
||||
"""Tests for BacktestEngine class."""
|
||||
|
||||
@pytest.fixture
|
||||
def config(self):
|
||||
"""Create test config."""
|
||||
return BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def engine(self, config):
|
||||
"""Create test engine."""
|
||||
return BacktestEngine(config)
|
||||
|
||||
@pytest.fixture
|
||||
def price_data(self):
|
||||
"""Create test price data."""
|
||||
return {
|
||||
"AAPL": [
|
||||
OHLCV(datetime(2023, 1, 3), 130, 132, 129, 131, 1000000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 4), 131, 135, 130, 134, 1200000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 5), 134, 136, 133, 135, 1100000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 6), 135, 138, 134, 137, 1300000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 9), 137, 140, 136, 139, 1400000, "AAPL"),
|
||||
],
|
||||
}
|
||||
|
||||
def test_initialization(self, engine):
|
||||
"""Test engine initialization."""
|
||||
assert engine.cash == Decimal("100000")
|
||||
assert len(engine.positions) == 0
|
||||
assert len(engine.trades) == 0
|
||||
|
||||
def test_reset(self, engine):
|
||||
"""Test engine reset."""
|
||||
engine.cash = Decimal("50000")
|
||||
engine.positions["AAPL"] = BacktestPosition(symbol="AAPL")
|
||||
engine.reset()
|
||||
assert engine.cash == Decimal("100000")
|
||||
assert len(engine.positions) == 0
|
||||
|
||||
def test_run_empty(self, engine):
|
||||
"""Test run with no data."""
|
||||
result = engine.run({}, [])
|
||||
assert result.total_trades == 0
|
||||
assert result.final_value == Decimal("100000")
|
||||
|
||||
def test_run_no_signals(self, engine, price_data):
|
||||
"""Test run with no signals."""
|
||||
result = engine.run(price_data, [])
|
||||
assert result.total_trades == 0
|
||||
assert result.final_value == Decimal("100000")
|
||||
assert len(result.snapshots) == 5
|
||||
|
||||
def test_run_buy_signal(self, engine, price_data):
|
||||
"""Test run with buy signal."""
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert result.total_trades == 1
|
||||
assert len(engine.positions) == 1
|
||||
assert "AAPL" in engine.positions
|
||||
|
||||
def test_run_buy_and_sell(self, engine, price_data):
|
||||
"""Test run with buy and sell signals."""
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 6),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.SELL,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert result.total_trades == 2
|
||||
assert len(engine.positions) == 0 # Position closed
|
||||
# Should have profit: bought at ~131, sold at ~137
|
||||
assert result.final_value > Decimal("100000")
|
||||
|
||||
def test_run_with_slippage(self, price_data):
|
||||
"""Test run with slippage model."""
|
||||
config = BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
slippage_model=FixedSlippage(Decimal("0.10")),
|
||||
)
|
||||
engine = BacktestEngine(config)
|
||||
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Check slippage was applied
|
||||
trade = result.trades[0]
|
||||
assert trade.slippage > ZERO
|
||||
assert trade.price > trade.base_price # Buy price increased by slippage
|
||||
|
||||
def test_run_with_commission(self, price_data):
|
||||
"""Test run with commission model."""
|
||||
config = BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
commission_model=FixedCommission(Decimal("10")),
|
||||
)
|
||||
engine = BacktestEngine(config)
|
||||
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert result.total_commission == Decimal("10")
|
||||
trade = result.trades[0]
|
||||
assert trade.commission == Decimal("10")
|
||||
|
||||
def test_run_insufficient_cash(self, price_data):
|
||||
"""Test run with insufficient cash."""
|
||||
config = BacktestConfig(
|
||||
initial_capital=Decimal("1000"), # Not enough for 100 shares at $131
|
||||
)
|
||||
engine = BacktestEngine(config)
|
||||
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Should have bought fewer shares
|
||||
if result.total_trades > 0:
|
||||
assert engine.positions["AAPL"].quantity < Decimal("100")
|
||||
# If no trades, that's also acceptable (couldn't afford any)
|
||||
|
||||
def test_run_no_shorting(self, engine, price_data):
|
||||
"""Test no shorting when disabled."""
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.SELL,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Sell should be rejected (no position and shorting disabled)
|
||||
assert result.total_trades == 0
|
||||
|
||||
def test_run_with_shorting(self, price_data):
|
||||
"""Test shorting when enabled."""
|
||||
config = BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
allow_shorting=True,
|
||||
)
|
||||
engine = BacktestEngine(config)
|
||||
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.SELL,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Should have short position
|
||||
assert result.total_trades == 1
|
||||
assert engine.positions["AAPL"].quantity == Decimal("-100")
|
||||
|
||||
def test_run_position_sizing(self, price_data):
|
||||
"""Test automatic position sizing."""
|
||||
engine = BacktestEngine(BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
position_sizing="equal",
|
||||
))
|
||||
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=ZERO, # Auto-size
|
||||
),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Should have calculated quantity
|
||||
if result.total_trades > 0:
|
||||
assert result.trades[0].quantity > ZERO
|
||||
|
||||
def test_get_position(self, engine, price_data):
|
||||
"""Test getting position."""
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
engine.run(price_data, signals)
|
||||
|
||||
position = engine.get_position("AAPL")
|
||||
assert position is not None
|
||||
assert position.quantity == Decimal("100")
|
||||
|
||||
no_position = engine.get_position("GOOG")
|
||||
assert no_position is None
|
||||
|
||||
def test_get_cash(self, engine, price_data):
|
||||
"""Test getting cash balance."""
|
||||
initial_cash = engine.get_cash()
|
||||
assert initial_cash == Decimal("100000")
|
||||
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
engine.run(price_data, signals)
|
||||
|
||||
assert engine.get_cash() < initial_cash
|
||||
|
||||
def test_get_portfolio_value(self, engine, price_data):
|
||||
"""Test getting portfolio value."""
|
||||
signals = [
|
||||
Signal(
|
||||
timestamp=datetime(2023, 1, 3),
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
),
|
||||
]
|
||||
engine.run(price_data, signals)
|
||||
|
||||
value = engine.get_portfolio_value()
|
||||
# Should be approximately initial capital (cash + position value)
|
||||
assert value > Decimal("99000")
|
||||
assert value < Decimal("101000")
|
||||
|
||||
|
||||
class TestBacktestResult:
|
||||
"""Tests for BacktestResult metrics."""
|
||||
|
||||
@pytest.fixture
|
||||
def price_data(self):
|
||||
"""Create test price data with clear trend."""
|
||||
return {
|
||||
"AAPL": [
|
||||
OHLCV(datetime(2023, 1, 3), 100, 102, 99, 100, 1000000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 4), 100, 105, 99, 105, 1200000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 5), 105, 110, 104, 110, 1100000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 6), 110, 115, 109, 115, 1300000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 9), 115, 120, 114, 120, 1400000, "AAPL"),
|
||||
],
|
||||
}
|
||||
|
||||
def test_winning_trade(self, price_data):
|
||||
"""Test metrics for winning trade."""
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("100")),
|
||||
Signal(datetime(2023, 1, 9), "AAPL", OrderSide.SELL, Decimal("100")),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert result.total_trades == 2
|
||||
assert result.winning_trades >= 1
|
||||
assert result.total_return > ZERO
|
||||
assert result.final_value > result.initial_capital
|
||||
|
||||
def test_max_drawdown(self, price_data):
|
||||
"""Test max drawdown calculation."""
|
||||
# Add some volatility
|
||||
price_data["AAPL"].insert(2, OHLCV(
|
||||
datetime(2023, 1, 4, 12), 105, 106, 95, 95, 1000000, "AAPL"
|
||||
))
|
||||
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("100")),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Should have some drawdown recorded
|
||||
assert result.max_drawdown >= ZERO
|
||||
|
||||
def test_snapshots(self, price_data):
|
||||
"""Test snapshot creation."""
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("100")),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert len(result.snapshots) == 5
|
||||
for snapshot in result.snapshots:
|
||||
assert snapshot.total_value > ZERO
|
||||
assert snapshot.cash >= ZERO
|
||||
|
||||
def test_daily_returns(self, price_data):
|
||||
"""Test daily returns calculation."""
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("100")),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert len(result.daily_returns) == 4 # 5 snapshots = 4 returns
|
||||
|
||||
def test_trade_stats(self, price_data):
|
||||
"""Test trade statistics."""
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("50")),
|
||||
Signal(datetime(2023, 1, 5), "AAPL", OrderSide.SELL, Decimal("50")),
|
||||
Signal(datetime(2023, 1, 5), "AAPL", OrderSide.BUY, Decimal("50")),
|
||||
Signal(datetime(2023, 1, 9), "AAPL", OrderSide.SELL, Decimal("50")),
|
||||
]
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert result.total_trades == 4
|
||||
assert result.winning_trades + result.losing_trades + (result.total_trades - result.winning_trades - result.losing_trades) == result.total_trades
|
||||
|
||||
|
||||
class TestBacktestEngineIntegration:
|
||||
"""Integration tests for backtest engine."""
|
||||
|
||||
def test_module_imports(self):
|
||||
"""Test that all classes are exported from module."""
|
||||
from tradingagents.backtest import (
|
||||
OrderSide,
|
||||
OrderType,
|
||||
FillStatus,
|
||||
OHLCV,
|
||||
Signal,
|
||||
BacktestConfig,
|
||||
BacktestPosition,
|
||||
BacktestTrade,
|
||||
BacktestSnapshot,
|
||||
BacktestResult,
|
||||
SlippageModel,
|
||||
NoSlippage,
|
||||
FixedSlippage,
|
||||
PercentageSlippage,
|
||||
VolumeSlippage,
|
||||
CommissionModel,
|
||||
NoCommission,
|
||||
FixedCommission,
|
||||
PerShareCommission,
|
||||
PercentageCommission,
|
||||
TieredCommission,
|
||||
BacktestEngine,
|
||||
create_backtest_engine,
|
||||
)
|
||||
|
||||
# All imports successful
|
||||
assert BacktestEngine is not None
|
||||
assert OrderSide.BUY is not None
|
||||
|
||||
def test_create_backtest_engine_factory(self):
|
||||
"""Test factory function."""
|
||||
engine = create_backtest_engine(
|
||||
initial_capital=Decimal("50000"),
|
||||
slippage=PercentageSlippage(Decimal("0.1")),
|
||||
commission=FixedCommission(Decimal("10")),
|
||||
)
|
||||
|
||||
assert engine.config.initial_capital == Decimal("50000")
|
||||
assert isinstance(engine.config.slippage_model, PercentageSlippage)
|
||||
assert isinstance(engine.config.commission_model, FixedCommission)
|
||||
|
||||
def test_strategy_callback(self):
|
||||
"""Test dynamic signal generation via callback."""
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
price_data = {
|
||||
"AAPL": [
|
||||
OHLCV(datetime(2023, 1, 3), 100, 102, 99, 101, 1000000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 4), 101, 105, 100, 104, 1200000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 5), 104, 108, 103, 107, 1100000, "AAPL"),
|
||||
],
|
||||
}
|
||||
|
||||
def strategy(timestamp, bars):
|
||||
"""Simple momentum strategy."""
|
||||
if "AAPL" in bars and bars["AAPL"].close > Decimal("102"):
|
||||
return [Signal(
|
||||
timestamp=timestamp,
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("10"),
|
||||
)]
|
||||
return []
|
||||
|
||||
result = engine.run(price_data, [], strategy_callback=strategy)
|
||||
|
||||
# Strategy should have generated signals on days 2 and 3
|
||||
assert result.total_trades >= 1
|
||||
|
||||
def test_multi_symbol(self):
|
||||
"""Test with multiple symbols."""
|
||||
engine = BacktestEngine(BacktestConfig(initial_capital=Decimal("100000")))
|
||||
|
||||
price_data = {
|
||||
"AAPL": [
|
||||
OHLCV(datetime(2023, 1, 3), 100, 102, 99, 101, 1000000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 4), 101, 105, 100, 104, 1200000, "AAPL"),
|
||||
],
|
||||
"GOOG": [
|
||||
OHLCV(datetime(2023, 1, 3), 90, 92, 89, 91, 500000, "GOOG"),
|
||||
OHLCV(datetime(2023, 1, 4), 91, 94, 90, 93, 600000, "GOOG"),
|
||||
],
|
||||
}
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("50")),
|
||||
Signal(datetime(2023, 1, 3), "GOOG", OrderSide.BUY, Decimal("50")),
|
||||
]
|
||||
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
assert result.total_trades == 2
|
||||
assert "AAPL" in engine.positions
|
||||
assert "GOOG" in engine.positions
|
||||
|
||||
def test_date_range_filter(self):
|
||||
"""Test date range filtering."""
|
||||
config = BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
start_date=datetime(2023, 1, 4),
|
||||
end_date=datetime(2023, 1, 5),
|
||||
)
|
||||
engine = BacktestEngine(config)
|
||||
|
||||
price_data = {
|
||||
"AAPL": [
|
||||
OHLCV(datetime(2023, 1, 3), 100, 102, 99, 101, 1000000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 4), 101, 105, 100, 104, 1200000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 5), 104, 108, 103, 107, 1100000, "AAPL"),
|
||||
OHLCV(datetime(2023, 1, 6), 107, 110, 106, 109, 1300000, "AAPL"),
|
||||
],
|
||||
}
|
||||
|
||||
signals = [
|
||||
Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("50")), # Before range
|
||||
Signal(datetime(2023, 1, 4), "AAPL", OrderSide.BUY, Decimal("50")), # In range
|
||||
Signal(datetime(2023, 1, 6), "AAPL", OrderSide.SELL, Decimal("50")), # After range
|
||||
]
|
||||
|
||||
result = engine.run(price_data, signals)
|
||||
|
||||
# Only Jan 4 signal should execute
|
||||
assert result.total_trades == 1
|
||||
assert len(result.snapshots) == 2 # Only Jan 4-5
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
"""Backtest module for historical strategy replay.
|
||||
|
||||
Issue #42: [BT-41] Backtest engine - historical replay, slippage
|
||||
|
||||
This module provides backtesting capabilities:
|
||||
- Historical price data replay
|
||||
- Realistic slippage modeling
|
||||
- Commission/fee handling
|
||||
- Position and portfolio tracking
|
||||
- Trade execution simulation
|
||||
- Performance metrics calculation
|
||||
|
||||
Classes:
|
||||
Enums:
|
||||
- OrderSide: Buy or sell
|
||||
- OrderType: Market, limit, stop, stop_limit
|
||||
- FillStatus: Order fill status
|
||||
|
||||
Data Classes:
|
||||
- OHLCV: Price bar data
|
||||
- Signal: Trading signal
|
||||
- BacktestConfig: Backtest configuration
|
||||
- BacktestPosition: Position tracking
|
||||
- BacktestTrade: Trade record
|
||||
- BacktestSnapshot: Portfolio snapshot
|
||||
- BacktestResult: Complete result with metrics
|
||||
|
||||
Slippage Models:
|
||||
- SlippageModel: Base class
|
||||
- NoSlippage: No slippage
|
||||
- FixedSlippage: Fixed amount per share
|
||||
- PercentageSlippage: Percentage of price
|
||||
- VolumeSlippage: Volume-impact model
|
||||
|
||||
Commission Models:
|
||||
- CommissionModel: Base class
|
||||
- NoCommission: No commission
|
||||
- FixedCommission: Fixed per trade
|
||||
- PerShareCommission: Per share commission
|
||||
- PercentageCommission: Percentage of value
|
||||
- TieredCommission: Tiered by trade value
|
||||
|
||||
Main Classes:
|
||||
- BacktestEngine: Main backtest engine
|
||||
|
||||
Example:
|
||||
>>> from tradingagents.backtest import (
|
||||
... BacktestEngine,
|
||||
... BacktestConfig,
|
||||
... OHLCV,
|
||||
... Signal,
|
||||
... OrderSide,
|
||||
... PercentageSlippage,
|
||||
... PercentageCommission,
|
||||
... )
|
||||
>>> from decimal import Decimal
|
||||
>>> from datetime import datetime
|
||||
>>>
|
||||
>>> config = BacktestConfig(
|
||||
... initial_capital=Decimal("100000"),
|
||||
... slippage_model=PercentageSlippage(Decimal("0.1")),
|
||||
... commission_model=PercentageCommission(Decimal("0.1")),
|
||||
... )
|
||||
>>> engine = BacktestEngine(config)
|
||||
>>>
|
||||
>>> price_data = {
|
||||
... "AAPL": [
|
||||
... OHLCV(datetime(2023, 1, 3), 130, 132, 129, 131, 1000000),
|
||||
... OHLCV(datetime(2023, 1, 4), 131, 135, 130, 134, 1200000),
|
||||
... ],
|
||||
... }
|
||||
>>> signals = [
|
||||
... Signal(datetime(2023, 1, 3), "AAPL", OrderSide.BUY, Decimal("100")),
|
||||
... ]
|
||||
>>> result = engine.run(price_data, signals)
|
||||
>>> print(f"Return: {result.total_return}%")
|
||||
"""
|
||||
|
||||
from .backtest_engine import (
|
||||
# Enums
|
||||
OrderSide,
|
||||
OrderType,
|
||||
FillStatus,
|
||||
# Data Classes
|
||||
OHLCV,
|
||||
Signal,
|
||||
BacktestConfig,
|
||||
BacktestPosition,
|
||||
BacktestTrade,
|
||||
BacktestSnapshot,
|
||||
BacktestResult,
|
||||
# Slippage Models
|
||||
SlippageModel,
|
||||
NoSlippage,
|
||||
FixedSlippage,
|
||||
PercentageSlippage,
|
||||
VolumeSlippage,
|
||||
# Commission Models
|
||||
CommissionModel,
|
||||
NoCommission,
|
||||
FixedCommission,
|
||||
PerShareCommission,
|
||||
PercentageCommission,
|
||||
TieredCommission,
|
||||
# Main Classes
|
||||
BacktestEngine,
|
||||
# Factory Functions
|
||||
create_backtest_engine,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"OrderSide",
|
||||
"OrderType",
|
||||
"FillStatus",
|
||||
# Data Classes
|
||||
"OHLCV",
|
||||
"Signal",
|
||||
"BacktestConfig",
|
||||
"BacktestPosition",
|
||||
"BacktestTrade",
|
||||
"BacktestSnapshot",
|
||||
"BacktestResult",
|
||||
# Slippage Models
|
||||
"SlippageModel",
|
||||
"NoSlippage",
|
||||
"FixedSlippage",
|
||||
"PercentageSlippage",
|
||||
"VolumeSlippage",
|
||||
# Commission Models
|
||||
"CommissionModel",
|
||||
"NoCommission",
|
||||
"FixedCommission",
|
||||
"PerShareCommission",
|
||||
"PercentageCommission",
|
||||
"TieredCommission",
|
||||
# Main Classes
|
||||
"BacktestEngine",
|
||||
# Factory Functions
|
||||
"create_backtest_engine",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue