feat: use day's low price for margin call evaluation
- Extend DailyClosePoint to include low, high, open (optional) - Update Databento source to extract OHLC data from ohlcv-1d schema - Update YFinance source to extract Low, High, Open from history - Modify backtest engine to use worst-case (low) price for margin call detection This ensures margin calls are evaluated at the day's worst price, not just the closing price, providing more realistic risk assessment.
This commit is contained in:
@@ -20,10 +20,19 @@ from app.models.strategy_template import TemplateLeg
|
||||
class DailyClosePoint:
|
||||
date: date
|
||||
close: float
|
||||
low: float | None = None # Day's low for margin call evaluation
|
||||
high: float | None = None # Day's high
|
||||
open: float | None = None # Day's open
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.close <= 0:
|
||||
raise ValueError("close must be positive")
|
||||
if self.low is not None and self.low <= 0:
|
||||
raise ValueError("low must be positive")
|
||||
if self.high is not None and self.high <= 0:
|
||||
raise ValueError("high must be positive")
|
||||
if self.open is not None and self.open <= 0:
|
||||
raise ValueError("open must be positive")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -181,7 +190,9 @@ class BacktestHistoricalProvider(Protocol):
|
||||
|
||||
class YFinanceHistoricalPriceSource:
|
||||
@staticmethod
|
||||
def _normalize_daily_close_row(*, row_date: object, close: object) -> DailyClosePoint | None:
|
||||
def _normalize_daily_close_row(
|
||||
*, row_date: object, close: object, low: object = None, high: object = None, open_price: object = None
|
||||
) -> DailyClosePoint | None:
|
||||
if close is None:
|
||||
return None
|
||||
if not hasattr(row_date, "date"):
|
||||
@@ -192,7 +203,23 @@ class YFinanceHistoricalPriceSource:
|
||||
raise TypeError(f"close must be numeric, got {type(close)!r}")
|
||||
if not isfinite(normalized_close):
|
||||
raise ValueError("historical close must be finite")
|
||||
return DailyClosePoint(date=row_date.date(), close=normalized_close)
|
||||
|
||||
# Parse optional OHLC fields
|
||||
def parse_optional(val: object) -> float | None:
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, (int, float)):
|
||||
result = float(val)
|
||||
return result if isfinite(result) and result > 0 else None
|
||||
return None
|
||||
|
||||
return DailyClosePoint(
|
||||
date=row_date.date(),
|
||||
close=normalized_close,
|
||||
low=parse_optional(low),
|
||||
high=parse_optional(high),
|
||||
open=parse_optional(open_price),
|
||||
)
|
||||
|
||||
def load_daily_closes(self, symbol: str, start_date: date, end_date: date) -> list[DailyClosePoint]:
|
||||
if yf is None:
|
||||
@@ -202,7 +229,13 @@ class YFinanceHistoricalPriceSource:
|
||||
history = ticker.history(start=start_date.isoformat(), end=inclusive_end_date.isoformat(), interval="1d")
|
||||
rows: list[DailyClosePoint] = []
|
||||
for index, row in history.iterrows():
|
||||
point = self._normalize_daily_close_row(row_date=index, close=row.get("Close"))
|
||||
point = self._normalize_daily_close_row(
|
||||
row_date=index,
|
||||
close=row.get("Close"),
|
||||
low=row.get("Low"),
|
||||
high=row.get("High"),
|
||||
open_price=row.get("Open"),
|
||||
)
|
||||
if point is not None:
|
||||
rows.append(point)
|
||||
return rows
|
||||
|
||||
Reference in New Issue
Block a user