scoring.py docstring promises: 'Missing indicators are excluded and the
remaining weights renormalised — so a single dead API doesn't drag the whole
score toward zero.' But _stablecoin_supply_signal and _open_interest_signal
returned 0.0 (not None) as placeholders — so they stayed in the alive set,
kept their combined 0.10 weight in the denominator, and contributed 0,
systematically compressing the composite toward zero by ~10%.
Example: ahr999 deeply cheap (+1) alone should score +100, but the two 0.0
placeholders dragged it to 80. Now they return None → excluded + renormalised,
matching the module's own stated design. Verified: same input now scores 100.
Re-activate either by returning a real [-1,+1] signal once a trend lookup is
wired in.
72 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>