←Back to Blog
Quantitative FinanceBayesian InferenceKelly CriterionControl Theory

Copy Trading as a
Constrained Control Policy

A mathematical framework for automated prediction market trading. Wilson-bounded accuracy, Bayesian probability updates, Kelly-optimal sizing, and stopping-time exits.

¡25 min read
Cycle-Based Control Loop π(St, At) → Ut
📡
Collect Alerts
🎯
Filter Eligible
📊
Compute Prior
🧮
Bayes Update
⚖️
Kelly Size
📈
Entry/Exit
⚡
Execute
The Policy

Trading as Control

The entire system is a single policy π that maps state and new alerts to actions. Each cycle processes incoming signals, filters through eligibility gates, computes Bayesian posteriors, sizes positions via Kelly, and executes.

This isn't discretionary trading. It's a constrained optimization where every decision is governed by mathematical invariants.

π: (state St, alerts At) → actions Ut

Data Structures

Alerts as Mathematical Objects

An alert a ∈ 𝒜 is a tuple encoding everything needed to evaluate a signal: trader identity, market, side, value, price, and timestamp.

Trader metrics form a snapshot vector xi(t) with edge score, confidence, win rate, PnL, and drawdown. Market state ym(t) tracks liquidity, resolution, and category.

The orderbook snapshot obm(t) provides best bid/ask, spread, depth, and freshness for execution decisions.

Alert Tuple a ∈ 𝒜struct
a = (
id:a_847291,
i:trader_42// trader_id,
m:polymarket_xyz// market_id,
s:+1// side (YES),
v:$2,500// value_usd,
p_a:0.62// alert_price,
τ:1706547200// created_at
)
Live State

Real-Time Portfolio Tracking

The bot maintains complete state: open positions, equity, exposures per market and category, daily PnL, and drawdown metrics.

Alerts Processed
0
↑+847 today
Active Positions
0
12 markets
Win Rate
68.2%
↑+2.1%
Monthly ROI
0%
↑Kelly-scaled
Portfolio State St
12
Open Positions
|𝒫t|
$50k
Portfolio Equity
Wt
$35k
Total Exposure
Et
Exposure Ratio70%
Et = Σp∈𝒫t notional(p) = $35,000
types.ts
1interface Position {
2 m: MarketId; // market_id
3 s: 1 | -1; // side (YES/NO)
4 q: number; // share quantity
5 p_entry: number; // avg entry price
6 t_open: number; // timestamp opened
7 i_src: TraderId; // source trader
8 entry_edge: number; // edge at entry
9}
10 
11interface PortfolioState {
12 positions: Map<PositionId, Position>;
13 equity: number; // W_t
14 exposure: number; // E_t = ÎŁ notional
15 exposure_by_market: Map<MarketId, number>;
16 exposure_by_category: Map<Category, number>;
17 daily_pnl: number; // L_t^(day)
18 drawdown: number; // DD_t
19}
Signal Eligibility Gates (must all pass)
🔒
Dedup
D(a,t)
¡
👤
Trader
𝟙_trader
¡
📈
Market
𝟙_market
¡
⏱️
Staleness
𝟙_stale
¡
💧
Liquidity
𝟙_liq
¡
📊
Slippage
𝟙_slip
𝟙eligible(a,t) = 𝟙dedup · 𝟙trader · 𝟙market · 𝟙stale · 𝟙liq · 𝟙slip
Hard Constraints

Signal Eligibility Gates

Before any math runs, alerts must pass a product of indicator functions. Each gate is binary: pass or reject.

Dedup: Redis TTL prevents processing the same alert twice. Trader: Whitelist + metric thresholds. Market: Liquidity, resolution, cooldown. Staleness: Max signal age.

Only if 𝟙eligible(a,t) = 1 does the alert proceed.

gates.ts
// Composite eligibility
const eligible =
!isDeduplicated(a, t) &&
isTraderWhitelisted(i, t) &&
meetsTraderMetrics(x_i, thresholds) &&
isMarketEligible(y_m, t) &&
!isStale(a.tau, t, MAX_SIGNAL_AGE) &&
hasLiquidity(ob_m, MIN_LIQUIDITY) &&
slippage(a, ob_m) <= MAX_SLIPPAGE;
Trader Accuracy

The Wilson Lower Bound

Raw win rate is deceptive. A trader with 3 wins in 4 trades (75%) could be lucky. We need a conservative estimate.

