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:
+34
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user