← back to Ken
Ken v2 follow-the-winners signal: durable win-record source (ken_daily_pnl) + graceful rebuild state (TK-10166)
5735837e1cdbf4e90c81305990cabf5e120837d4 · 2026-08-03 08:21:49 -0700 · Steve Abrams
Files touched
A kalshi-dash/follow-the-winners.mjs
Diff
commit 5735837e1cdbf4e90c81305990cabf5e120837d4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 08:21:49 2026 -0700
Ken v2 follow-the-winners signal: durable win-record source (ken_daily_pnl) + graceful rebuild state (TK-10166)
---
kalshi-dash/follow-the-winners.mjs | 111 +++++++++++++++++++++++++++++++++++++
1 file changed, 111 insertions(+)
diff --git a/kalshi-dash/follow-the-winners.mjs b/kalshi-dash/follow-the-winners.mjs
new file mode 100644
index 0000000..b8cb81b
--- /dev/null
+++ b/kalshi-dash/follow-the-winners.mjs
@@ -0,0 +1,111 @@
+// follow-the-winners.mjs — Ken v2 signal (Steve, 2026-08-03, TK-10166)
+// READ-ONLY. "Bet what the WINNING faux portfolios are doing."
+//
+// Win-record source priority (churn-immune first):
+// 1) ken_daily_pnl — durable per-portfolio rollup (wins/losses/cumulative_pnl); survives
+// the raw-trade-table churn that a `pm2 restart ken` sim-replay causes.
+// 2) ken_portfolios — live counters, fallback while daily_pnl is still empty (rebuilding).
+// Direction source: ken_portfolio_trades open positions of the proven winners (live).
+//
+// Gate: a portfolio must clear WIN_GATE win-rate over >= MIN_RESOLVED resolved trades
+// before we ever follow it. Emits a ranked signal; places ZERO orders, writes nothing.
+import pg from 'pg';
+const ken = new pg.Pool({ connectionString: process.env.KEN_DATABASE_URL || 'postgresql://localhost:5432/ken' });
+
+const MIN_RESOLVED = Number(process.env.FTW_MIN_RESOLVED || 20);
+const WIN_GATE = Number(process.env.FTW_WIN_GATE || 0.60);
+const TOP_K = Number(process.env.FTW_TOP_K || 8);
+
+const num = v => Number(v || 0);
+
+// --- 1) load the durable win-record, preferring ken_daily_pnl ---
+async function loadWinRecord() {
+ const dp = await ken.query(`
+ SELECT p.id, p.name, p.strategy,
+ COALESCE(SUM(d.wins),0) AS w,
+ COALESCE(SUM(d.losses),0) AS l,
+ COALESCE(MAX(d.cumulative_pnl_cents),0) AS net_c
+ FROM ken_portfolios p LEFT JOIN ken_daily_pnl d ON d.portfolio_id = p.id
+ GROUP BY p.id, p.name, p.strategy`);
+ const anyDaily = dp.rows.some(r => num(r.w) + num(r.l) > 0);
+ if (anyDaily) return { src: 'ken_daily_pnl (durable)', rows: dp.rows };
+
+ // fallback: live counters (used while the durable rollup is still empty/rebuilding)
+ const lp = await ken.query(`
+ SELECT id, name, strategy, trades_won AS w, trades_lost AS l,
+ (total_returned_cents - total_invested_cents) AS net_c
+ FROM ken_portfolios`);
+ return { src: 'ken_portfolios (live fallback)', rows: lp.rows };
+}
+
+const { src, rows } = await loadWinRecord();
+
+const scored = rows.map(p => {
+ const resolved = num(p.w) + num(p.l);
+ const winPct = resolved ? num(p.w) / resolved : 0;
+ const netC = num(p.net_c);
+ const score = (winPct - 0.5) * Math.log10(resolved + 1) * (netC >= 0 ? 1 : 0.4);
+ return { ...p, resolved, winPct, netC, score };
+});
+
+const winners = scored
+ .filter(p => p.resolved >= MIN_RESOLVED && p.winPct >= WIN_GATE)
+ .sort((a, b) => b.score - a.score)
+ .slice(0, TOP_K);
+
+console.log(`\nwin-record source: ${src}`);
+const totalResolved = scored.reduce((s, p) => s + p.resolved, 0);
+
+if (totalResolved === 0) {
+ console.log(`\n⏳ SIM REBUILDING — no resolved paper trades on record yet (a restart replays the`);
+ console.log(` simulation from scratch). No winners to follow. Re-run once the paper sim has`);
+ console.log(` accumulated resolved trades; this engine will light up automatically. Do NOT bet live.`);
+ await ken.end(); process.exit(0);
+}
+
+console.log(`=== PROVEN WINNERS (win% >= ${(WIN_GATE*100)|0}, resolved >= ${MIN_RESOLVED}) ===`);
+if (!winners.length) {
+ console.log(` (none clear the gate yet — ${totalResolved} resolved trades on record but no portfolio`);
+ console.log(` meets the bar. Do NOT bet live.)`);
+ await ken.end(); process.exit(0);
+}
+for (const p of winners) {
+ console.log(` ${p.name.padEnd(12)} ${String(Math.round(p.winPct*100)).padStart(3)}% ` +
+ `${p.w}W/${p.l}L net $${(p.netC/100).toFixed(0).padStart(5)} score ${p.score.toFixed(3)}`);
+}
+
+// --- 2) winners' current open positions → weighted consensus per market ---
+const winnerIds = winners.map(p => p.id);
+const wById = Object.fromEntries(winners.map(p => [p.id, p]));
+const { rows: open } = await ken.query(`
+ SELECT portfolio_id, market_id, market_title, direction, entry_price_cents
+ FROM ken_portfolio_trades WHERE status='open' AND portfolio_id = ANY($1)`, [winnerIds]);
+
+const mkt = {};
+for (const t of open) {
+ const m = mkt[t.market_id] ||= { title: t.market_title, dir: {}, holders: new Set(), prices: [] };
+ const w = Math.max(0.01, wById[t.portfolio_id]?.score || 0.01);
+ m.dir[t.direction] = (m.dir[t.direction] || 0) + w;
+ m.holders.add(t.portfolio_id);
+ m.prices.push(num(t.entry_price_cents));
+}
+const signals = Object.entries(mkt).map(([id, m]) => {
+ const entries = Object.entries(m.dir).sort((a, b) => b[1] - a[1]);
+ const [topDir, topW] = entries[0];
+ const totalW = entries.reduce((s, [, w]) => s + w, 0);
+ return { title: m.title, dir: topDir, agreement: totalW ? topW / totalW : 0,
+ holders: m.holders.size, avgPrice: Math.round(m.prices.reduce((s,p)=>s+p,0)/m.prices.length),
+ conviction: topW };
+}).sort((a, b) => (b.conviction * b.agreement) - (a.conviction * a.agreement));
+
+console.log(`\n=== LIVE-BET SIGNAL — what the winners hold open RIGHT NOW (ranked) ===`);
+if (!signals.length) { console.log(' (winners hold no open positions this moment)'); }
+else {
+ console.log(` ${'DIR'.padEnd(8)} ${'agree'.padStart(5)} ${'#win'.padStart(4)} ${'~price'.padStart(6)} market`);
+ for (const s of signals.slice(0, 15))
+ console.log(` ${s.dir.padEnd(8)} ${String(Math.round(s.agreement*100)).padStart(4)}% ` +
+ `${String(s.holders).padStart(4)} ${(s.avgPrice+'¢').padStart(6)} ${s.title.slice(0,52)}`);
+ console.log(`\n ${signals.length} markets carry a winner signal. A live re-arm would bet ONLY top`);
+ console.log(` unanimous-high-conviction rows, and ONLY after the sample/edge gate is committed.`);
+}
+await ken.end();
← 5e77ebc auto-save: 2026-08-03T07:51:47 (1 files) — kalshi-dash/serve
·
back to Ken
·
auto-save: 2026-08-03T08:22:05 (1 files) — kalshi-dash/.giti f60f68f →