Polymarket Quant Trader
Professional-grade Polymarket prediction market trading system. Includes Kelly Criterion position sizing, EV calculator, Bayesian probability updater, cross-...
Description
name: polymarket-quant-trader description: "Professional-grade Polymarket prediction market trading system. Includes Kelly Criterion position sizing, EV calculator, Bayesian probability updater, cross-platform arbitrage detector (Polymarket vs 1WIN), and autoresearch loop that self-improves strategy overnight via Brier score optimisation. Use when: user wants to trade prediction markets, find arbitrage opportunities, build a trading bot, or improve prediction accuracy. Triggers: polymarket, prediction markets, kelly criterion, EV trading, arb detector, brier score, prediction market bot, market making, quant trading, sports betting math, cross-platform arbitrage." version: 1.0.0
Polymarket Quant Trader
A professional quant trading system for Polymarket prediction markets, built and battle-tested in production. Three alpha streams. One integrated system.
Overview
This skill gives you a complete quantitative trading system for Polymarket with three independent alpha streams:
- EV-Based Signal Trading — Kelly Criterion position sizing + Bayesian probability updating. Find edges, size them correctly, update beliefs as evidence arrives.
- Self-Improving Strategy (Autoresearch Loop) — An autonomous hill-climbing optimizer that tunes your strategy parameters overnight using Brier score as the objective function. Wake up to a better strategy.
- Cross-Platform Arbitrage (PM x 1WIN) — Detect spread discrepancies between Polymarket and 1WIN bookmaker. Fuzzy title matching, confidence tiering, Kelly-sized positions.
Each stream works independently or together. The system ships with TypeScript source, npm scripts for every workflow, and a backtester to validate before going live.
Current production performance: Brier score 0.18 (meaningful edge territory — baseline random is 0.25, professional is sub-0.12).
Stream 1: EV-Based Signal Trading
How It Works
The core loop: estimate a probability, compare it to the market price, calculate expected value, size the position with Kelly Criterion, and update beliefs as new evidence arrives.
Kelly Criterion Position Sizing
Kelly answers: "Given my edge, what fraction of my bankroll should I bet?"
The formula:
f* = (p * b - q) / b
where:
f* = optimal fraction of bankroll to wager
p = probability of winning (your estimate, 0-1)
b = net odds multiplier (payout per $1 risked)
q = 1 - p (probability of losing)
In prediction markets, odds derive from the market price:
b = (1 - marketYesPrice) / marketYesPrice
If YES trades at $0.40, then b = 0.60/0.40 = 1.5 (you risk $0.40 to win $0.60).
Implementation:
// kelly-criterion.ts
export function kelly(p: number, b: number, q?: number): number {
const qVal = q ?? 1 - p;
return (p * b - qVal) / b;
}
export function quarterKelly(p: number, b: number): number {
return 0.25 * kelly(p, b);
}
export function kellySizing(
bankroll: number,
p: number,
b: number,
mode: 'full' | 'half' | 'quarter' = 'quarter'
): number {
const fraction = mode === 'full' ? kelly(p, b)
: mode === 'half' ? 0.5 * kelly(p, b)
: quarterKelly(p, b);
return Math.max(0, bankroll * Math.min(fraction, 0.15));
}
Why quarter Kelly? Full Kelly maximizes long-run growth rate but produces brutal drawdowns (50%+ swings). Quarter Kelly captures ~75% of the growth rate with dramatically lower variance. Every serious quant fund uses fractional Kelly.
EV Calculator
Expected value quantifies your edge per dollar risked:
// ev-calculator.ts
export interface MarketEV {
marketId: string;
ourP: number; // Your estimated probability
marketP: number; // Market-implied probability (= YES price)
b: number; // Net odds: (1 - marketP) / marketP
ev: number; // Expected value per dollar risked
edgePct: number; // Edge as percentage of market price
kellyFraction: number; // Quarter Kelly optimal fraction
recommend: boolean; // Worth trading? (ev > 0 && edgePct >= 2%)
}
export function calcEV(ourProbability: number, marketYesPrice: number) {
const b = (1 - marketYesPrice) / marketYesPrice;
const ev = ourProbability * b - (1 - ourProbability);
const edgePct = (ev / marketYesPrice) * 100;
return { ev, edgePct, b };
}
export function scoreMarket(market: any, ourP: number): MarketEV {
const { ev, edgePct, b } = calcEV(ourP, market.yesPrice);
const kellyFraction = quarterKelly(ourP, b);
return {
marketId: market.id,
ourP,
marketP: market.yesPrice,
b,
ev,
edgePct,
kellyFraction,
recommend: ev > 0 && edgePct >= 2,
};
}
export function rankByEV(markets: MarketEV[]): MarketEV[] {
return [...markets].sort((a, b) => b.ev - a.ev);
}
Reading the output: An edgePct of 5% means your model thinks the market is mispriced by 5%. The recommend flag fires when EV is positive AND edge exceeds 2% (below that, transaction costs eat your edge).
Bayesian Probability Updater
Update your probability estimates as new evidence arrives:
// bayesian-updater.ts
export interface BayesianState {
marketId: string;
priorP: number;
currentP: number;
evidence: Evidence[];
lastUpdated: Date;
}
export interface Evidence {
description: string;
likelihoodRatio: number; // > 1 supports YES, < 1 supports NO
timestamp: Date;
}
export function bayesUpdate(prior: number, likelihoodRatio: number): number {
const posterior = (prior * likelihoodRatio) /
(prior * likelihoodRatio + (1 - prior));
return Math.max(0.001, Math.min(0.999, posterior));
}
export function addEvidence(
state: BayesianState,
evidence: Evidence
): BayesianState {
const newP = bayesUpdate(state.currentP, evidence.likelihoodRatio);
return {
...state,
currentP: newP,
evidence: [...state.evidence, evidence],
lastUpdated: evidence.timestamp,
};
}
export function getRecommendation(
state: BayesianState,
marketPrice: number
): { action: 'buy' | 'sell' | 'hold'; confidence: number; reason: string } {
const diff = state.currentP - marketPrice;
if (Math.abs(diff) < 0.02) return { action: 'hold', confidence: 0, reason: 'Within noise' };
if (diff > 0) return { action: 'buy', confidence: diff, reason: `Model ${(diff*100).toFixed(1)}% above market` };
return { action: 'sell', confidence: -diff, reason: `Model ${(-diff*100).toFixed(1)}% below market` };
}
Likelihood ratios: A ratio of 2.0 means "this evidence is twice as likely if YES is true." A ratio of 0.5 means "this evidence is twice as likely if NO is true." The Bayesian updater chains multiple evidence items — each update feeds the next as a new prior.
Market Scorer (Composite Ranking)
Combines all signals into a single score for market selection:
// market-scorer.ts — Weighted scoring model
// EV Score: 40% weight — edge percentage
// Kelly Fraction: 30% weight — optimal sizing (higher = more confident)
// Expiry Window: 20% weight — sweet spot 6-72 hours
// Volume Score: 10% weight — log-normalized liquidity
Markets scoring highest get traded first. The expiry window filter avoids two failure modes: too-short expiry (can't exit if wrong) and too-long expiry (capital locked up, edge decays).
Stream 2: Self-Improving Strategy (Autoresearch Loop)
The Brier Score Metric
Brier score measures prediction calibration — how close your probability estimates are to actual outcomes:
brierScore = mean((predictedProbability - actualOutcome)^2)
where actualOutcome = 1 if resolved YES, 0 if resolved NO
Interpretation scale:
| Score | Level | Meaning |
|---|---|---|
| 0.25 | Random | Coin-flip predictions |
| 0.22 | Weak edge | Slightly better than random |
| 0.18 | Meaningful edge | Consistent alpha |
| 0.12 | Professional | Elite forecaster territory |
| < 0.10 | Superforecaster | Top 1% calibration |
Lower is better. The system tracks Brier score as the primary optimization objective.
Strategy Configuration
The strategy is defined by tunable parameters:
// research/strategy.ts
export interface StrategyConfig {
minVolume: number; // Minimum market volume ($)
minEdgePct: number; // Minimum edge to trade (%)
kellyMode: "full"|"half"|"quarter";
maxKellyFraction: number; // Cap on position size
expiryMinHours: number; // Earliest expiry to consider
expiryMaxHours: number; // Latest expiry to consider
}
export const DEFAULT_CONFIG: StrategyConfig = {
minVolume: 10000,
minEdgePct: 3.0,
kellyMode: "quarter",
maxKellyFraction: 0.15,
expiryMinHours: 6,
expiryMaxHours: 72,
};
// Category-specific base rates (priors for YES resolution)
const CATEGORY_PRIORS = {
sports: 0.48,
crypto: 0.45,
politics: 0.50,
tech: 0.50,
weather: 0.45,
misc: 0.50,
};
Prediction logic:
const PRIOR_WEIGHT = 0.15; // How much to weight the category prior
ourProbability = marketYesPrice * (1 - PRIOR_WEIGHT) + prior * PRIOR_WEIGHT;
edgePct = Math.abs(ourProbability - marketYesPrice) * 100;
// Decision:
if (edgePct < minEdgePct) → skip
else if (ourP > marketYesPrice) → buy_yes
else → buy_no
Running the Autoresearch Loop
# One-shot evaluation against resolved markets
npm run research:eval
# Manual iteration (5 rounds, stops on plateau)
npm run research
# Autonomous hill-climbing optimizer (run overnight)
npm run research:auto
How research:auto works:
- Loads current strategy config
- Tries a parameter mutation (e.g., minEdgePct 3.0 → 2.5)
- Evaluates against all resolved markets → gets Brier score
- If Brier improved → keep change, bump version, save checkpoint
- If Brier worsened → revert to backup
- Move to next untried mutation
- Stop when all mutations exhausted without improvement
Parameter search space:
// auto-improve.ts explores:
minEdgePct: [1.0, 1.5, 2.5, 3.0]
PRIOR_WEIGHT: [0.05, 0.10, 0.20, 0.25, 0.30]
maxKellyFraction: [0.08, 0.10, 0.12, 0.20]
minVolume: [5000, 15000, 20000]
expiryMaxHours: [48, 96]
expiryMinHours: [4, 8, 12]
kellyMode: quarter ↔ half
categoryPriors: dynamic adjustments per category
Reading the Iteration Log
Results are logged to research/program.md:
## Iteration 4 (Auto 4/8)
- Changed: minEdgePct 2 → 3
- Brier: 0.1804 (prev: 0.1814)
- Improvement: +0.0010
- Status: ✅ KEPT — new best
- Version: 1.0.1
Breaking a Plateau
When auto-improve exhausts its search space without improvement:
- Inject a hypothesis manually — Edit
research/strategy.tswith a theory (e.g., "crypto markets are less efficient after 10pm UTC") and runnpm run research:eval - Add new data — More resolved markets = more signal for the optimizer
- Change the objective — Weight Brier + Sharpe ratio instead of pure Brier
- Try category-specific strategies — Separate configs for sports vs politics vs crypto
Stream 3: Cross-Platform Arbitrage (PM x 1WIN)
How Spread Arbitrage Works
When two platforms price the same event differently, you can profit from the spread:
Polymarket YES price: $0.40 (implied 40%)
1WIN decimal odds: 2.80 (implied 1/2.80 = 35.7%)
Spread = |40% - 35.7%| = 4.3%
If Polymarket says 40% and 1WIN says 35.7%, Polymarket is pricing YES higher. The direction depends on which platform you think is wrong — or you can bet both sides if the spread exceeds the combined vig.
Spread Calculator
// spread-calculator.ts
export function calcSpread(polyProb: number, onewinDecimalOdds: number) {
const onewinProb = 1 / onewinDecimalOdds;
const spread = Math.abs(polyProb - onewinProb);
const spreadPct = spread * 100;
const direction = polyProb < onewinProb ? "buy_poly_yes" : "buy_poly_no";
return { onewinProb, spread, spreadPct, direction };
}
export function calcExpectedProfit(polyProb: number, onewinProb: number): number {
const edge = Math.abs(polyProb - onewinProb);
const ONEWIN_VIG = 0.02;
return Math.max((edge - ONEWIN_VIG) * 100, 0);
}
export function calcKellyFraction(polyProb: number, onewinProb: number): number {
const edge = Math.abs(polyProb - onewinProb);
const fraction = edge / (1 - Math.min(polyProb, onewinProb));
return Math.min(fraction, 0.10); // Hard cap at 10%
}
export function getConfidence(spreadPct: number): "HIGH"|"MEDIUM"|"LOW"|null {
if (spreadPct > 5) return "HIGH";
if (spreadPct >= 3) return "MEDIUM";
if (spreadPct >= 1) return "LOW";
return null; // Skip
}
Running the Arb Scanner
npm run arb:scan
What it does:
- Fetches active Polymarket markets (sports/crypto, expires within 48h, volume > 0)
- Fetches 1WIN events via API (with fallback to CLOB proxy if geo-blocked)
- Fuzzy-matches event titles across platforms (Dice coefficient, 0.4 threshold)
- Calculates spreads for all matches
- Tiers by confidence (HIGH/MEDIUM/LOW)
- Returns top 20 opportunities sorted by spread percentage
Reading the output:
🟢 HIGH CONFIDENCE | Spread: 6.2%
PM: "Will Bitcoin hit $100k by March?" @ $0.35
1WIN: Same event @ 2.50 odds (40.0%)
Direction: buy_poly_yes
Kelly: 4.8% of bankroll
Expected profit: 4.2%
🟡 MEDIUM CONFIDENCE | Spread: 3.8%
PM: "Lakers vs Celtics Game 5 winner" @ $0.55
1WIN: Same event @ 1.72 odds (58.1%)
Direction: buy_poly_no
Kelly: 2.1% of bankroll
Expected profit: 1.8%
Title Matching
The fuzzy matcher handles cross-platform naming differences:
// title-matcher.ts
// Normalizes: lowercase, remove punctuation, strip stop words
// Stop words: vs, v, the, will, who, win, to, in, at, on, a, an,
// of, for, and, or, be, is, are, was, match, game, fight, bout
// Dice coefficient: 2 * |intersection| / (|a| + |b|)
// Threshold: 0.4 minimum for a match
Continuous Monitoring
// detector.ts — startMonitor()
// Polls every 60 seconds
// Tracks seen arb IDs to alert only on NEW opportunities
// Logs all discoveries with timestamps
Setup Guide
1. Clone and Install
git clone <your-polymarket-bot-repo>
cd polymarket-bot
npm install
2. Configure Environment
Copy .env.example to .env and fill in:
# Required for live trading
POLYGON_WALLET_PRIVATE_KEY=your_polygon_private_key
POLYMARKET_FUNDER_ADDRESS=your_funder_address
POLYMARKET_API_URL=https://gamma-api.polymarket.com
POLYMARKET_CLOB_URL=https://clob.polymarket.com
# Risk management
STARTING_CAPITAL=1000
MAX_POSITION_SIZE=500
MAX_TOTAL_EXPOSURE=2000
MIN_EDGE_THRESHOLD=0.10
STOP_LOSS_PERCENT=5
TAKE_PROFIT_PERCENT=5
# Optional: CEX for hedging
BINANCE_API_KEY=
BINANCE_API_SECRET=
# Safety
DRY_RUN=true # Start with paper trading!
LOG_LEVEL=info
LOG_TO_FILE=true
3. Run Each Stream
# Stream 1: Score markets and find EV opportunities
npm run agent:alpha
# Stream 2: Run strategy optimizer overnight
npm run research:auto
# Stream 3: Scan for cross-platform arb
npm run arb:scan
# Full bot (all streams)
npm run bot
Recipes
Recipe 1: Morning Scan for Arb Opportunities
# 1. Scan for spreads
npm run arb:scan
# 2. Review HIGH confidence opportunities only
# Look for spreadPct > 5% with Kelly > 3%
# 3. Verify the match manually
# Check that the title matcher correctly paired the events
# Open both Polymarket and 1WIN to confirm prices are live
# 4. If confirmed, execute on Polymarket (DRY_RUN=false)
# The bot respects MAX_POSITION_SIZE and STOP_LOSS_PERCENT
Recipe 2: Run Overnight Strategy Improvement
# 1. Check current Brier score
npm run research:eval
# 2. Start the auto-improver (takes 10-30 minutes)
npm run research:auto
# 3. Check results in the morning
cat research/program.md | tail -30
# 4. If version bumped, verify the checkpoint
ls research/checkpoints/
# 5. Deploy updated strategy
# The agent automatically uses the latest strategy.ts
Recipe 3: Check Portfolio EV
// Run in your TypeScript environment
import { scoreMarket, rankByEV } from './src/quant/ev-calculator';
import { quarterKelly } from './src/quant/kelly-criterion';
// For each active position, score against your current probability
const positions = [
{ id: 'btc-100k', yesPrice: 0.35, ourP: 0.42 },
{ id: 'election-x', yesPrice: 0.60, ourP: 0.55 },
];
const scored = positions.map(p => scoreMarket(p, p.ourP));
const ranked = rankByEV(scored);
ranked.forEach(m => {
console.log(`${m.marketId}: EV=${m.ev.toFixed(3)}, Edge=${m.edgePct.toFixed(1)}%, Kelly=${m.kellyFraction.toFixed(3)}`);
console.log(` → ${m.recommend ? '✅ TRADE' : '⏭️ SKIP'}`);
});
Recipe 4: Add a New Market to the Bayesian Tracker
import { bayesUpdate, addEvidence } from './src/quant/bayesian-updater';
// Initialize state for a new market
let state = {
marketId: 'fed-rate-cut-march',
priorP: 0.50,
currentP: 0.50,
evidence: [],
lastUpdated: new Date(),
};
// New evidence: Fed minutes suggest dovish stance (LR > 1 → supports YES)
state = addEvidence(state, {
description: 'Fed minutes dovish tone, multiple members favor cut',
likelihoodRatio: 1.8,
timestamp: new Date(),
});
console.log(`Updated probability: ${(state.currentP * 100).toFixed(1)}%`);
// Output: ~64.3% (moved from 50% toward YES)
// More evidence: CPI comes in hot (LR < 1 → supports NO)
state = addEvidence(state, {
description: 'CPI +0.4% MoM, above expectations',
likelihoodRatio: 0.6,
timestamp: new Date(),
});
console.log(`Updated probability: ${(state.currentP * 100).toFixed(1)}%`);
// Pulled back toward 50%
Recipe 5: Backtest a Strategy Change
// 1. Edit research/strategy.ts with your hypothesis
// Example: change minEdgePct from 3.0 to 2.0
// 2. Run evaluation
// npm run research:eval
// 3. Check the backtest output:
// BacktestResult {
// winRate: 0.54,
// brierScore: 0.1820,
// sharpeEstimate: 1.2,
// recommendation: 'EDGE' // or 'STRONG_EDGE' / 'NO_EDGE'
// }
// Decision matrix:
// STRONG_EDGE (winRate > 56% AND Brier < 0.22) → Deploy immediately
// EDGE (winRate > 52%) → Run for 1 week on paper
// NO_EDGE → Revert the change
Architecture Notes
The system is modular by design. Each quant module (kelly-criterion.ts, ev-calculator.ts, bayesian-updater.ts) is pure functions with no side effects — they can be imported independently into any project. The research loop operates on strategy.ts as a single source of truth, with versioned checkpoints for rollback. The arbitrage detector cascades through data sources (1WIN API → CLOB proxy → mock) for reliability.
All trading respects risk constraints: MAX_POSITION_SIZE, STOP_LOSS_PERCENT, and DRY_RUN mode. Start with paper trading. Always.
Reviews (0)
No reviews yet. Be the first to review!
Comments (0)
No comments yet. Be the first to share your thoughts!