feat(backtest): wire real Alpaca historical fetcher
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Previously the backtest API instantiated a _NoAlpaca stub that returned an empty DataFrame for every fetch_daily_bars call, so the mention-driven engine had no price data to mark trades against and every backtest reported total_return=0 / trade_count=0. Replace with _AlpacaHistoricalFetcher which: - Uses StockHistoricalDataClient (alpaca-py) with Day timeframe - Reads creds from TRADING_ALPACA_API_KEY/SECRET_KEY env vars (already injected via trading-bot-secrets ESO) - DataFeed.IEX (free tier — same as services/market_data uses) - Lazy-instantiates the SDK clients on first use to avoid import cost in the api-gateway hot path - Returns indexed DataFrame matching KevinPriceLoader's expected shape ([open, high, low, close, volume], timestamp index) - Returns empty DataFrame on Alpaca failure (loader has its own cache-miss fallback that no-ops gracefully) is_asset_tradable also wired to the real Alpaca TradingClient so the backtest doesn't trade non-tradable tickers.
This commit is contained in:
parent
1a95bfc06a
commit
bcd0857729
1 changed files with 90 additions and 13 deletions
|
|
@ -4,11 +4,13 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
|
|
@ -29,6 +31,92 @@ router = APIRouter(prefix="/api/meet-kevin/backtest", tags=["meet-kevin-backtest
|
|||
_background_tasks: set[asyncio.Task[Any]] = set()
|
||||
|
||||
|
||||
class _AlpacaHistoricalFetcher:
|
||||
"""Adapter that satisfies KevinPriceLoader's alpaca_fetcher protocol
|
||||
using Alpaca's market data API. Reads credentials from env vars
|
||||
(TRADING_ALPACA_API_KEY / TRADING_ALPACA_SECRET_KEY).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._api_key = os.environ.get("TRADING_ALPACA_API_KEY", "")
|
||||
self._secret_key = os.environ.get("TRADING_ALPACA_SECRET_KEY", "")
|
||||
self._data_client: Any = None
|
||||
self._trading_client: Any = None
|
||||
|
||||
def _ensure_data_client(self) -> Any:
|
||||
if self._data_client is None:
|
||||
from alpaca.data.historical import StockHistoricalDataClient
|
||||
|
||||
self._data_client = StockHistoricalDataClient(
|
||||
api_key=self._api_key,
|
||||
secret_key=self._secret_key,
|
||||
)
|
||||
return self._data_client
|
||||
|
||||
def _ensure_trading_client(self) -> Any:
|
||||
if self._trading_client is None:
|
||||
from alpaca.trading.client import TradingClient
|
||||
|
||||
self._trading_client = TradingClient(
|
||||
api_key=self._api_key,
|
||||
secret_key=self._secret_key,
|
||||
paper=True,
|
||||
)
|
||||
return self._trading_client
|
||||
|
||||
async def fetch_daily_bars(
|
||||
self, symbol: str, start: datetime, end: datetime
|
||||
) -> pd.DataFrame:
|
||||
from alpaca.data.enums import DataFeed
|
||||
from alpaca.data.requests import StockBarsRequest
|
||||
from alpaca.data.timeframe import TimeFrame
|
||||
|
||||
client = self._ensure_data_client()
|
||||
request = StockBarsRequest(
|
||||
symbol_or_symbols=[symbol],
|
||||
timeframe=TimeFrame.Day,
|
||||
start=start,
|
||||
end=end,
|
||||
feed=DataFeed.IEX,
|
||||
)
|
||||
try:
|
||||
bars = await asyncio.to_thread(client.get_stock_bars, request)
|
||||
except Exception as exc:
|
||||
logger.warning("Alpaca bars fetch failed for %s: %s", symbol, exc)
|
||||
return pd.DataFrame()
|
||||
|
||||
try:
|
||||
rows = bars[symbol]
|
||||
except (KeyError, IndexError):
|
||||
return pd.DataFrame()
|
||||
|
||||
if not rows:
|
||||
return pd.DataFrame()
|
||||
|
||||
df = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"timestamp": b.timestamp,
|
||||
"open": float(b.open),
|
||||
"high": float(b.high),
|
||||
"low": float(b.low),
|
||||
"close": float(b.close),
|
||||
"volume": float(b.volume),
|
||||
}
|
||||
for b in rows
|
||||
]
|
||||
)
|
||||
return df.set_index("timestamp")
|
||||
|
||||
async def is_asset_tradable(self, symbol: str) -> bool:
|
||||
try:
|
||||
client = self._ensure_trading_client()
|
||||
asset = await asyncio.to_thread(client.get_asset, symbol)
|
||||
return bool(getattr(asset, "tradable", False))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class KevinBacktestRunRequest(BaseModel):
|
||||
"""Request body for /api/meet-kevin/backtest/run."""
|
||||
|
||||
|
|
@ -131,20 +219,9 @@ async def _execute_backtest(
|
|||
)
|
||||
strategy = KevinStrategy(strategy_cfg)
|
||||
|
||||
# Use an Alpaca-less price loader for now — backtest will fall back
|
||||
# to whatever is already in market_data. A real run would inject the
|
||||
# AlpacaBroker here.
|
||||
class _NoAlpaca:
|
||||
async def fetch_daily_bars(self, *a: Any, **kw: Any) -> Any:
|
||||
import pandas as pd
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
async def is_asset_tradable(self, *a: Any, **kw: Any) -> bool:
|
||||
return True
|
||||
|
||||
price_loader = KevinPriceLoader(
|
||||
session_factory=session_factory, alpaca_fetcher=_NoAlpaca()
|
||||
session_factory=session_factory,
|
||||
alpaca_fetcher=_AlpacaHistoricalFetcher(),
|
||||
)
|
||||
|
||||
runner = KevinBacktestRunner(strategy, price_loader)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue