From 213bb911e3b13637ab7b0dc778ec5da9f6a0d505 Mon Sep 17 00:00:00 2001 From: k Date: Sat, 30 May 2026 03:09:16 +0800 Subject: [PATCH] fix(macro): dead placeholder indicators no longer compress composite score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/services/macro/scoring.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/services/macro/scoring.py b/app/services/macro/scoring.py index 977de0f..1bc5003 100644 --- a/app/services/macro/scoring.py +++ b/app/services/macro/scoring.py @@ -74,11 +74,16 @@ def _eth_btc_signal(v: Optional[float]) -> Optional[float]: def _stablecoin_supply_signal(v: Optional[float]) -> Optional[float]: - """Absolute supply tells us little day-over-day; we need the delta. Since - this scorer sees only the snapshot, we treat presence as 0 and let the - visual chart show the trend. Returns 0 if we have any value at all.""" - if v is None: return None - return 0.0 # contribution = 0 until we wire in a trend lookup + """Absolute supply tells us little day-over-day; we need the delta, which + this snapshot-only scorer doesn't have yet. + + Return None (NOT 0.0) so this indicator is EXCLUDED from the weighted sum + and its weight is renormalised away — exactly the "a dead indicator must + not drag the score toward zero" rule stated in the module docstring. + Returning 0.0 would keep its 0.05 weight in the denominator and silently + compress every other indicator's contribution. Wire in a trend lookup to + re-activate it.""" + return None def _etf_flow_signal(v: Optional[float]) -> Optional[float]: @@ -95,9 +100,10 @@ def _etf_flow_signal(v: Optional[float]) -> Optional[float]: def _open_interest_signal(v: Optional[float]) -> Optional[float]: """OI in isolation doesn't tell us direction — we'd need OI vs price - correlation. Until we have a trend window, contribute 0.""" - if v is None: return None - return 0.0 + correlation. Return None (NOT 0.0) until we have a trend window, so this + indicator is excluded + its weight renormalised away rather than diluting + every other indicator. (Same reasoning as _stablecoin_supply_signal.)""" + return None # Weights (sum to 1.0 across all). When an indicator is missing, we drop its