You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

132 lines
3.9 KiB
Python

"""
Alpaca bars → AlpacaPriceData conversion and DB storage
"""
import logging
from datetime import datetime, timezone
from typing import Dict, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from app.models.alpaca_price import AlpacaPriceData
from app.services.alpaca_client import AlpacaClient
logger = logging.getLogger(__name__)
class AlpacaPriceService:
"""Alpaca bars → AlpacaPriceData conversion and DB storage"""
def __init__(self, client: Optional[AlpacaClient] = None):
self.client = client or AlpacaClient()
def is_available(self) -> bool:
return self.client.is_configured()
async def fetch_and_store_bars(
self,
db: AsyncSession,
ticker: str,
start_date: datetime,
end_date: datetime,
interval: str = "1d",
) -> int:
"""
Fetch bars from Alpaca and upsert into AlpacaPriceData table.
Returns:
Number of newly inserted records.
"""
ticker = ticker.upper()
start_str = start_date.strftime("%Y-%m-%d")
end_str = end_date.strftime("%Y-%m-%d")
bars = await self.client.get_bars(
symbol=ticker,
timeframe=interval,
start=start_str,
end=end_str,
)
if not bars:
logger.warning(f"Alpaca returned 0 bars for {ticker}")
return 0
# Fetch existing dates for dedup
result = await db.execute(
select(AlpacaPriceData.date).where(AlpacaPriceData.ticker == ticker)
)
existing_dates = {row[0].date() for row in result.fetchall()}
inserted = 0
for bar in bars:
bar_dt = _parse_bar_timestamp(bar["t"])
if bar_dt.date() in existing_dates:
continue
record = AlpacaPriceData(
ticker=ticker,
date=bar_dt,
open=float(bar.get("o", 0)),
high=float(bar.get("h", 0)),
low=float(bar.get("l", 0)),
close=float(bar.get("c", 0)),
volume=float(bar.get("v", 0)),
vwap=float(bar["vw"]) if bar.get("vw") else None,
trade_count=int(bar["n"]) if bar.get("n") else None,
data_source="ALPACA",
)
db.add(record)
existing_dates.add(bar_dt.date())
inserted += 1
if inserted:
await db.commit()
logger.info(f"Alpaca: inserted {inserted} bars for {ticker}")
return inserted
async def fetch_bars_raw(
self,
ticker: str,
interval: str = "1d",
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
) -> List[Dict]:
"""
Return raw bar dicts without touching the DB (useful for intraday / non-persistent use).
"""
start_str = start_date.strftime("%Y-%m-%d") if start_date else None
end_str = end_date.strftime("%Y-%m-%d") if end_date else None
bars = await self.client.get_bars(
symbol=ticker.upper(),
timeframe=interval,
start=start_str,
end=end_str,
)
return [
{
"timestamp": bar["t"],
"open": bar.get("o"),
"high": bar.get("h"),
"low": bar.get("l"),
"close": bar.get("c"),
"volume": bar.get("v"),
"vwap": bar.get("vw"),
"trade_count": bar.get("n"),
}
for bar in bars
]
def _parse_bar_timestamp(ts_str: str) -> datetime:
"""Parse Alpaca bar timestamp (RFC-3339) into a timezone-aware datetime."""
# Alpaca returns e.g. "2024-01-02T05:00:00Z"
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt