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:
BizzleBot
2026-03-21 23:18:29 +00:00
co-authored by Claude Opus 4.6
parent f1d38f9abb
commit 4647c596b3
6 changed files with 942 additions and 18 deletions
+58 -7
View File
@@ -121,8 +121,35 @@ def _compute_ath_series(price_lookup, dates):
return drawdowns
def score_day(date, index, drawdowns):
"""Score a single day using all available metrics. Returns (composite_score, individual_scores, n_metrics)."""
def _load_ml_weights():
"""Load ML weights for ML-optimized scoring mode."""
ml_path = _os.path.join(_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "config", "ml_weights.json")
try:
with open(ml_path) as f:
data = _json.load(f)
return data.get("weights", {})
except Exception:
return {}
# ML weight key mapping (backtest metric keys -> ML weight keys)
_BT_ML_KEY_MAP = {
"fear_greed": "fear_greed",
"puell_multiple": "puell_multiple",
"mvrv_zscore": "mvrv_zscore",
"reserve_risk": "reserve_risk",
"rhodl_ratio": "rhodl_ratio",
"nupl": "nupl",
"price_vs_200w_sma": "pct_above_200w_sma",
"lth_realized_price": "pct_above_lth_rp",
"drawdown": "drawdown",
}
def score_day(date, index, drawdowns, ml_weights=None):
"""Score a single day using all available metrics. Returns (composite_score, individual_scores, n_metrics).
If ml_weights is provided, uses ML-optimized weighting instead of equal weights.
"""
scores = []
details = {}
@@ -163,7 +190,21 @@ def score_day(date, index, drawdowns):
if not scores:
return None, details, 0
composite = sum(scores) / len(scores) * 10
if ml_weights:
# ML-weighted composite
weighted_sum = 0.0
weight_total = 0.0
for metric_key, info in details.items():
ml_key = _BT_ML_KEY_MAP.get(metric_key, metric_key)
w = ml_weights.get(ml_key, 0.0)
weighted_sum += info["score"] * w
weight_total += w
if weight_total > 0:
composite = weighted_sum / weight_total * 10
else:
composite = sum(scores) / len(scores) * 10
else:
composite = sum(scores) / len(scores) * 10
return round(composite, 1), details, len(scores)
@@ -208,9 +249,12 @@ def compute_max_drawdown_forward(price_lookup, date, window=90):
return round(max_dd, 2) if max_dd > 0 else 0
def run_backtest():
"""Run the full backtest and return comprehensive results."""
log.info("Loading historical data...")
def run_backtest(ml_mode=False):
"""Run the full backtest and return comprehensive results.
If ml_mode=True, uses ML-optimized metric weights instead of equal weights.
"""
log.info("Loading historical data... (ml_mode=%s)", ml_mode)
if not os.path.exists(HISTORY_PATH):
return {"error": "No historical data found. Run history collector first."}
@@ -240,11 +284,17 @@ def run_backtest():
log.info("Computing forward returns...")
fwd_returns = compute_forward_returns(price_lookup, all_dates)
# Load ML weights if in ML mode
ml_weights = _load_ml_weights() if ml_mode else None
if ml_mode and not ml_weights:
log.warning("ML mode requested but no weights found — falling back to equal weights")
ml_weights = None
# Score each day
log.info("Scoring %d days...", len(all_dates))
daily_scores = []
for d in all_dates:
composite, details, n_metrics = score_day(d, index, drawdowns)
composite, details, n_metrics = score_day(d, index, drawdowns, ml_weights=ml_weights)
if composite is not None and n_metrics >= 3: # Require at least 3 metrics
price = price_lookup.get(d)
entry = {
@@ -435,6 +485,7 @@ def run_backtest():
"signal_events": signal_events,
"current_context": current_context,
"chart_data": chart_data,
"ml_mode": ml_mode,
"computed_at": datetime.utcnow().isoformat() + "Z",
}