feat(macro): Macro Vibes — 8-indicator daily snapshot + composite score

New backend pipeline: 8 free public macro signals fetched in parallel,
upserted once per calendar day, served via /api/macro/{snapshot,history}.

  - AHR999 (computed from Binance 200d klines)
  - Altcoin Season Index (CoinGecko top-50 30d)
  - Fear & Greed (alternative.me)
  - BTC dominance, ETH/BTC ratio
  - Stablecoin supply (DeFiLlama)
  - Spot BTC ETF net flow (Farside)
  - BTC perp open interest (Binance fapi)

Each fetcher is independently @_none_on_fail decorated so one outage
can't take down the snapshot; scoring renormalises across whichever
indicators returned a value. Daily cron at 03:00 UTC; on startup a
fire-and-forget bootstrap fills today's row if missing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-26 01:04:53 +08:00
parent 5fb1d52026
commit 4442e97f28
12 changed files with 1461 additions and 1 deletions
+34
View File
@@ -28,6 +28,7 @@ from app.api.signals import router as signals_router
from app.api.positions import router as positions_router
from app.api.scanners import router as scanners_router
from app.api.kol import router as kol_router
from app.api.macro import router as macro_router
logging.basicConfig(
level=logging.INFO,
@@ -176,6 +177,38 @@ async def lifespan(app: FastAPI):
)
logger.info("KOL divergence scan scheduled daily at 02:15 UTC.")
# ── Macro indicator daily snapshot ────────────────────────────────────
# 8 indicators (AHR999, Fear & Greed, BTC dominance, ETH/BTC, stablecoin
# supply, ETF flow, BTC OI, altcoin season). One row per calendar date.
# Runs after KOL jobs so a slow KOL fetch can't make this one miss.
from app.services.macro.poll import run_macro_poll
_scheduler.add_job(
run_macro_poll, "cron", hour=3, minute=0,
id="macro_poll", max_instances=1, coalesce=True,
)
logger.info("Macro indicator snapshot scheduled daily at 03:00 UTC.")
# Kick off an initial poll on startup IF today's row doesn't exist yet.
# Otherwise a fresh deploy shows an empty macro panel until 03:00 UTC of
# the next day. Fire-and-forget — never blocks startup.
async def _macro_bootstrap():
try:
from datetime import datetime, timezone
from sqlalchemy import select
from app.models import MacroSnapshot
today = datetime.now(timezone.utc).date()
async with AsyncSessionLocal() as db:
exists = (await db.execute(
select(MacroSnapshot).where(MacroSnapshot.snapshot_date == today)
)).scalar_one_or_none()
if exists is None:
logger.info("Macro: no row for today, running one-shot bootstrap fetch.")
await run_macro_poll()
except Exception as exc:
logger.warning("Macro bootstrap fetch failed: %s (%s)",
type(exc).__name__, exc)
asyncio.create_task(_macro_bootstrap())
_scheduler.start()
logger.info(
"Truth Social pollers scheduled every %ds (CNN + trumpstruth.org).",
@@ -254,6 +287,7 @@ app.include_router(signals_router, prefix="/api")
app.include_router(positions_router, prefix="/api")
app.include_router(scanners_router, prefix="/api")
app.include_router(kol_router, prefix="/api")
app.include_router(macro_router, prefix="/api")
@app.get("/api/health")