fix: judge robustness under the recommended exit, not the abandoned one
The robustness warning was computed on the target-model distribution while the same panel recommends the hold exit — internally inconsistent. _robustness_stats (median, profit factor, ex-top-5% expectancy) is now shared by _bucket_stats and _time_exit_bucket, the time-exit table shows Median Net R and Ex-Top-5% per hold length, and _build_recommendation reads the trimmed expectancy from the recommended exit's bucket (falling back to the target model when no hold is recommended). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -375,14 +375,6 @@ def _bucket_stats(cands: list[dict]) -> dict:
|
||||
holds = [c["hold_days"] for c in cands if c.get("hold_days")]
|
||||
avg_hold = sum(holds) / len(holds) if holds else None
|
||||
net_avg = sum(net_rs) / len(net_rs) if net_rs else None
|
||||
# Robustness: does the edge depend on a handful of outliers? Median and
|
||||
# profit factor describe the distribution; ex-top-5% is the expectancy with
|
||||
# the biggest winners removed — if it stays positive, the edge isn't a
|
||||
# lottery ticket.
|
||||
gains = sum(r for r in net_rs if r > 0)
|
||||
losses_abs = -sum(r for r in net_rs if r < 0)
|
||||
trim_n = math.ceil(len(net_rs) * 0.05) if net_rs else 0
|
||||
trimmed = sorted(net_rs, reverse=True)[trim_n:] if net_rs else []
|
||||
return {
|
||||
"total": len(cands),
|
||||
"wins": wins,
|
||||
@@ -400,7 +392,21 @@ def _bucket_stats(cands: list[dict]) -> dict:
|
||||
"net_r_per_day": (
|
||||
round(net_avg / avg_hold, 4) if net_avg is not None and avg_hold else None
|
||||
),
|
||||
"median_net_r": round(statistics.median(net_rs), 3) if net_rs else None,
|
||||
**_robustness_stats(net_rs),
|
||||
}
|
||||
|
||||
|
||||
def _robustness_stats(net_rs: list[float]) -> dict:
|
||||
"""Distribution-shape stats: the median (typical) trade, gross wins vs
|
||||
losses, and the expectancy with the top 5% of winners removed — the direct
|
||||
test of whether the edge depends on a handful of outliers."""
|
||||
if not net_rs:
|
||||
return {"median_net_r": None, "profit_factor": None, "net_avg_r_ex_top5": None}
|
||||
gains = sum(r for r in net_rs if r > 0)
|
||||
losses_abs = -sum(r for r in net_rs if r < 0)
|
||||
trimmed = sorted(net_rs, reverse=True)[math.ceil(len(net_rs) * 0.05):]
|
||||
return {
|
||||
"median_net_r": round(statistics.median(net_rs), 3),
|
||||
"profit_factor": round(gains / losses_abs, 2) if losses_abs > 0 else None,
|
||||
"net_avg_r_ex_top5": (
|
||||
round(sum(trimmed) / len(trimmed), 3) if trimmed else None
|
||||
@@ -466,6 +472,7 @@ def _time_exit_bucket(cands: list[dict], hold_days: int) -> dict:
|
||||
"net_r_per_day": (
|
||||
round(net_avg / avg_hold, 4) if net_avg is not None and avg_hold else None
|
||||
),
|
||||
**_robustness_stats(net_rs),
|
||||
}
|
||||
|
||||
|
||||
@@ -1190,15 +1197,27 @@ def _build_recommendation(report: dict) -> dict:
|
||||
),
|
||||
})
|
||||
|
||||
# Robustness: does the edge survive without the biggest winners?
|
||||
trimmed = q.get("net_avg_r_ex_top5")
|
||||
# Robustness: does the edge survive without the biggest winners? Judged on
|
||||
# the RECOMMENDED exit — outlier dependence under an exit we'd abandon
|
||||
# would be the wrong warning.
|
||||
hold_recommended = (
|
||||
best_hold is not None and target_net is not None
|
||||
and best_hold["net_avg_r"] > target_net + _EXIT_SWITCH_THRESHOLD
|
||||
)
|
||||
if hold_recommended and best_hold.get("net_avg_r_ex_top5") is not None:
|
||||
trimmed = best_hold["net_avg_r_ex_top5"]
|
||||
basis = f"under the recommended {best_hold['hold_days']}d hold"
|
||||
else:
|
||||
trimmed = q.get("net_avg_r_ex_top5")
|
||||
basis = "under the S/R target exit"
|
||||
if trimmed is not None:
|
||||
if trimmed > 0:
|
||||
items.append({
|
||||
"topic": "robustness",
|
||||
"text": (
|
||||
f"Robustness: expectancy survives removing the top 5% of winners "
|
||||
f"({trimmed:+.2f}R net/trade) — the edge is not a handful of outliers."
|
||||
f"({trimmed:+.2f}R net/trade {basis}) — the edge is not a handful "
|
||||
"of outliers."
|
||||
),
|
||||
})
|
||||
else:
|
||||
@@ -1206,13 +1225,13 @@ def _build_recommendation(report: dict) -> dict:
|
||||
"topic": "robustness",
|
||||
"text": (
|
||||
f"Robustness WARNING: without the top 5% of winners the edge disappears "
|
||||
f"({trimmed:+.2f}R net/trade) — outlier-dependent, treat the headline "
|
||||
"expectancy with caution."
|
||||
f"({trimmed:+.2f}R net/trade {basis}) — outlier-dependent, treat the "
|
||||
"headline expectancy with caution."
|
||||
),
|
||||
})
|
||||
|
||||
headline = None
|
||||
if best_hold is not None and target_net is not None and best_hold["net_avg_r"] > target_net + _EXIT_SWITCH_THRESHOLD:
|
||||
if hold_recommended:
|
||||
cagr_note = (
|
||||
f" (~{hold_sim['cagr_pct']:.0f}% CAGR simulated)"
|
||||
if hold_sim is not None and hold_sim.get("cagr_pct") is not None
|
||||
|
||||
Reference in New Issue
Block a user