import math from scoring import engine def _complete_metrics(): return { "fear_greed": {"value": 10, "classification": "Extreme Fear"}, "puell_multiple": {"value": 0.3}, "mvrv_zscore": {"value": -0.1}, "drawdown": {"value": 60.0, "ath": 250.0}, "price": {"price": 100.0}, "200w_sma": {"value": 120.0}, "reserve_risk": {"value": 0.001}, "rhodl_ratio": {"value": 50.0}, "nupl": {"value": -0.1}, "lth_realized_price": {"value": 120.0}, "hash_ribbons": {"buy_signal": False}, } def test_score_all_ml_normalizes_displayed_weights_and_contributions(monkeypatch): monkeypatch.setattr( engine, "load_ml_weights", lambda: { "fear_greed": 0.40, "puell_multiple": 0.20, "mvrv_zscore": 0.15, "drawdown": 0.10, "pct_above_200w_sma": 0.05, "reserve_risk": 0.04, "rhodl_ratio": 0.03, "nupl": 0.02, "pct_above_lth_rp": 0.01, }, ) scored = engine.score_all_ml(_complete_metrics()) assert scored["ml_mode"] is True valid_metrics = [m for m in scored["metrics"] if m["score"] is not None] assert scored["ml_weight_total"] == 1.01 # trained weights + small hash-ribbons fallback assert math.isclose(sum(m["ml_weight"] for m in valid_metrics), 1.0, abs_tol=0.001) assert math.isclose( sum(m["ml_contribution"] for m in valid_metrics), scored["composite_score"], abs_tol=0.05, ) hash_ribbons = next(m for m in valid_metrics if m["key"] == "hash_ribbons") assert hash_ribbons["ml_raw_weight"] == 0.01 assert hash_ribbons["ml_weight"] == round(0.01 / 1.01, 4) def test_score_all_ml_preserves_classic_fallback_when_weights_missing(monkeypatch): monkeypatch.setattr(engine, "load_ml_weights", lambda: {}) scored = engine.score_all_ml(_complete_metrics()) assert scored["ml_mode"] is False assert scored["ml_error"] == "ML weights not found — run ml/optimizer.py" assert "classic_score" not in scored