Skip to content

Commit 7b7c1de

Browse files
committed
feat: Polymarket daily sports play + dailyPlayPicks scoreboard + Coinbase scaffold + CodeRabbit fixes
Implements the plan in /root/.claude/plans/sparkling-churning-dusk.md. Targets the user's stated goal of arming live trading on both prediction platforms while tracking the bot's daily sports-pick win/loss record. Phase 0 — CodeRabbit correctness fixes (BLOCKING) - kalshiSignals.calculateSignalPerformance: lazy-init each SignalType so newly added types (linguistic_tell, wikipedia_edit) no longer crash on the non-null assertion. Regression test added. - wikipediaEditWatcher: breaker tightened to 30s/30s (CLAUDE.md profile), cursor advance is now monotonic (no rollback on overlapping polls), matchSignalsToMarkets gates by category when both sides expose it (kills "musk" → music false positives). New regression tests. - grokPersonas: getGrokPersona preserves caller's category; ${RULES_BLOCK} placeholder removed from cached mandate so injectVerbatimRulesBlock remains the only splice path. - linguistic-tells test: cap assertion tightened to 0.92. Phase 1 — Polymarket Daily Sports Play (new) - server/_core/polymarketDailySportsPlay.ts mirrors dailySportsPlay.ts. Reuses operator's existing tradingPreferences gates so a single liveTradingEnabled toggle arms BOTH Kalshi and Polymarket daily plays. Sized 2.5% of bankroll (Kalshi capital proxy until USDC.e poll lands). Filters Polymarket sports markets via category literal OR classifier fallback (Polymarket's category strings are messier than Kalshi's). AI-reviewed via reviewPolymarketSignalsWithTrader; full risk-gate stack inline (existing position, drawdown breaker, portfolio + per- category exposure caps). Per-user mutex wraps placement. Phase 2 — dailyPlayPicks lifecycle table - New table tracks each daily-play pick from entry to resolution across BOTH platforms. Unique index on (userId, platform, playType, playDate) makes inserts idempotent at the DB level. - server/db.daily-play-picks.ts: insertDailyPlayPick (ON CONFLICT DO NOTHING), linkPositionToPick, closeDailyPlayPickByPosition, closeDailyPlayPickByMarketFallback, getDailyPlayPicks, getTodayDailyPlayPicks, rollupScoreboard. Phase 3 — Scheduler wiring - maybeRunPolymarketDailySportsPlay added next to the Kalshi version, same 5-min tick + per-user dedup (in-process Set + audit-log). Phase 4 — Close-hook side effects - closeKalshiPosition (Kalshi live + paper) closes matching daily picks - polymarketPositionSync drift-close updates picks with estimated exit price (snapshot mark) — flagged in audit - paperTrading polymarket close closes matching paper picks - upsertPolymarketPosition closes picks on transition-to-closed (defensive — drift-close is the primary detector) Phase 5 — tRPC daily.* namespace - daily.getDailyPlayScoreboard returns today's picks + 30-day rollup + lifetime win-rate by platform (Kalshi vs Polymarket). - daily.getTodayDailyPlay returns today's row(s) only. Phase 6 — Daily Pick Scoreboard widget - client/src/components/widgets/DailyPlayScoreboard.tsx: 3-row compact card (today's picks + last 30d split + lifetime tally) with per-day table in full mode. Mounted on Dashboard.tsx after the equity chart. Phase 7 — env vars - ENABLE_POLYMARKET_DAILY_SPORTS_PLAY (default true) - POLYMARKET_DAILY_SPORTS_PLAY_HOUR_UTC (default 14) - POLYMARKET_DAILY_SPORTS_PLAY_PCT_OF_CAPITAL (default 0.025) - ENABLE_COINBASE_LIVE (default false), COINBASE_SANDBOX_MODE (true) Phase 8/9 — Operator pre-arm checklist + verification doc - docs/POLYMARKET_DAILY_PLAY_VERIFICATION.md walks through every Railway env var, per-user trading pref, funding step, self-test check, and an 11-step paper→live smoke test sequence. Phase 10 — Coinbase scaffolding (architectural ONLY) - drizzle/migrations/0014_coinbase_scaffold.sql + schema exports for coinbaseCredentials, coinbaseOrders, coinbasePositions, coinbaseCapital - server/_core/coinbaseAuth.ts (format validation only, balance fetch stubbed), server/_core/coinbaseExecution.ts (THROWS on every call — inert until Phase 10 build-out lands) - server/db.coinbase-credentials.ts mirrors Polymarket creds (AES-GCM) - coinbase.* tRPC namespace (connect/status/disconnect) - CoinbaseConnectPanel on Connect.tsx — operator can connect now to be ready when instruments + signal sources are decided Verification: 1009 tests passing (up from 1002), typecheck clean. https://claude.ai/code/session_01To82bdNQsUtCeAyGJFLQc3
1 parent 090cd34 commit 7b7c1de

28 files changed

Lines changed: 2740 additions & 25 deletions
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import React from "react";
2+
import { Trophy, TrendingDown, Clock, MinusCircle } from "lucide-react";
3+
import { trpc } from "@/lib/trpc";
4+
import { cn } from "@/lib/utils";
5+
6+
/**
7+
* Daily-pick Scoreboard widget.
8+
*
9+
* Renders the bot's win/loss record on its daily sports + moonshot picks
10+
* across both Kalshi and Polymarket. Two modes:
11+
*
12+
* compact (Dashboard.tsx): 3-row card — today's picks, last 30d split,
13+
* lifetime tally.
14+
* full (Performance.tsx): compact + per-platform breakdown table.
15+
*/
16+
interface DailyPlayScoreboardProps {
17+
compact?: boolean;
18+
className?: string;
19+
}
20+
21+
interface PlatformRollup {
22+
wins: number;
23+
losses: number;
24+
pending: number;
25+
picks: number;
26+
totalStaked: number;
27+
totalPnl: number;
28+
}
29+
30+
function formatPnl(v: number): string {
31+
const sign = v > 0 ? "+" : v < 0 ? "−" : "";
32+
return `${sign}$${Math.abs(v).toFixed(2)}`;
33+
}
34+
35+
function formatPct(v: number): string {
36+
return `${(v * 100).toFixed(1)}%`;
37+
}
38+
39+
function StatusBadge({ status }: { status: string }) {
40+
const map: Record<string, { label: string; cls: string; Icon: typeof Trophy }> = {
41+
pending: { label: "Open", cls: "bg-blue-500/20 text-blue-300", Icon: Clock },
42+
won: { label: "Won", cls: "bg-green-500/20 text-green-300", Icon: Trophy },
43+
lost: { label: "Lost", cls: "bg-red-500/20 text-red-300", Icon: TrendingDown },
44+
closed_breakeven: {
45+
label: "Even",
46+
cls: "bg-zinc-500/20 text-zinc-300",
47+
Icon: MinusCircle,
48+
},
49+
partial: { label: "Partial", cls: "bg-amber-500/20 text-amber-300", Icon: Clock },
50+
voided: { label: "Voided", cls: "bg-zinc-700/40 text-zinc-400", Icon: MinusCircle },
51+
};
52+
const e = map[status] ?? map.pending;
53+
const Icon = e.Icon;
54+
return (
55+
<span
56+
className={cn(
57+
"inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium",
58+
e.cls,
59+
)}
60+
>
61+
<Icon size={12} />
62+
{e.label}
63+
</span>
64+
);
65+
}
66+
67+
interface ChipPick {
68+
marketId: string;
69+
side: string;
70+
stakeUsd: number;
71+
status: string;
72+
confidence: number | null;
73+
realizedPnl?: number | null;
74+
}
75+
76+
function PlatformChip({
77+
label,
78+
pick,
79+
}: {
80+
label: string;
81+
pick: ChipPick | null;
82+
}) {
83+
if (!pick) {
84+
return (
85+
<div className="flex flex-col gap-1 rounded-md border border-white/5 bg-white/[0.02] px-3 py-2">
86+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">{label}</div>
87+
<div className="text-sm text-muted-foreground italic">No play yet today</div>
88+
</div>
89+
);
90+
}
91+
const truncated = pick.marketId.slice(0, 40);
92+
return (
93+
<div className="flex flex-col gap-1 rounded-md border border-white/5 bg-white/[0.02] px-3 py-2">
94+
<div className="flex items-center justify-between">
95+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">{label}</div>
96+
<StatusBadge status={pick.status} />
97+
</div>
98+
<div className="text-sm font-medium" title={pick.marketId}>
99+
{truncated}
100+
</div>
101+
<div className="text-xs text-muted-foreground">
102+
{pick.side.toUpperCase()} · ${pick.stakeUsd.toFixed(2)}
103+
{pick.confidence != null && ` · ${formatPct(pick.confidence)}`}
104+
{pick.realizedPnl != null && pick.status !== "pending" && (
105+
<span className={pick.realizedPnl >= 0 ? "text-green-300" : "text-red-300"}>
106+
{" · "}
107+
{formatPnl(pick.realizedPnl)}
108+
</span>
109+
)}
110+
</div>
111+
</div>
112+
);
113+
}
114+
115+
function RollupCell({ label, rollup }: { label: string; rollup: PlatformRollup }) {
116+
return (
117+
<div className="flex flex-col items-start">
118+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">{label}</div>
119+
<div className="text-sm font-medium">
120+
<span className="text-green-300">{rollup.wins}W</span>
121+
<span className="text-muted-foreground"> · </span>
122+
<span className="text-red-300">{rollup.losses}L</span>
123+
{rollup.pending > 0 && (
124+
<>
125+
<span className="text-muted-foreground"> · </span>
126+
<span className="text-blue-300">{rollup.pending}P</span>
127+
</>
128+
)}
129+
</div>
130+
<div
131+
className={cn(
132+
"text-xs",
133+
rollup.totalPnl > 0
134+
? "text-green-300"
135+
: rollup.totalPnl < 0
136+
? "text-red-300"
137+
: "text-muted-foreground",
138+
)}
139+
>
140+
{formatPnl(rollup.totalPnl)}
141+
</div>
142+
</div>
143+
);
144+
}
145+
146+
export function DailyPlayScoreboard({ compact = false, className }: DailyPlayScoreboardProps) {
147+
const scoreboardQuery = trpc.daily.getDailyPlayScoreboard.useQuery({
148+
platform: "both",
149+
daysBack: 30,
150+
});
151+
152+
if (scoreboardQuery.isLoading || !scoreboardQuery.data) {
153+
return (
154+
<div className={cn("data-card", className)}>
155+
<div className="h-4 w-32 animate-shimmer rounded-md bg-white/5 mb-3" />
156+
<div className="h-12 animate-shimmer rounded-md bg-white/5 mb-3" />
157+
<div className="h-4 w-24 animate-shimmer rounded-md bg-white/5" />
158+
</div>
159+
);
160+
}
161+
162+
const { today, days, lifetime } = scoreboardQuery.data;
163+
// Compute the 30-day combined rollup from days[]
164+
const last30 = days.reduce<PlatformRollup>(
165+
(acc, d) => ({
166+
wins: acc.wins + d.combined.wins,
167+
losses: acc.losses + d.combined.losses,
168+
pending: acc.pending + d.combined.pending,
169+
picks: acc.picks + d.combined.picks,
170+
totalStaked: acc.totalStaked + d.combined.totalStaked,
171+
totalPnl: acc.totalPnl + d.combined.totalPnl,
172+
}),
173+
{ wins: 0, losses: 0, pending: 0, picks: 0, totalStaked: 0, totalPnl: 0 },
174+
);
175+
const last30Kalshi = days.reduce<PlatformRollup>(
176+
(acc, d) => ({
177+
wins: acc.wins + d.kalshi.wins,
178+
losses: acc.losses + d.kalshi.losses,
179+
pending: acc.pending + d.kalshi.pending,
180+
picks: acc.picks + d.kalshi.picks,
181+
totalStaked: acc.totalStaked + d.kalshi.totalStaked,
182+
totalPnl: acc.totalPnl + d.kalshi.totalPnl,
183+
}),
184+
{ wins: 0, losses: 0, pending: 0, picks: 0, totalStaked: 0, totalPnl: 0 },
185+
);
186+
const last30Poly = days.reduce<PlatformRollup>(
187+
(acc, d) => ({
188+
wins: acc.wins + d.polymarket.wins,
189+
losses: acc.losses + d.polymarket.losses,
190+
pending: acc.pending + d.polymarket.pending,
191+
picks: acc.picks + d.polymarket.picks,
192+
totalStaked: acc.totalStaked + d.polymarket.totalStaked,
193+
totalPnl: acc.totalPnl + d.polymarket.totalPnl,
194+
}),
195+
{ wins: 0, losses: 0, pending: 0, picks: 0, totalStaked: 0, totalPnl: 0 },
196+
);
197+
198+
return (
199+
<div className={cn("data-card flex flex-col gap-4", className)}>
200+
<div className="flex items-center justify-between">
201+
<div className="flex items-center gap-2">
202+
<Trophy size={16} className="text-amber-400" />
203+
<h3 className="text-sm font-semibold">Daily Pick Scoreboard</h3>
204+
</div>
205+
<div className="text-xs text-muted-foreground">
206+
Bot's daily sports/moonshot picks
207+
</div>
208+
</div>
209+
210+
{/* Row 1: today's picks */}
211+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
212+
<PlatformChip label="Kalshi (today)" pick={today.kalshi} />
213+
<PlatformChip label="Polymarket (today)" pick={today.polymarket} />
214+
</div>
215+
216+
{/* Row 2: last 30d split */}
217+
<div className="grid grid-cols-3 gap-2 rounded-md border border-white/5 bg-white/[0.02] px-3 py-2">
218+
<RollupCell label="Last 30d · Kalshi" rollup={last30Kalshi} />
219+
<RollupCell label="Last 30d · Polymarket" rollup={last30Poly} />
220+
<RollupCell label="Last 30d · Combined" rollup={last30} />
221+
</div>
222+
223+
{/* Row 3: lifetime */}
224+
<div className="flex items-baseline justify-between rounded-md border border-white/5 bg-white/[0.02] px-3 py-2">
225+
<div>
226+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
227+
Lifetime
228+
</div>
229+
<div className="text-sm">
230+
<span className="font-medium">{formatPct(lifetime.winRate)}</span>
231+
<span className="text-muted-foreground">
232+
{" "}
233+
win rate · {lifetime.totalPicks} picks
234+
</span>
235+
</div>
236+
</div>
237+
<div
238+
className={cn(
239+
"text-base font-semibold",
240+
lifetime.totalPnl > 0
241+
? "text-green-300"
242+
: lifetime.totalPnl < 0
243+
? "text-red-300"
244+
: "text-muted-foreground",
245+
)}
246+
>
247+
{formatPnl(lifetime.totalPnl)}
248+
</div>
249+
</div>
250+
251+
{/* Full mode: per-day table */}
252+
{!compact && days.length > 0 && (
253+
<div className="mt-2 overflow-x-auto">
254+
<table className="w-full text-xs">
255+
<thead>
256+
<tr className="border-b border-white/5 text-left text-muted-foreground">
257+
<th className="py-2 pr-2">Date</th>
258+
<th className="py-2 pr-2">Kalshi (W/L/P)</th>
259+
<th className="py-2 pr-2">Polymarket (W/L/P)</th>
260+
<th className="py-2 pr-2 text-right">Daily PnL</th>
261+
</tr>
262+
</thead>
263+
<tbody>
264+
{days.map((d) => (
265+
<tr key={d.date} className="border-b border-white/5">
266+
<td className="py-2 pr-2 font-mono">{d.date}</td>
267+
<td className="py-2 pr-2">
268+
{d.kalshi.wins}/{d.kalshi.losses}/{d.kalshi.pending}
269+
</td>
270+
<td className="py-2 pr-2">
271+
{d.polymarket.wins}/{d.polymarket.losses}/{d.polymarket.pending}
272+
</td>
273+
<td
274+
className={cn(
275+
"py-2 pr-2 text-right",
276+
d.combined.totalPnl > 0
277+
? "text-green-300"
278+
: d.combined.totalPnl < 0
279+
? "text-red-300"
280+
: "text-muted-foreground",
281+
)}
282+
>
283+
{formatPnl(d.combined.totalPnl)}
284+
</td>
285+
</tr>
286+
))}
287+
</tbody>
288+
</table>
289+
</div>
290+
)}
291+
</div>
292+
);
293+
}
294+
295+
export default DailyPlayScoreboard;

0 commit comments

Comments
 (0)