""" 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 sqlalchemy.dialects.postgresql import insert as pg_insert 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 # Build rows for batch insert rows = [] for bar in bars: bar_dt = _parse_bar_timestamp(bar["t"]) rows.append({ "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", }) if not rows: return 0 # Batch insert — skip duplicates via ON CONFLICT DO NOTHING stmt = pg_insert(AlpacaPriceData).values(rows) stmt = stmt.on_conflict_do_nothing(constraint='uq_alpaca_price_data') result = await db.execute(stmt) await db.commit() inserted = result.rowcount if inserted: 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