Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 18 additions & 3 deletions alphaflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down Expand Up @@ -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}")
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handling of the "ignore" case is implicit in the final return statement. For better code maintainability and clarity, consider making it explicit with an else clause or elif self.on_missing_price == "ignore": before the return statement. This would make the three cases more obvious and prevent potential bugs if additional options are added in the future.

Suggested change
logger.warning(f"No price data for symbol {symbol} after timestamp {timestamp}")
logger.warning(f"No price data for symbol {symbol} after timestamp {timestamp}")
elif self.on_missing_price == "ignore":
return 0.0

Copilot uses AI. Check for mistakes.
return 0.0
2 changes: 2 additions & 0 deletions alphaflow/brokers/simple_broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions alphaflow/tests/test_alphaflow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for AlphaFlow core functionality."""

import logging
from datetime import datetime

import pytest
Expand Down Expand Up @@ -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()
Expand Down