The Wilson score interval gives us θi: the lower bound on true accuracy at 95% confidence. This is what we use for Bayesian updates.

Edge score is simply θi − 0.50. A random coin flip has edge zero. Elite traders push θ toward 0.75+.

wilson.ts
function wilsonLower(wins: number, n: number): number {
const p = wins / n;
const z = 1.96; // 95% confidence
 
return (
p + (z*z)/(2*n) -
z * Math.sqrt((p*(1-p))/n + (z*z)/(4*n*n))
) / (1 + (z*z)/n);
}
 
const theta_i = wilsonLower(wins, resolved);
const edge_score = theta_i - 0.50;
Wilson Score Lower Bound (95% CI)
Wins65
Resolved Positions100
Win Rate
65.0%
θi (Wilson)
55.3%
Edge Score
+5.3%
Trader Tier: Edge
θi = WilsonLower(p=0.65, n=100, z=1.96) = 0.553
Bayesian Probability Update
Trader Accuracy (θi)65%
Random (50%)Elite (95%)
Market Prior (p)50%
If Trader Alerts YES
65.0%
π̂(Y=1|s=+1)
If Trader Alerts NO
35.0%
π̂(Y=1|s=−1)
π̂ = (θ·p) / (θ·p + (1−θ)·(1−p))
Belief Model

Bayes Posterior

Given the market's prior probability p (from orderbook mid) and a trader's alert, we compute the posterior probability of the outcome using Bayes' theorem.

The trader is modeled as a noisy signal with symmetric accuracy θi. If Y=1 (YES true), trader says YES with prob θ. If Y=0, says NO with prob θ.

π̂ = (θ·p) / (θ·p + (1−θ)·(1−p))

This is the "math heart" of the system. A trader with θ=0.5 contributes nothing (posterior equals prior). As θ→1, the posterior moves decisively.

Expected Value

The EV Gate

An alert only proceeds if the expected value is positive after fees. This is the fundamental constraint that filters out negative-EV trades.

Expected Value Formula

For buying YES at price c with belief π̂, the expected return per dollar is:

EV = π̂ / c − 1 − buffer

The fee buffer accounts for platform fees, spread costs, and execution slippage. Only if EV > 0 do we proceed.

Example Calculation

TypeScript
const belief = 0.72; // π̂ from Bayes
const fillPrice = 0.58; // c from orderbook
const feeBuffer = 0.02; // 2% for fees/slippage
 
const ev = belief / fillPrice - 1 - feeBuffer;
// = 0.72 / 0.58 - 1 - 0.02
// = 1.241 - 1 - 0.02
// = 0.221 (+22.1% expected edge)
 
const passesGate = ev > 0; // true ✓
Position Sizing

Kelly Criterion

The Kelly criterion tells us the optimal fraction of bankroll to wager for long-term growth. For binary contracts:

f* = (π̂ − c) / (1 − c)

Raw Kelly is aggressive. We apply dampers: a global Kelly fraction (e.g., 25%), drawdown scaling, and latency reduction for stale signals.

f(a,t) = kelly_fraction ¡ Νdd(t) ¡ Νlat(a,t) ¡ f*(a,t)

Kelly Position Sizing
Belief (π̂)65%
Fill Price (c)50¢
Kelly Damper25%
Raw Kelly
30.0%
EV
+30.0%
Position
$750
f* = (π̂ − c) / (1 − c) = (0.65 − 0.50) / 0.50
Position Sizing Clamp Stack
Raw Kelly$2,500
Max Position$2,000
Portfolio Cap$1,800
Liquidity$1,500
Trader Cap$1,200
Final Size$1,200
u(a,t) = min(uraw, Upos, Uport, Uliq, Utrader, ...)
Risk Management

The Clamp Stack

Kelly gives us an ideal size. Reality imposes constraints. The final position is the minimum of all applicable caps.

  • Upos: Maximum position size
  • Uport: Remaining portfolio capacity
  • Uliq: Liquidity × max_liquidity_pct
  • Utrader: Alert value × trader_bet_multiplier
  • Umarket: Per-market exposure limit
  • Ucategory: Per-category exposure limit
TypeScript
const finalSize = Math.min(
rawKellySize,
MAX_POSITION_SIZE,
portfolioCapacity,
liquidity * MAX_LIQUIDITY_PCT,
alertValue * TRADER_BET_MULTIPLIER,
marketExposureRemaining,
categoryExposureRemaining
);
Exit Logic

Stopping Times

