"""Macro indicators API. GET /api/macro/snapshot Latest macro snapshot (today's row, or the most recent available). GET /api/macro/history?days=30 Time series of every indicator over the last N days. Used by the sparklines on the BTC page macro panel. """ from __future__ import annotations from datetime import datetime, timedelta, timezone from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import MacroSnapshot router = APIRouter(prefix="/macro", tags=["macro"]) def _row_to_dict(r: MacroSnapshot) -> dict: """Snapshot row → JSON-friendly dict, in the canonical indicator order the frontend lists them in. Order rule (see UI mock-up in BtcPageClient MacroPanel): 1. AHR999 2. Altcoin Season Index 3. Fear & Greed 4. BTC Dominance 5. ETH/BTC Ratio 6. Stablecoin Total Supply 7. ETF Net Flow (1d) 8. BTC Open Interest """ return { "date": r.snapshot_date.isoformat() if r.snapshot_date else None, "captured_at": r.captured_at.replace(tzinfo=timezone.utc).isoformat() if r.captured_at else None, "indicators": { "ahr999": r.ahr999, "altcoin_season_index": r.altcoin_season_index, "fear_greed": r.fear_greed, "fear_greed_label": r.fear_greed_label, "btc_dominance_pct": r.btc_dominance_pct, "eth_btc_ratio": r.eth_btc_ratio, "stablecoin_supply_usd": r.stablecoin_supply_usd, "etf_flow_net_usd_1d": r.etf_flow_net_usd_1d, "btc_open_interest_usd": r.btc_open_interest_usd, }, "composite_score": r.composite_score, "regime_label": r.regime_label, } @router.get("/snapshot") async def get_snapshot(db: AsyncSession = Depends(get_db)) -> dict: """Latest macro snapshot. May be null if poll hasn't run yet.""" row = (await db.execute( select(MacroSnapshot).order_by(MacroSnapshot.snapshot_date.desc()).limit(1) )).scalar_one_or_none() if not row: return {"ok": False, "error": "no snapshots yet — poll has not run"} return {"ok": True, **_row_to_dict(row)} @router.get("/history") async def get_history( days: int = Query(default=30, ge=1, le=365), db: AsyncSession = Depends(get_db), ) -> dict: """Time series across the last N days — for the panel sparklines.""" cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).date() rows = (await db.execute( select(MacroSnapshot) .where(MacroSnapshot.snapshot_date >= cutoff) .order_by(MacroSnapshot.snapshot_date.asc()) )).scalars().all() return { "ok": True, "days": days, "count": len(rows), "items": [_row_to_dict(r) for r in rows], }