Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Walks mentions chronologically, T+1 entry, time-based exit per KevinStrategy. Reuses backtester/metrics::compute_metrics for headline numbers. KevinPriceLoader fronts market_data + Alpaca.
96 lines
3.1 KiB
Python
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()
|