Component attribution: per-stage scorecard (CLI + /api/attribution + dashboard + board pulse)
The owner's modularity/monitoring ask: rate each pipeline component so what's earning (and what's cutting) is visible instead of inferred. Everything derives from the existing event stream — no pipeline changes. - research/attribution.py: compute_attribution(days) -> per-day per-stage metrics: DISCOVERY (symbols found + precision = fraction that ever go ready), SIGNALS (ready split EARLY-vs-LATE against the confirmation window — the only +EV ones), GATES (risk_rule histogram: what each gate cut), EXECUTION (submissions vs cancels = fill efficiency), OUTCOMES (realized P&L, win rate, per-close detail). CLI: python -m research.attribution. All queries filter the native timestamp column first (payload casts on the 676k-row events table time out). - api/main.py: GET /api/attribution?days=N. - dashboard.html: "Component Attribution" panel, lazy-loads on open, per-day table with gate-histogram hover. - momentum_cli board header: live pulse line — today's early/late signal split + top gate cuts (two tiny date-scoped queries). Verified live: CLI scorecard over 7 days, endpoint returns real ratings (e.g. 2026-07-02: discovery 45%, early 38%, fill 39%, win 50%, P&L +$29), board renders the pulse line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2239ccbe6
commit
6c36d04f7a
4 files changed
+222
-1
No files matched your search
@@ -391,6 +391,12 @@ class DashboardHandler(BaseHTTPRequestHandler):
|
||||
symbol = (q.get("symbol") or [""])[0]
|
||||
minutes = int((q.get("minutes") or ["60"])[0])
|
||||
self._send_json(self.state.bars(symbol, minutes))
|
||||
elif route == "/api/attribution":
|
||||
# per-component stage ratings (research/attribution.py); computed on
|
||||
# demand — a handful of indexed event queries, fine per click
|
||||
from research.attribution import compute_attribution
|
||||
days = int((self._query().get("days") or ["7"])[0])
|
||||
self._send_json(compute_attribution(days=days))
|
||||
elif route == "/api/health":
|
||||
self._send_json({"ok": True, "mode": self.state.execution_mode})
|
||||
else:
|
||||
|
||||
@@ -506,8 +506,46 @@
|
||||
<summary><h2>Order Lifecycle <span id="ol-count" class="count">0</span></h2></summary>
|
||||
<div id="order-lifecycle"></div>
|
||||
</details>
|
||||
|
||||
<details class="panel full" id="p-attrib">
|
||||
<summary><h2>Component Attribution <span class="hint">per-stage ratings: discovery → signals (early vs late) → gates → execution → P&L · loads on open</span></h2></summary>
|
||||
<div id="attrib"><span class="empty">Open to load the last 7 days.</span></div>
|
||||
</details>
|
||||
</main>
|
||||
<script>
|
||||
(function(){
|
||||
// Component Attribution — lazy-load on first open (it queries the event store)
|
||||
var ap = document.getElementById('p-attrib'), loadedAttrib = false;
|
||||
function esc(s){ return String(s == null ? '' : s).replace(/[&<>]/g, function(c){ return {'&':'&','<':'<','>':'>'}[c]; }); }
|
||||
function renderAttrib(a){
|
||||
var days = Object.keys(a.days || {}).sort().reverse();
|
||||
if (!days.length) { document.getElementById('attrib').innerHTML = '<span class="empty">No events in window.</span>'; return; }
|
||||
var h = '<table><tr><th>day</th><th>disc</th><th>ready</th><th>early%</th><th>gate cuts</th><th>subm</th><th>fill%</th><th>trades</th><th>win%</th><th>P&L</th></tr>';
|
||||
days.forEach(function(d){
|
||||
var st = a.days[d], c = st.counts || {}, r = st.rating || {}, oc = st.outcomes || {};
|
||||
var gates = Object.entries(st.gates || {}).sort(function(x, y){ return y[1] - x[1]; })
|
||||
.map(function(kv){ return esc(kv[0]) + '×' + kv[1]; }).join(', ');
|
||||
var pnl = (r.pnl == null) ? '-' : ('$' + (r.pnl >= 0 ? '+' : '') + r.pnl.toFixed(0));
|
||||
var cls = (r.pnl == null) ? '' : (r.pnl >= 0 ? ' style="color:#2f9e44"' : ' style="color:#e03131"');
|
||||
h += '<tr title="' + gates + '"><td>' + d + '</td><td>' + (c.symbol_discovered || 0) + '</td><td>' + (c.signal_ready || 0) +
|
||||
'</td><td>' + (r.signals_early_pct == null ? '-' : r.signals_early_pct + '%') +
|
||||
'</td><td>' + Object.values(st.gates || {}).reduce(function(s, v){ return s + v; }, 0) +
|
||||
'</td><td>' + (c.order_submitted || 0) +
|
||||
'</td><td>' + (r.fill_efficiency_pct == null ? '-' : r.fill_efficiency_pct + '%') +
|
||||
'</td><td>' + (oc.trades || 0) + '</td><td>' + (r.win_rate_pct == null ? '-' : r.win_rate_pct + '%') +
|
||||
'</td><td' + cls + '>' + pnl + '</td></tr>';
|
||||
});
|
||||
h += '</table><div class="hint" style="margin-top:6px">early% = ready signals inside the ' + a.confirm_minute + 'm confirmation window (the only +EV ones) · hover a row for its gate histogram</div>';
|
||||
document.getElementById('attrib').innerHTML = h;
|
||||
}
|
||||
if (ap) ap.addEventListener('toggle', function(){
|
||||
if (!ap.open || loadedAttrib) return;
|
||||
loadedAttrib = true;
|
||||
document.getElementById('attrib').innerHTML = '<span class="empty">Loading…</span>';
|
||||
fetch('/api/attribution?days=7').then(function(r){ return r.json(); }).then(renderAttrib)
|
||||
.catch(function(e){ document.getElementById('attrib').innerHTML = '<span class="empty">failed: ' + esc(e) + '</span>'; loadedAttrib = false; });
|
||||
});
|
||||
})();
|
||||
(function(){
|
||||
// Collapsible panels — remember each panel's open/closed state across refreshes,
|
||||
// and an expand/collapse-all toggle. Pure UI; the live app JS keys off the inner ids.
|
||||
|
||||
Reference in new issue
Block a user