add: 30d/90d/180d/365d forward returns in all backtest views

- Bracket table now shows Avg 30d, 90d, 180d, and 1yr columns
- Signal events show all 4 timeframes
- Current context shows all 4 average returns
- Comparable examples show all available timeframes
- Updated backtest screenshot
This commit is contained in:
BizzleBot 2026-03-20 23:20:42 +00:00
parent 0ddb4ab01b
commit e385765fda
5 changed files with 26 additions and 13 deletions

View File

@ -361,11 +361,13 @@ def run_backtest():
if abs(d["score"] - current_score) <= margin and d["forward_returns"]: if abs(d["score"] - current_score) <= margin and d["forward_returns"]:
comparable.append(d) comparable.append(d)
avg_1yr = None avg_returns = {}
if comparable: if comparable:
yr_returns = [d["forward_returns"]["365d"] for d in comparable if "365d" in d["forward_returns"]] for period in ["30d", "90d", "180d", "365d"]:
if yr_returns: vals = [d["forward_returns"][period] for d in comparable if period in d["forward_returns"]]
avg_1yr = round(sum(yr_returns) / len(yr_returns), 2) if vals:
avg_returns[period] = round(sum(vals) / len(vals), 2)
avg_1yr = avg_returns.get("365d")
# Best comparable examples (most recent 5) # Best comparable examples (most recent 5)
examples = [] examples = []
@ -383,6 +385,9 @@ def run_backtest():
"percentile": percentile, "percentile": percentile,
"comparable_days": len(comparable), "comparable_days": len(comparable),
"avg_1yr_return": avg_1yr, "avg_1yr_return": avg_1yr,
"avg_30d_return": avg_returns.get("30d"),
"avg_90d_return": avg_returns.get("90d"),
"avg_180d_return": avg_returns.get("180d"),
"examples": examples, "examples": examples,
} }

View File

@ -1,6 +1,6 @@
{ {
"provider": "ollama", "provider": "openrouter",
"model": "qwen3.5:27b", "model": "minimax/minimax-m2.5",
"providers": { "providers": {
"ollama": { "ollama": {
"base_url": "http://100.100.242.21:11434" "base_url": "http://100.100.242.21:11434"
@ -15,7 +15,7 @@
"api_key": "" "api_key": ""
}, },
"openrouter": { "openrouter": {
"api_key": "" "api_key": "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"
} }
} }
} }

View File

