CodeFix Solution

From Notebook to Production: Shipping Your First Sports Trading Bot

May 11, 2026 · 13 min read · Python, Production, Deployment, Trading Bots

You have a Jupyter notebook that produces calibrated win probabilities. You have backtested a strategy that looks profitable. The next step is the hard part: turning the notebook into a service that runs unattended, places real orders against a real market, recovers from crashes, and surfaces the right alerts when something breaks. This post is the practical playbook for that transition.

The model in your notebook is maybe 20% of the work. The other 80% is what we cover here.

What changes between notebook and production

A notebook lives inside one Python process. State is in memory. If something goes wrong, you re-run the cell. The model is loaded once and runs against a static dataset. There are no concurrent users, no failures, no clocks.

A production trading bot lives in a long-running process under a supervisor. State must persist across restarts. Failures must be handled gracefully. Concurrent events arrive on the network and must be processed in the right order. A clock advances continuously and the bot must keep up.

Each of those differences requires explicit engineering work that the notebook lets you skip.

Step 1: Extract from the notebook

Move the modeling code out of the notebook into a real Python module. Save the trained model to a pickle file. Replace the notebook's data loading with a function that pulls live data instead of static CSVs.

# notebook/research_v3.ipynb → bot/model.py
import pickle
from sklearn.isotonic import IsotonicRegression
from xgboost import XGBClassifier

class ProductionModel:
    def __init__(self, model_path: str):
        with open(model_path, "rb") as f:
            self.clf, self.iso = pickle.load(f)
    def predict(self, features) -> float:
        raw = self.clf.predict_proba([features])[:, 1][0]
        return float(self.iso.transform([raw])[0])

# Save once after training:
# with open("model.pkl", "wb") as f:
#     pickle.dump((clf, iso), f)

The pickle file becomes a versioned artifact. Always backup before retraining (we use a timestamped backups/ directory). One corrupted pickle from an interrupted training job can take you offline for hours.

Step 2: Build the live data ingestion

Notebooks read CSVs. Production reads live feeds. For sports, that usually means polling ESPN's JSON endpoints (or an official league API) on a sport-appropriate cadence and maintaining an in-memory cache.

import asyncio, time
from typing import Optional

class LiveStateCache:
    def __init__(self):
        self._state = {}  # game_id -> dict
        self._ts = {}     # game_id -> epoch

    def update(self, game_id: str, state: dict):
        self._state[game_id] = state
        self._ts[game_id] = time.time()

    def get(self, game_id: str, max_age_s: float = 30.0) -> Optional[dict]:
        if game_id not in self._state:
            return None
        if time.time() - self._ts[game_id] > max_age_s:
            return None  # Stale
        return self._state[game_id]

CACHE = LiveStateCache()

async def poll_loop(sport: str, cadence: float):
    while True:
        try:
            data = fetch_scoreboard(sport)
            for ev in data.get("events", []):
                CACHE.update(ev["id"], parse_event(ev))
        except Exception as e:
            logger.exception(f"Poll failed for {sport}: {e}")
        await asyncio.sleep(cadence)

The poll loop runs as a background task. The bot's main loop reads from the cache. The two are decoupled so a slow upstream does not block the bot.

Step 3: Persist your trade log

A bot that loses its trade history on restart is dangerous. It might re-buy positions it already holds. It might miss settlements it should have processed. The fix is an append-only trade log that survives restarts.

import json
from pathlib import Path
from datetime import datetime, timezone

TRADE_LOG = Path("trades.jsonl")

def log_trade(game_id: str, side: str, size: float, price_c: float, fair_prob: float):
    rec = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "game_id": game_id,
        "side": side,
        "size": size,
        "price_c": price_c,
        "fair_prob": fair_prob,
        "resolved": None,
    }
    TRADE_LOG.open("a").write(json.dumps(rec) + "\n")

def load_open_positions() -> list[dict]:
    if not TRADE_LOG.exists():
        return []
    open_pos = []
    for line in TRADE_LOG.open("r"):
        rec = json.loads(line)
        if rec.get("resolved") is None:
            open_pos.append(rec)
    return open_pos

JSONL is the right format. Each trade is one line. Append is atomic on most filesystems. You can tail -f the file to watch live activity. You can read it with pandas for analysis.

Step 4: Reconstruct state on startup

When the bot restarts, it reads the trade log and rebuilds its in-memory position map. Without this step, the bot wakes up thinking it has zero positions, even when there are several open trades on Polymarket.

def restore_positions():
    positions = {}
    for trade in load_open_positions():
        gid = trade["game_id"]
        positions.setdefault(gid, []).append(trade)
    return positions

# In bot startup
positions = restore_positions()
logger.info(f"Restored {sum(len(v) for v in positions.values())} open positions across {len(positions)} games")

Step 5: Add a supervisor

A bot that crashes and stays down is worse than no bot. Run it under systemd (Linux) or launchd (macOS) with auto-restart. Systemd unit:

[Unit]
Description=My Sports Trading Bot
After=network-online.target

[Service]
Type=simple
User=mybot
WorkingDirectory=/opt/mybot
ExecStart=/opt/mybot/venv/bin/python -m bot.main
Restart=always
RestartSec=15
StandardOutput=append:/var/log/mybot/bot.log
StandardError=append:/var/log/mybot/bot.log

[Install]
WantedBy=multi-user.target

Restart=always with RestartSec=15 prevents tight crash loops while ensuring the bot recovers from transient errors. Logs go to a file you can tail -f.

Step 6: Build monitoring

The most common production failure mode is silent. The bot is "running" but not actually trading. Three things to monitor:

import time, json
from pathlib import Path

HEARTBEAT = Path("heartbeat.json")

def write_heartbeat():
    HEARTBEAT.write_text(json.dumps({
        "ts": time.time(),
        "iso": datetime.now(timezone.utc).isoformat(),
    }))

# In bot main loop, every minute
write_heartbeat()

Step 7: Deployment workflow

You will iterate on the bot. Define a deploy workflow that is fast and safe:

  1. Edit code locally.
  2. Run unit tests.
  3. rsync changes to the server (excluding stateful files like trade logs).
  4. Run a remote import smoke test (catches syntax errors).
  5. systemctl restart the service.
  6. curl the health endpoint to verify it came back up.

Wrap this in a shell script. Run it 50 times a day during early development. Once you have confidence in the workflow, the friction of deploying drops to near-zero.

Step 8: Risk controls

Before live trading, add hard limits:

These controls feel paranoid until the day they save you. Add them all before going live, not after the first incident.

What separates the bots that survive

Three patterns across the bots that have run in production for many months without intervention:

They restart cleanly. State persists. The bot can be killed and brought back at any moment without losing track of positions or producing duplicate trades.

They monitor themselves. Heartbeats, trade-rate alerts, and CLV monitoring catch the silent failure modes that watching P&L would miss.

They fail loud, not silent. Every exception logs. Every uncovered edge case raises an alert. The cost of fixing one bug today is much lower than the cost of debugging six months of compounded weird behavior.

The bottom line

Shipping a sports trading bot is mostly engineering discipline, not modeling. The modeling work in your notebook is necessary. The persistence, monitoring, supervision, and deployment work is what makes it actually run unattended for months. Build the eight steps above before going live. The compound returns start when the entire stack is in place — not when any single piece is perfect.

The full Polymarket Bot Course

Six Jupyter modules covering everything from ESPN scraping to live deployment, including the production patterns in this post. $49 standalone or included with every ZenHodl API plan.

Get the course