feat: per-metric historical exploration with click-to-select context
- Click any metric card to see historical periods where it was at a similar level - Purple dot highlighting on chart shows matching periods - Metric overlay line plotted on chart (dashed purple) - Metric Context panel shows percentile, comparable days, avg forward returns, and historical examples from different market cycles - New /api/metric-context endpoint for per-metric similarity analysis - Backtest chart_data now includes per-metric raw values - score_day() returns raw metric values alongside scores - Fixed JS SyntaxError from broken inline onclick escaping (uses addEventListener) Co-Authored-By: Claude Opus 4.6 <<EMAIL>>
This commit is contained in:
co-authored by
Claude Opus 4.6 <<EMAIL>>
parent
4647c596b3
commit
8fca6181d5
+26
-6
@@ -146,9 +146,10 @@ _BT_ML_KEY_MAP = {
|
||||
|
||||
|
||||
def score_day(date, index, drawdowns, ml_weights=None):
|
||||
"""Score a single day using all available metrics. Returns (composite_score, individual_scores, n_metrics).
|
||||
"""Score a single day using all available metrics. Returns (composite_score, details, n_metrics).
|
||||
|
||||
If ml_weights is provided, uses ML-optimized weighting instead of equal weights.
|
||||
details includes both "score" and "raw" (the actual metric value before scoring).
|
||||
"""
|
||||
scores = []
|
||||
details = {}
|
||||
@@ -160,7 +161,7 @@ def score_day(date, index, drawdowns, ml_weights=None):
|
||||
s = _score_range(val, cfg["ranges"])
|
||||
if s is not None:
|
||||
scores.append(s)
|
||||
details[metric_key] = {"value": val, "score": s}
|
||||
details[metric_key] = {"value": val, "score": s, "raw": val}
|
||||
|
||||
# Ratio-based metrics (price vs reference)
|
||||
for metric_key, cfg in RATIO_SCORERS.items():
|
||||
@@ -177,7 +178,7 @@ def score_day(date, index, drawdowns, ml_weights=None):
|
||||
s = _score_range(pct_above, cfg["ranges"])
|
||||
if s is not None:
|
||||
scores.append(s)
|
||||
details[metric_key] = {"value": pct_above, "score": s}
|
||||
details[metric_key] = {"value": pct_above, "score": s, "raw": pct_above}
|
||||
|
||||
# Drawdown
|
||||
dd = drawdowns.get(date)
|
||||
@@ -185,7 +186,7 @@ def score_day(date, index, drawdowns, ml_weights=None):
|
||||
s = _score_range(dd, DRAWDOWN_RANGES)
|
||||
if s is not None:
|
||||
scores.append(s)
|
||||
details["drawdown"] = {"value": dd, "score": s}
|
||||
details["drawdown"] = {"value": dd, "score": s, "raw": dd}
|
||||
|
||||
if not scores:
|
||||
return None, details, 0
|
||||
@@ -297,12 +298,19 @@ def run_backtest(ml_mode=False):
|
||||
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)
|
||||
# Collect raw metric values for per-metric historical exploration
|
||||
metric_values = {}
|
||||
for mk, info in details.items():
|
||||
raw = info.get("raw")
|
||||
if raw is not None:
|
||||
metric_values[mk] = round(raw, 6) if isinstance(raw, float) else raw
|
||||
entry = {
|
||||
"date": d,
|
||||
"score": composite,
|
||||
"n_metrics": n_metrics,
|
||||
"price": price,
|
||||
"forward_returns": fwd_returns.get(d, {}),
|
||||
"metric_values": metric_values,
|
||||
}
|
||||
daily_scores.append(entry)
|
||||
|
||||
@@ -462,6 +470,7 @@ def run_backtest(ml_mode=False):
|
||||
|
||||
# --- Build time series for charting ---
|
||||
# Smart downsampling: daily for last 2 years, weekly before that
|
||||
# Include per-metric values so the frontend can plot any metric.
|
||||
chart_data = []
|
||||
import datetime as _dt
|
||||
try:
|
||||
@@ -469,14 +478,25 @@ def run_backtest(ml_mode=False):
|
||||
cutoff_date = (last_date - _dt.timedelta(days=730)).strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
cutoff_date = "2024-01-01"
|
||||
|
||||
# Collect all metric keys that were ever scored (for per-metric series)
|
||||
all_metric_keys = set()
|
||||
for d in daily_scores:
|
||||
all_metric_keys.update(d.get("metric_values", {}).keys())
|
||||
|
||||
for i, d in enumerate(daily_scores):
|
||||
is_recent = d["date"] >= cutoff_date
|
||||
if is_recent or i % 7 == 0 or i == len(daily_scores) - 1:
|
||||
chart_data.append({
|
||||
entry = {
|
||||
"date": d["date"],
|
||||
"score": d["score"],
|
||||
"price": d["price"],
|
||||
})
|
||||
}
|
||||
# Include per-metric values (raw metric value, not score)
|
||||
metric_vals = d.get("metric_values", {})
|
||||
if metric_vals:
|
||||
entry["metrics"] = metric_vals
|
||||
chart_data.append(entry)
|
||||
|
||||
result = {
|
||||
"date_range": {"start": daily_scores[0]["date"], "end": daily_scores[-1]["date"]},
|
||||
|
||||
Reference in New Issue
Block a user