"""Daily bar loader for KevinBacktestRunner. Reads from market_data table first; falls back to Alpaca on cache miss and writes through so subsequent runs are warm. """ from __future__ import annotations import logging from datetime import datetime from typing import Any import pandas as pd from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import async_sessionmaker from shared.models.timeseries import MarketData logger = logging.getLogger(__name__) class KevinPriceLoader: def __init__( self, session_factory: async_sessionmaker, alpaca_fetcher: Any, ) -> None: self.session_factory = session_factory self.alpaca = alpaca_fetcher async def daily_bars( self, symbol: str, start: datetime, end: datetime ) -> pd.DataFrame: async with self.session_factory() as session: rows = ( await session.execute( select( MarketData.timestamp, MarketData.open, MarketData.high, MarketData.low, MarketData.close, MarketData.volume, ) .where( and_( MarketData.ticker == symbol, MarketData.timestamp >= start, MarketData.timestamp <= end, ) ) .order_by(MarketData.timestamp) ) ).all() if rows: df = pd.DataFrame( rows, columns=["timestamp", "open", "high", "low", "close", "volume"] ) df = df.set_index("timestamp") return df # cache miss — back-fetch from Alpaca, write through try: df = await self.alpaca.fetch_daily_bars(symbol, start, end) if not df.empty: await self._write_through(symbol, df) return df except Exception as e: logger.warning("alpaca fetch failed for %s: %s", symbol, e) return pd.DataFrame() async def benchmark_bars(self, start: datetime, end: datetime) -> pd.DataFrame: return await self.daily_bars("SPY", start, end) async def is_tradable(self, symbol: str) -> bool: try: return bool(await self.alpaca.is_asset_tradable(symbol)) except Exception: return False async def _write_through(self, symbol: str, df: pd.DataFrame) -> None: async with self.session_factory() as session: for ts, row in df.iterrows(): session.add( MarketData( ticker=symbol, timestamp=ts.to_pydatetime(), open=row["open"], high=row["high"], low=row["low"], close=row["close"], volume=row.get("volume", 0), ) ) await session.commit()