diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb3eb0..f98b0b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `on_missing_price` config option - control behavior when `get_price()` cannot find data: `"raise"` (default), `"warn"`, or `"ignore"` + ### Changed - Replaced `Make` with `just` for development commands diff --git a/alphaflow/__init__.py b/alphaflow/__init__.py index 0580590..67dcba0 100644 --- a/alphaflow/__init__.py +++ b/alphaflow/__init__.py @@ -308,8 +308,19 @@ def read_event(self, event: Event) -> None: class AlphaFlow: """Event-driven backtesting engine for trading strategies.""" - def __init__(self) -> None: - """Initialize the AlphaFlow backtest engine.""" + def __init__(self, *, on_missing_price: str = "raise") -> None: + """Initialize the AlphaFlow backtest engine. + + Args: + on_missing_price: Behavior when price data is missing. Options are: + - "raise": Raise an error (default) + - "warn": Log a warning and return zero price + - "ignore": Silently return zero price + + """ + if on_missing_price not in ("raise", "warn", "ignore"): + raise ValueError("on_missing_price must be 'raise', 'warn', or 'ignore'") + self.on_missing_price = on_missing_price self.event_bus = EventBus() self.portfolio = Portfolio(self) self.strategies: list[Strategy] = [] @@ -525,4 +536,8 @@ def get_price(self, symbol: str, timestamp: datetime) -> float: for event in self._data[symbol]: if event.timestamp >= timestamp: return event.close - raise ValueError(f"No price data for symbol {symbol} after timestamp {timestamp}") + if self.on_missing_price == "raise": + raise ValueError(f"No price data for symbol {symbol} after timestamp {timestamp}") + elif self.on_missing_price == "warn": + logger.warning(f"No price data for symbol {symbol} after timestamp {timestamp}") + return 0.0 diff --git a/alphaflow/brokers/simple_broker.py b/alphaflow/brokers/simple_broker.py index 02b6aa9..39f9d3d 100644 --- a/alphaflow/brokers/simple_broker.py +++ b/alphaflow/brokers/simple_broker.py @@ -90,6 +90,8 @@ def _can_execute_order(self, event: OrderEvent) -> bool: # For buys, calculate total cost including slippage and commission market_price = self._get_price(event.symbol, event.timestamp) + if market_price == 0: + return False # Calculate expected fill price (with slippage if model provided) if self.slippage_model is not None: diff --git a/alphaflow/tests/test_alphaflow.py b/alphaflow/tests/test_alphaflow.py index 1bd1ae2..f6a934d 100644 --- a/alphaflow/tests/test_alphaflow.py +++ b/alphaflow/tests/test_alphaflow.py @@ -1,5 +1,6 @@ """Tests for AlphaFlow core functionality.""" +import logging from datetime import datetime import pytest @@ -218,6 +219,43 @@ def test_alphaflow_get_price_raises_error_for_missing_data() -> None: af.get_price("AAPL", datetime(2030, 1, 1)) +def test_alphaflow_on_missing_price_warn(caplog: pytest.LogCaptureFixture) -> None: + """Test that on_missing_price='warn' logs a warning and returns 0.0.""" + af = AlphaFlow(on_missing_price="warn") + af.set_data_feed(CSVDataFeed("alphaflow/tests/data/AAPL.csv")) + af.add_equity("AAPL") + af.set_cash(10000) + af.set_data_start_timestamp(datetime(1980, 12, 25)) + af.set_backtest_end_timestamp(datetime(1980, 12, 31)) + af.run() + + with caplog.at_level(logging.WARNING): + price = af.get_price("AAPL", datetime(2030, 1, 1)) + + assert price == 0.0 + assert "No price data for symbol AAPL" in caplog.text + + +def test_alphaflow_on_missing_price_ignore() -> None: + """Test that on_missing_price='ignore' silently returns 0.0.""" + af = AlphaFlow(on_missing_price="ignore") + af.set_data_feed(CSVDataFeed("alphaflow/tests/data/AAPL.csv")) + af.add_equity("AAPL") + af.set_cash(10000) + af.set_data_start_timestamp(datetime(1980, 12, 25)) + af.set_backtest_end_timestamp(datetime(1980, 12, 31)) + af.run() + + price = af.get_price("AAPL", datetime(2030, 1, 1)) + assert price == 0.0 + + +def test_alphaflow_on_missing_price_invalid_value() -> None: + """Test that invalid on_missing_price value raises ValueError.""" + with pytest.raises(ValueError, match="on_missing_price must be 'raise', 'warn', or 'ignore'"): + AlphaFlow(on_missing_price="invalid") + + def test_alphaflow_run_raises_error_without_data_feed() -> None: """Test run raises error when data feed is not set.""" af = AlphaFlow()