feat: ML-optimized accumulation scoring with dashboard toggle
Train GradientBoostedClassifier on 2,601 days of historical data (2018-2025) to find optimal metric weights for identifying the best long-term buying opportunities. Uses time-series cross-validation to prevent look-ahead bias. Key results: - pct_above_200w_sma: 50.7% weight (was 11.1% equal) - drawdown: 14.6%, lth_rp: 10.9%, rhodl: 8.9% - fear_greed demoted from 11.1% to 5.1% - nupl/mvrv nearly eliminated (0.7-1.8%) ML Strong Accumulation bracket: avg +210% 1yr (vs +176% classic) New files: ml/optimizer.py, config/ml_weights.json Modified: scoring/engine.py (score_all_ml), backtesting/engine.py (ml_mode), dashboard/server.py (Classic/ML toggle) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f1d38f9abb
commit
4647c596b3
@@ -428,3 +428,104 @@ def score_all(metrics):
|
||||
"scored_count": len(valid_scores),
|
||||
"total_count": len(results),
|
||||
}
|
||||
|
||||
|
||||
# ── ML-Optimized Scoring ──────────────────────────────────────────────
|
||||
|
||||
ML_WEIGHTS_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"config",
|
||||
"ml_weights.json",
|
||||
)
|
||||
|
||||
# Maps scoring engine metric keys to ML weight keys
|
||||
_ML_KEY_MAP = {
|
||||
"fear_greed": "fear_greed",
|
||||
"puell_multiple": "puell_multiple",
|
||||
"mvrv_zscore": "mvrv_zscore",
|
||||
"drawdown": "drawdown",
|
||||
"price_vs_200w_sma": "pct_above_200w_sma",
|
||||
"reserve_risk": "reserve_risk",
|
||||
"rhodl_ratio": "rhodl_ratio",
|
||||
"nupl": "nupl",
|
||||
"lth_realized_price": "pct_above_lth_rp",
|
||||
}
|
||||
|
||||
|
||||
def load_ml_weights():
|
||||
"""Load ML-optimized weights from config."""
|
||||
try:
|
||||
with open(ML_WEIGHTS_PATH) as f:
|
||||
data = json.load(f)
|
||||
return data.get("weights", {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def score_all_ml(metrics):
|
||||
"""Score all metrics using ML-optimized weights.
|
||||
|
||||
Same output format as score_all() but uses learned weights
|
||||
instead of equal weighting. Each metric still shows its
|
||||
individual 0-10 score plus the ML weight applied to it.
|
||||
"""
|
||||
# Get classic scores first (reuses all individual scoring logic)
|
||||
classic = score_all(metrics)
|
||||
ml_weights = load_ml_weights()
|
||||
|
||||
if not ml_weights:
|
||||
# Fallback to classic if no ML weights available
|
||||
classic["ml_mode"] = False
|
||||
classic["ml_error"] = "ML weights not found — run ml/optimizer.py"
|
||||
return classic
|
||||
|
||||
results = classic["metrics"]
|
||||
|
||||
# Compute ML-weighted composite
|
||||
weighted_sum = 0.0
|
||||
weight_total = 0.0
|
||||
|
||||
for m in results:
|
||||
if m["score"] is None:
|
||||
continue
|
||||
ml_key = _ML_KEY_MAP.get(m["key"])
|
||||
if ml_key is None:
|
||||
# Hash ribbons or unknown metric — use small default weight
|
||||
w = 0.01
|
||||
else:
|
||||
w = ml_weights.get(ml_key, 0.0)
|
||||
|
||||
m["ml_weight"] = round(w, 4)
|
||||
m["ml_contribution"] = round(m["score"] * w * 10, 2)
|
||||
weighted_sum += m["score"] * w
|
||||
weight_total += w
|
||||
|
||||
# Normalize if weights don't sum to 1 (e.g., missing metrics)
|
||||
if weight_total > 0:
|
||||
composite = weighted_sum / weight_total * 10
|
||||
else:
|
||||
composite = 0
|
||||
|
||||
# Assessment text (same thresholds as classic)
|
||||
if composite >= 80:
|
||||
assessment = "EXTREME ACCUMULATION ZONE"
|
||||
elif composite >= 65:
|
||||
assessment = "STRONG ACCUMULATION ZONE"
|
||||
elif composite >= 50:
|
||||
assessment = "MODERATE OPPORTUNITY"
|
||||
elif composite >= 35:
|
||||
assessment = "NEUTRAL"
|
||||
elif composite >= 20:
|
||||
assessment = "CAUTION — OVERHEATED"
|
||||
else:
|
||||
assessment = "EXTREME CAUTION"
|
||||
|
||||
return {
|
||||
"metrics": results,
|
||||
"composite_score": round(composite, 1),
|
||||
"assessment": assessment,
|
||||
"scored_count": classic["scored_count"],
|
||||
"total_count": classic["total_count"],
|
||||
"ml_mode": True,
|
||||
"classic_score": classic["composite_score"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user