@ -1121,7 +1121,7 @@ tr:hover td{background:var(--card-hover)}
<thead> <thead>
<tr> <tr>
<th>Score Range</th><th>Label</th><th>Days</th> <th>Score Range</th><th>Label</th><th>Days</th>
<th>Avg 30d</th><th>Avg 90d</th><th>Avg 1yr</th> <th>Avg 30d</th><th>Avg 90d</th><th>Avg 180d</th><th>Avg 1yr</th>
<th>Win Rate (1yr)</th><th>Max Gain (1yr)</th><th>Max Loss (1yr)</th> <th>Win Rate (1yr)</th><th>Max Gain (1yr)</th><th>Max Loss (1yr)</th>
<th>Avg Max DD</th> <th>Avg Max DD</th>
</tr> </tr>
@ -1228,7 +1228,7 @@ function renderAll() {
document.getElementById('mainContent').innerHTML = ` document.getElementById('mainContent').innerHTML = `
<div class="section"><div class="context-box" id="contextBox"><h2>Current Signal Context</h2><div class="context-score" id="ctxScore">--</div><div class="context-percentile" id="ctxPercentile"></div><div class="context-return" id="ctxReturn"></div><div class="comparable-list" id="ctxComparables"></div></div></div> <div class="section"><div class="context-box" id="contextBox"><h2>Current Signal Context</h2><div class="context-score" id="ctxScore">--</div><div class="context-percentile" id="ctxPercentile"></div><div class="context-return" id="ctxReturn"></div><div class="comparable-list" id="ctxComparables"></div></div></div>
<div class="section"><div class="card"><h2>Historical Score vs BTC Price</h2><div class="chart-dual"><canvas id="dualChart"></canvas></div></div></div> <div class="section"><div class="card"><h2>Historical Score vs BTC Price</h2><div class="chart-dual"><canvas id="dualChart"></canvas></div></div></div>
<div class="section"><div class="card"><h2>Score Bracket Performance</h2><div style="overflow-x:auto"><table><thead><tr><th>Score Range</th><th>Label</th><th>Days</th><th>Avg 30d</th><th>Avg 90d</th><th>Avg 1yr</th><th>Win Rate (1yr)</th><th>Max Gain</th><th>Max Loss</th><th>Avg Max DD</th></tr></thead><tbody id="bracketBody"></tbody></table></div></div></div> <div class="section"><div class="card"><h2>Score Bracket Performance</h2><div style="overflow-x:auto"><table><thead><tr><th>Score Range</th><th>Label</th><th>Days</th><th>Avg 30d</th><th>Avg 90d</th><th>Avg 180d</th><th>Avg 1yr</th><th>Win Rate (1yr)</th><th>Max Gain</th><th>Max Loss</th><th>Avg Max DD</th></tr></thead><tbody id="bracketBody"></tbody></table></div></div></div>
<div class="section"><div class="card"><h2>Major Signal Events (Score Crossed 70/80/90+)</h2><div id="signalEvents"></div></div></div> <div class="section"><div class="card"><h2>Major Signal Events (Score Crossed 70/80/90+)</h2><div id="signalEvents"></div></div></div>
`; `;
renderContext(); renderContext();
@ -1246,9 +1246,12 @@ function renderContext() {
document.getElementById('ctxPercentile').textContent = document.getElementById('ctxPercentile').textContent =
'Historical percentile: top ' + (100 - ctx.percentile).toFixed(1) + '% of all days (' + ctx.comparable_days + ' comparable days found)'; 'Historical percentile: top ' + (100 - ctx.percentile).toFixed(1) + '% of all days (' + ctx.comparable_days + ' comparable days found)';
if (ctx.avg_1yr_return != null) { if (ctx.avg_1yr_return != null) {
document.getElementById('ctxReturn').textContent = let retHtml = 'Average returns from this score level: ';
'Average 1-year return from this score level: ' + fmtPct(ctx.avg_1yr_return); if (ctx.avg_30d_return != null) retHtml += '<span class="' + retClass(ctx.avg_30d_return) + '">30d: ' + fmtPct(ctx.avg_30d_return) + '</span> · ';
document.getElementById('ctxReturn').className = 'context-return ' + retClass(ctx.avg_1yr_return); if (ctx.avg_90d_return != null) retHtml += '<span class="' + retClass(ctx.avg_90d_return) + '">90d: ' + fmtPct(ctx.avg_90d_return) + '</span> · ';
if (ctx.avg_180d_return != null) retHtml += '<span class="' + retClass(ctx.avg_180d_return) + '">180d: ' + fmtPct(ctx.avg_180d_return) + '</span> · ';
retHtml += '<span class="' + retClass(ctx.avg_1yr_return) + '"><strong>1yr: ' + fmtPct(ctx.avg_1yr_return) + '</strong></span>';
document.getElementById('ctxReturn').innerHTML = retHtml;
} }
// Examples // Examples
const list = document.getElementById('ctxComparables'); const list = document.getElementById('ctxComparables');
@ -1259,6 +1262,8 @@ function renderContext() {
html += '<div class="comparable-item"><span>' + ex.date + ' — Score ' + ex.score + '' + fmtPrice(ex.price) + '</span>'; html += '<div class="comparable-item"><span>' + ex.date + ' — Score ' + ex.score + '' + fmtPrice(ex.price) + '</span>';
html += '<span>'; html += '<span>';
if (fr['30d'] != null) html += '<span class="' + retClass(fr['30d']) + '">30d: ' + fmtPct(fr['30d']) + '</span> '; if (fr['30d'] != null) html += '<span class="' + retClass(fr['30d']) + '">30d: ' + fmtPct(fr['30d']) + '</span> ';
if (fr['90d'] != null) html += '<span class="' + retClass(fr['90d']) + '">90d: ' + fmtPct(fr['90d']) + '</span> ';
if (fr['180d'] != null) html += '<span class="' + retClass(fr['180d']) + '">180d: ' + fmtPct(fr['180d']) + '</span> ';
if (fr['365d'] != null) html += '<span class="' + retClass(fr['365d']) + '">1yr: ' + fmtPct(fr['365d']) + '</span>'; if (fr['365d'] != null) html += '<span class="' + retClass(fr['365d']) + '">1yr: ' + fmtPct(fr['365d']) + '</span>';
html += '</span></div>'; html += '</span></div>';
} }
@ -1362,6 +1367,7 @@ function renderBracketTable() {
html += '<td>' + b.days + '</td>'; html += '<td>' + b.days + '</td>';
html += '<td class="' + retClass(b.avg_30d) + '">' + fmtPct(b.avg_30d) + '</td>'; html += '<td class="' + retClass(b.avg_30d) + '">' + fmtPct(b.avg_30d) + '</td>';
html += '<td class="' + retClass(b.avg_90d) + '">' + fmtPct(b.avg_90d) + '</td>'; html += '<td class="' + retClass(b.avg_90d) + '">' + fmtPct(b.avg_90d) + '</td>';
html += '<td class="' + retClass(b.avg_180d) + '">' + fmtPct(b.avg_180d) + '</td>';
html += '<td class="' + retClass(b.avg_365d) + '">' + fmtPct(b.avg_365d) + '</td>'; html += '<td class="' + retClass(b.avg_365d) + '">' + fmtPct(b.avg_365d) + '</td>';
html += '<td>' + (b.win_rate_365d != null ? b.win_rate_365d + '%' : '--') + '</td>'; html += '<td>' + (b.win_rate_365d != null ? b.win_rate_365d + '%' : '--') + '</td>';
html += '<td class="t-green">' + fmtPct(b.max_gain_365d) + '</td>'; html += '<td class="t-green">' + fmtPct(b.max_gain_365d) + '</td>';
@ -1390,6 +1396,7 @@ function renderSignalEvents() {
const fr = ev.forward_returns || {}; const fr = ev.forward_returns || {};
if (fr['30d'] != null) html += '<span class="' + retClass(fr['30d']) + '">30d: ' + fmtPct(fr['30d']) + '</span>'; if (fr['30d'] != null) html += '<span class="' + retClass(fr['30d']) + '">30d: ' + fmtPct(fr['30d']) + '</span>';
if (fr['90d'] != null) html += '<span class="' + retClass(fr['90d']) + '">90d: ' + fmtPct(fr['90d']) + '</span>'; if (fr['90d'] != null) html += '<span class="' + retClass(fr['90d']) + '">90d: ' + fmtPct(fr['90d']) + '</span>';
if (fr['180d'] != null) html += '<span class="' + retClass(fr['180d']) + '">180d: ' + fmtPct(fr['180d']) + '</span>';
if (fr['365d'] != null) html += '<span class="' + retClass(fr['365d']) + '">1yr: ' + fmtPct(fr['365d']) + '</span>'; if (fr['365d'] != null) html += '<span class="' + retClass(fr['365d']) + '">1yr: ' + fmtPct(fr['365d']) + '</span>';
if (ev.price_365d) html += '<span style="color:var(--text-dim)">Price 1yr: ' + fmtPrice(ev.price_365d) + '</span>'; if (ev.price_365d) html += '<span style="color:var(--text-dim)">Price 1yr: ' + fmtPrice(ev.price_365d) + '</span>';
html += '</div></div>'; html += '</div></div>';

View File

@ -2,3 +2,4 @@
{"timestamp": "2026-03-20T22:30:13.547149+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.910215736040605}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}} {"timestamp": "2026-03-20T22:30:13.547149+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.910215736040605}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
{"timestamp": "2026-03-20T22:46:34.952569+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.931630710659896}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}} {"timestamp": "2026-03-20T22:46:34.952569+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.931630710659896}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
{"timestamp": "2026-03-20T22:51:27.724327+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.94907994923858}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}} {"timestamp": "2026-03-20T22:51:27.724327+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.94907994923858}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
{"timestamp": "2026-03-20T23:07:48.303808+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.942734771573605}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

After

Width:  |  Height:  |  Size: 633 KiB