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.

131 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 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