Case Study 1: Building and Deploying a Live Trading Bot
Overview
This case study walks through the complete process of building a prediction market trading bot, from initial strategy design through paper trading validation to live deployment. We follow a trader named Alex who develops a calendar-spread strategy for political prediction markets, validates it systematically, and deploys it using the architecture from Chapter 19.
Part 1: Strategy Development
The Strategy Idea
Alex notices that prediction markets for recurring events (monthly economic reports, quarterly earnings, weekly political polls) exhibit predictable patterns in the days leading up to the event. Specifically, contract prices tend to drift toward extreme values (closer to 0 or 1) as the event approaches and uncertainty resolves. This creates opportunities for a mean-reversion strategy in the intermediate period.
The core thesis: In the 3-14 day window before an event, market prices overreact to minor news and tend to revert to a slowly-changing baseline. Inside 3 days, fundamental uncertainty dominates, and outside 14 days, the edge is too small.
Formalizing the Signal
Alex defines the signal as:
fair_value = exponential moving average of mid-price (span=48 hours)
signal = (fair_value - current_mid) / historical_std_of_deviations
Buy when signal > +1.5 (price significantly below fair value)
Sell when signal < -1.5 (price significantly above fair value)
Exit when signal crosses zero (price returns to fair value)
Backtesting Results
Alex backtests over 8 months of historical data across 45 recurring-event markets:
| Metric | Value |
|---|---|
| Total trades | 312 |
| Win rate | 58.3% |
| Average win | $22.40 |
| Average loss | $18.70 |
| Profit factor | 1.67 |
| Max drawdown | $420 |
| Sharpe ratio | 1.82 |
| Average holding time | 14 hours |
These results look promising, but Alex knows backtesting assumptions are optimistic. The critical question: how much of this edge survives contact with reality?
Part 2: Building the Bot
Architecture Decisions
Alex chooses the architecture from Section 19.2:
"""
Alex's trading bot architecture decisions:
1. Data Feed: REST API polling every 30 seconds
- WebSockets would be better for latency but the platform's
WS API is unreliable
- 30s is sufficient for a strategy with 14-hour average hold time
2. Signal Generator: Python implementation of the EMA-based
mean reversion signal
- Requires 48 hours of price history to compute the EMA
- Signal threshold of 1.5 standard deviations
3. Risk Manager: Conservative limits for initial deployment
- Max position: 100 contracts per market
- Max daily loss: $200
- Max 5 open positions simultaneously
- Max portfolio exposure: $2,000
4. Order Executor: Limit orders only, passive pricing
- Place buy limits 1 tick inside the best bid
- Place sell limits 1 tick inside the best ask
- Cancel unfilled orders after 10 minutes
5. Position Tracker: In-memory with periodic persistence
- Save state to disk every 60 seconds
- Reconcile with exchange every 5 minutes
6. Logger: Structured JSON logging to file + console
- Rotate log files daily
- Alert on email for CRITICAL events
"""
Key Implementation Details
Alex implements the signal generator:
import numpy as np
from collections import deque
from datetime import datetime, timedelta
class MeanReversionSignal:
"""
Generates mean-reversion signals based on price deviation
from exponential moving average.
"""
def __init__(self, ema_span_hours: float = 48.0,
signal_threshold: float = 1.5,
min_history_hours: float = 72.0,
event_blackout_days: float = 3.0):
self.ema_span_hours = ema_span_hours
self.signal_threshold = signal_threshold
self.min_history_hours = min_history_hours
self.event_blackout_days = event_blackout_days
# Per-market state
self.price_history = {} # market_id -> [(timestamp, price)]
self.current_signals = {} # market_id -> last signal value
def __call__(self, update, history):
"""Called by SignalGenerator on each market update."""
market_id = update.market_id
# Calculate mid price
if update.best_bid and update.best_ask:
mid = (update.best_bid + update.best_ask) / 2
elif update.last_price:
mid = update.last_price
else:
return None
# Maintain price history
if market_id not in self.price_history:
self.price_history[market_id] = deque(maxlen=10000)
self.price_history[market_id].append(
(update.timestamp, mid)
)
# Need minimum history
prices = self.price_history[market_id]
if len(prices) < 100: # Need at least 100 data points
return None
first_timestamp = prices[0][0]
hours_of_data = (
update.timestamp - first_timestamp
).total_seconds() / 3600
if hours_of_data < self.min_history_hours:
return None
# Calculate EMA
price_array = np.array([p[1] for p in prices])
alpha = 2.0 / (self.ema_span_hours * 120 + 1)
# 120 data points per hour at 30s polling
ema = self._calculate_ema(price_array, alpha)
# Calculate signal
deviation = mid - ema
deviations = price_array - self._calculate_ema_array(
price_array, alpha
)
std_dev = np.std(deviations)
if std_dev < 0.001:
return None
signal_value = deviation / std_dev
self.current_signals[market_id] = signal_value
# Generate trading signals
if signal_value > self.signal_threshold:
# Price is above fair value -- sell signal
return {
"side": Side.SELL,
"price": update.best_ask or mid,
"confidence": min(abs(signal_value) / 3.0, 1.0),
"metadata": {
"signal_value": signal_value,
"ema": ema,
"deviation": deviation,
},
}
elif signal_value < -self.signal_threshold:
# Price is below fair value -- buy signal
return {
"side": Side.BUY,
"price": update.best_bid or mid,
"confidence": min(abs(signal_value) / 3.0, 1.0),
"metadata": {
"signal_value": signal_value,
"ema": ema,
"deviation": deviation,
},
}
return None
def _calculate_ema(self, prices, alpha):
"""Calculate the current EMA value."""
ema = prices[0]
for p in prices[1:]:
ema = alpha * p + (1 - alpha) * ema
return ema
def _calculate_ema_array(self, prices, alpha):
"""Calculate EMA for every point in the array."""
ema_arr = np.zeros_like(prices)
ema_arr[0] = prices[0]
for i in range(1, len(prices)):
ema_arr[i] = alpha * prices[i] + (1 - alpha) * ema_arr[i-1]
return ema_arr
Risk Configuration
Alex sets conservative risk limits for the initial deployment:
risk_limits = RiskLimits(
max_position_size=100, # Max 100 contracts per market
max_position_value=500.0, # Max $500 per position
max_order_size=50, # Max 50 contracts per order
max_daily_loss=200.0, # Stop trading after $200 loss
max_portfolio_exposure=2000.0, # Max $2000 total exposure
max_category_exposure=800.0, # Max $800 per category
max_price_deviation=0.10, # Max 10% from current market
max_volume_fraction=0.03, # Max 3% of daily volume
min_spread=0.01, # Don't trade if spread > 1 cent
)
Part 3: Paper Trading
Setup
Alex deploys the bot in paper trading mode for 21 days against live market data. The paper trading engine simulates: - 80% fill probability for limit orders - Random slippage with std dev of 0.2 cents - 20% chance of partial fills - 150ms average simulated latency
Paper Trading Results
After 21 days:
| Metric | Backtest (same period) | Paper Trading |
|---|---|---|
| Total trades | 68 | 54 |
| Win rate | 60.3% | 57.4% |
| Average win | $21.80 | $19.60 | |
| Average loss | $17.90 | $18.40 | |
| Total P&L | +$187 | +$98 | |
| Max drawdown | $145 | $178 | |
| Fill rate | 100% | 78% |
| Average slippage | 0 | $0.003 |
Analysis
Alex observes several important differences:
-
Fewer trades. The paper trader filled only 78% of orders, so 14 potential trades were missed. This accounts for most of the P&L difference.
-
Lower win rate. 57.4% vs 60.3%. The missed fills tended to be on orders near the edge of filling -- exactly the ones that would have been marginal winners.
-
Slightly worse average loss. $18.40 vs $17.90 in backtest. Slippage makes losing trades slightly worse.
-
Higher drawdown. $178 vs $145. A few missed winning trades during a drawdown period extended the drawdown.
Adjustments Before Going Live
Based on paper trading results, Alex makes three changes:
-
More aggressive pricing. Instead of placing limits 1 tick inside the best bid/ask, Alex moves to placing at the best bid/ask. This should improve fill rate at the cost of slightly higher slippage.
-
Lower signal threshold. Reducing from 1.5 to 1.3 standard deviations. The paper trading showed that signals at 1.3-1.5 were profitable but often not filled. By trading at a slightly lower threshold, Alex expects more fills.
-
Wider stop-loss. Increasing the exit threshold from 0 to -0.3 sigma. Some winning trades were exited prematurely at exact zero crossing.
Part 4: Small Live Deployment
Go/No-Go Decision
Alex reviews the go/no-go criteria:
- [x] Paper trading ran for 21+ days without system crashes
- [x] Paper trading P&L is positive (though lower than backtest, as expected)
- [x] Fill rate explanation is satisfactory (passive pricing, not a system bug)
- [x] All risk checks triggered correctly during testing
- [x] Position reconciliation showed zero discrepancies
- [x] Monitoring alerts fired correctly on simulated failures
- [x] Runbook is written and tested
- [x] Kill switch tested and accessible via mobile phone
Decision: GO for small live at 25% of target capital.
First Week of Live Trading
Alex starts with $500 of capital (25% of the target $2,000 allocation) and watches closely.
Day 1: Three signals, two orders placed, one filled. Bought 15 contracts at 0.43 in a market with a 0.42 bid / 0.44 ask. Fill price was 0.43 (at the ask). Current price by end of day: 0.44. Unrealized P&L: +$0.15. System ran without errors.
Day 2: One signal, one order placed, one filled. Sold the position from Day 1 at 0.455. Realized P&L: +$0.375. A new buy signal fired but was not filled (limit at 0.38, market moved up). No errors.
Day 3: Four signals, three orders placed, two filled. One position entered and exited the same day for a loss of $0.82. Another position opened (still held). One order expired unfilled after 10 minutes. System ran cleanly.
Day 4: Quiet day. No signals fired. Alex resisted the urge to tweak parameters. Checked the decision journal: "No action taken. System working as designed."
Day 5: The API returned a 500 error on one request. The retry handler retried after 2 seconds and succeeded. The circuit breaker did not trip (threshold = 5 failures). Alert email sent for the 500 error. Alex checked the platform status page -- they had reported a brief outage. System handled it correctly.
Days 6-7 (weekend): Lower volume. Two signals, one fill. Alex noticed the weekend markets are thinner and made a note to investigate whether the strategy performs differently on weekends.
First Week Summary
| Metric | Value |
|---|---|
| Trades | 5 |
| Win rate | 60% (3/5) |
| Total P&L | +$1.24 |
| Max drawdown | $0.82 |
| Fill rate | 62.5% (5/8 orders) |
| Average slippage | $0.004 |
| System uptime | 100% |
| Manual interventions | 0 |
Scaling Up
After two weeks of consistent operation with no system issues, Alex scales to 50% of target capital ($1,000) and widens position limits proportionally. After two more weeks, Alex scales to full target capital ($2,000).
Part 5: Ongoing Operations
Weekly Review Process
Every Sunday, Alex reviews:
-
P&L by market and by day. Looking for patterns -- are certain markets or days consistently bad?
-
Execution quality. Fill rate, slippage, and latency trends. Is execution getting better or worse?
-
Risk check activity. How often were orders rejected? Were any rejections incorrect (would have been profitable trades)?
-
System health. Any errors, warnings, or near-misses?
-
Strategy signals. Are signal characteristics (frequency, distribution, accuracy) consistent with the backtest?
Month 1 Results
After the first full month of live trading:
| Metric | Paper Trading (21 days) | Live Trading (30 days) |
|---|---|---|
| Total trades | 54 | 73 |
| Win rate | 57.4% | 56.2% |
| Average win | $19.60 | $20.10 | |
| Average loss | $18.40 | $19.30 | |
| Total P&L | +$98 | +$104 | |
| Fill rate | 78% | 68% |
| Average slippage | $0.003 | $0.005 |
Key observations:
- Lower fill rate in live vs. paper. The paper trader was slightly optimistic. Alex adjusts the paper trading fill probability from 80% to 70% for future validation.
- Higher slippage in live. Expected -- real market impact is not fully captured by simulation. Alex adjusts the slippage model to 0.5 cents std dev.
- P&L is positive and within expected range. Not as high as the backtest, but the strategy appears to have genuine edge after execution costs.
Lessons Learned
Alex documents these lessons after the first month:
- Paper trading overstated fill rates by about 10%. Adjusted the simulation parameters.
- Weekend markets behave differently. Considering adding a "weekend mode" with tighter limits.
- The hardest part was not intervening. Several times Alex wanted to override the bot. The decision journal helped maintain discipline.
- Infrastructure issues were minor but real. Two API errors, one brief data delay. All handled by the retry and monitoring systems.
- The 48-hour EMA needs more data at strategy start. Added a warmup period where the bot collects data but does not trade for the first 72 hours after starting.
Code
The complete code for this case study is in code/case-study-code.py.