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.
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
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.
Real-Time Portfolio Tracking
The bot maintains complete state: open positions, equity, exposures per market and category, daily PnL, and drawdown metrics.
1interface Position {2 m: MarketId; // market_id3 s: 1 | -1; // side (YES/NO)4 q: number; // share quantity5 p_entry: number; // avg entry price6 t_open: number; // timestamp opened7 i_src: TraderId; // source trader8 entry_edge: number; // edge at entry9}10 11interface PortfolioState {12 positions: Map<PositionId, Position>;13 equity: number; // W_t14 exposure: number; // E_t = ÎŁ notional15 exposure_by_market: Map<MarketId, number>;16 exposure_by_category: Map<Category, number>;17 daily_pnl: number; // L_t^(day)18 drawdown: number; // DD_t19}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.
// Composite eligibilityconst 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;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+.
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;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.
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:
The fee buffer accounts for platform fees, spread costs, and execution slippage. Only if EV > 0 do we proceed.
Example Calculation
const belief = 0.72; // ĎĚ from Bayesconst fillPrice = 0.58; // c from orderbookconst 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 âKelly Criterion
The Kelly criterion tells us the optimal fraction of bankroll to wager for long-term growth. For binary contracts:
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)
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
const finalSize = Math.min( rawKellySize, MAX_POSITION_SIZE, portfolioCapacity, liquidity * MAX_LIQUIDITY_PCT, alertValue * TRADER_BET_MULTIPLIER, marketExposureRemaining, categoryExposureRemaining);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)
The Full Algorithm
At each cycle time tk, the policy executes these steps in sequence. No undefined functions remain.
1async function executeCycle(t: number, alerts: Alert[], state: PortfolioState) {2 // 1. Dedup + hard filters3 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 posterior14 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 > 020 const evPositive = candidates.filter(a =>21 a.posterior / a.fillPrice - 1 - FEE_BUFFER > 022 );23 24 // 5. Kelly sizing with clamps25 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 decisions32 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 decisions40 const exits = [...state.positions.values()].filter(p =>41 t >= exitTime(p, t)42 );43 44 // 8. Execute45 await executeEntries(entries, state);46 await executeExits(exits, state);47 48 // 9. Set dedup keys49 for (const a of candidates) {50 await setDedupKey(a);51 }52}Core Equations
Wilson Lower Bound
Conservative accuracy estimate at 95% confidence
Bayes Posterior
Updated probability given trader signal
Expected Value
Expected return per dollar after fees
Kelly Fraction
Optimal bet size for long-term growth
Total Exposure
Sum of notional across all positions
Exit Time
First stopping time among all exit rules
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.
State + Alerts â Actions. That's the entire system.
Mathematical Components
- Wilson Score Interval
- Bayesian Posterior
- Kelly Criterion
- Indicator Functions
- Stopping Times
- Constrained MDP
- Redis Deduplication
- ClickHouse Orderbooks
- Async Execution