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 asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import select
|
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()
|
_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):
|
class KevinBacktestRunRequest(BaseModel):
|
||||||
"""Request body for /api/meet-kevin/backtest/run."""
|
"""Request body for /api/meet-kevin/backtest/run."""
|
||||||
|
|
||||||
|
|
@ -131,20 +219,9 @@ async def _execute_backtest(
|
||||||
)
|
)
|
||||||
strategy = KevinStrategy(strategy_cfg)
|
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(
|
price_loader = KevinPriceLoader(
|
||||||
session_factory=session_factory, alpaca_fetcher=_NoAlpaca()
|
session_factory=session_factory,
|
||||||
|
alpaca_fetcher=_AlpacaHistoricalFetcher(),
|
||||||
)
|
)
|
||||||
|
|
||||||
runner = KevinBacktestRunner(strategy, price_loader)
|
runner = KevinBacktestRunner(strategy, price_loader)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue