[object Object]

← 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

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 →