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
+58
-7
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user