Each open position has multiple exit conditions defined as stopping times. The position closes at the first condition that triggers.

Risk Manager handles stop-loss, take-profit, and the ceiling rule (exit when price ≥ 98¢).

Exit Monitor tracks resolved/expired markets, trader edge decay, and follow-trader exits.

τexit = min(τSL, τTP, τceil, τresolved, τexpired, τdecay, τfollow)

Exit Stopping Times (first triggered wins)
🛑
Stop-Loss
τ_SL
🎯
Take-Profit
τ_TP
📈
Ceiling
τ_ceil
✅
Resolved
τ_resolved
⏰
Expired
τ_expired
📉
Edge Decay
τ_decay
👤
Follow Exit
τ_follow
τexit(p) = min(τSL, τTP, τceil, τresolved, τexpired, τdecay, τfollow)
Complete System

The Full Algorithm

At each cycle time tk, the policy executes these steps in sequence. No undefined functions remain.

cycle.ts
1async function executeCycle(t: number, alerts: Alert[], state: PortfolioState) {
2 // 1. Dedup + hard filters
3 const candidates = alerts.filter(a =>
4 !isDeduplicated(a, t) &&
5 isEligible(a, t, state)
6 );
7 
8 // 2. Compute market prior (orderbook mid or trade-mid fallback)
9 for (const a of candidates) {
10 a.prior = computePrior(a.market, t);
11 }
12 
13 // 3. Compute Wilson theta, then Bayesian posterior
14 for (const a of candidates) {
15 const theta = wilsonLower(a.trader.wins, a.trader.resolved);
16 a.posterior = bayesPosterior(theta, a.prior, a.side);
17 }
18 
19 // 4. EV gate: require π̂/c - 1 - buffer > 0
20 const evPositive = candidates.filter(a =>
21 a.posterior / a.fillPrice - 1 - FEE_BUFFER > 0
22 );
23 
24 // 5. Kelly sizing with clamps
25 for (const a of evPositive) {
26 const rawKelly = (a.posterior - a.fillPrice) / (1 - a.fillPrice);
27 const scaled = rawKelly * KELLY_FRACTION * ddScalar(state) * latencyScalar(a, t);
28 a.size = clampSize(scaled * state.equity, a, state);
29 }
30 
31 // 6. Entry decisions
32 const entries = evPositive.filter(a =>
33 a.size > 0 &&
34 state.positions.size < MAX_OPEN_POSITIONS &&
35 state.daily_pnl > -MAX_DAILY_LOSS &&
36 !isPaused(t)
37 );
38 
39 // 7. Exit decisions
40 const exits = [...state.positions.values()].filter(p =>
41 t >= exitTime(p, t)
42 );
43 
44 // 8. Execute
45 await executeEntries(entries, state);
46 await executeExits(exits, state);
47 
48 // 9. Set dedup keys
49 for (const a of candidates) {
50 await setDedupKey(a);
51 }
52}
Mathematics

Core Equations

θ

Wilson Lower Bound

Conservative accuracy estimate at 95% confidence

θi = (p + z²/2n − z·√(...)) / (1 + z²/n)
π̂

Bayes Posterior

Updated probability given trader signal

π̂ = (θ·p) / (θ·p + (1−θ)·(1−p))
EV

Expected Value

Expected return per dollar after fees

EV = π̂/c − 1 − buffer
f*

Kelly Fraction

Optimal bet size for long-term growth

f* = (π̂ − c) / (1 − c)
E

Total Exposure

Sum of notional across all positions

Et = Σp∈𝒫 notional(p)
τ

Exit Time

First stopping time among all exit rules

τexit = min(τSL, τTP, τceil, ...)

The Complete System

No more black boxes. Every decision from signal eligibility to position sizing to exit timing is governed by explicit mathematical invariants. The only modeling choice that remains is how to calibrate the edge confidence shrinkage factor.

The system is a single policy π defined by: a feasibility region (all gates and caps), a belief model (Bayes with Wilson-bounded θ), a utility sizing rule (Kelly-scaled), and stopping-time exits.

π: (St, At) → Ut

State + Alerts → Actions. That's the entire system.

Mathematical Components

Probability
  • Wilson Score Interval
  • Bayesian Posterior
  • Kelly Criterion
Control
  • Indicator Functions
  • Stopping Times
  • Constrained MDP
Implementation
  • Redis Deduplication
  • ClickHouse Orderbooks
  • Async Execution