trading/backtester/kevin_price_loader.py
Viktor Barzin 23ce45a4f2
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
feat(kevin): mention-driven backtest mini-engine
Walks mentions chronologically, T+1 entry, time-based exit per
KevinStrategy. Reuses backtester/metrics::compute_metrics for headline
numbers. KevinPriceLoader fronts market_data + Alpaca.
2026-05-24 00:56:57 +00:00

96 lines
3.1 KiB
Python

"""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()