Skip to content
All writing

Execution research2025–2026Stopped8 min read

Polymarket journey — part 2 of 3

Trading 15-minute Polymarket markets until the data said stop

I built a vectorized backtester with four entry modes and walk-forward selection, then ran a live bot on Polymarket's order book. Once the simulator charged for latency, fees and slippage, expected value stayed near zero, so I stopped.

Role
Independent research and systems engineering
Markets
15-minute Up/Down markets on Polymarket
Stack
Python, WebSockets, Polymarket CLOB, Binance volatility data
Built
Vectorized simulator, optimizer, live bot, dashboards
Outcome
Stopped: expected value near zero after fees
  • Python
  • Vectorized backtesting
  • Walk-forward
  • State machines
  • WebSockets
  • CLOB
  • Volatility regimes
  • Polymarket
Backtest plot of one 15-minute SOL Up/Down market. The Up and Down bid and ask lines mirror each other until Up rises to 1 and Down falls to 0. A buy of Down at 0.08 around minute nine ends at zero when the market expires.
Backtest of one 15-minute SOL Up/Down market. Up and Down mirror each other and converge to $1 and $0 at expiry. The backtest bought Down at 0.08 around minute nine; it never recovered and settled at zero. Labels are in Spanish: compra = buy, liquidación = settlement.
Question
Could 15-minute Up/Down markets on Polymarket still leave room for timing mistakes or order-book inefficiencies?
Approach
Record market snapshots, sweep four entry modes with walk-forward selection in a simulator that charges for latency, fees and slippage, then trade live through a state machine on the order book.
Outcome
Prices were well calibrated and expected value stayed around zero after fees. I stopped the strategy and kept the framework, the live state machine and the deployment tooling.

Hypothesis

Fifteen-minute markets promised a faster feedback loop

After pausing long-horizon prediction in part 1, I moved to Polymarket's 15-minute Up/Down markets. Each one asks whether an asset such as SOL finishes a 15-minute window up or down. Up and Down shares trade between $0 and $1, and the side that turns out right pays $1 at expiry.

The attraction was the loop: faster iteration, cleaner labels and a short path from research to execution. The hypothesis was that such a short expiry might still leave room for timing mistakes or order-book inefficiencies.

The plan: record snapshots continuously, enrich every market with Binance volatility context, sweep large parameter grids across four entry modes, then run a live system with real orders and real fills.

System

A strategy notebook grew into an execution environment

The trading15m module is not one script but a small execution environment: a simulator, optimizer tooling, a live state machine, dashboards and a server-ready package, organized in three layers.

  1. 01

    Record

    Research layer

    • Historical and real-time snapshots into sim_snapshots.csv
    • Binance volatility per market: vol_15m, vol_1h, vol_4h
    • Low, medium and high volatility regimes
  2. 02

    Simulate

    Research layer

    • Vectorized backtests over block configurations
    • 3D parameter sweeps
    • FADE, MOMENTUM, REVERSE, DUAL_ENTRY
    • Latency, fees, slippage, liquidity
  3. 03

    Select

    Research layer

    • Walk-forward selection
    • Diversity across volatility regimes
    • Flip analyzer for dead zones
    • Optimizer output: best_config.json and a rules file
  4. 04

    Trade

    Execution layer

    • Five-state machine on the CLOB
    • WebSocket prices and fills
    • Limit orders, 5-share minimum
    • Proactive stop, lock pairs
  5. 05

    Operate

    Operations layer

    • Terminal and web dashboards
    • Server scripts: start, stop, status, logs
    • Dry run before live credentials
    • Reconciliation and ghost-order cleanup

Live runs fed back into research: mismatch reports compared exported market windows with backtests to show where the two diverged.

Research

Four entry modes, tuned per block and per volatility regime

The research layer compared four ways to enter a market. FADE and MOMENTUM were the two core reactions to early price behavior; REVERSE and DUAL_ENTRY were alternative entries for noisier setups.

Each market was split into blocks, and each block carried its own absolute prices for entry, take-profit and stop (entry_range, tp_range, stop_range) instead of relative deviations. Vectorized backtests over these block configurations made large 3D parameter sweeps practical.

Volatility changed what a good configuration looked like. For every market, volatility_by_slug.csv stored Binance volatility over 15 minutes, 1 hour and 4 hours, plus a low, medium or high regime label. Rules could then differ by regime instead of relying on one global setting.

Selection was walk-forward: parameters chosen on one period were judged on the next, with diversity across volatility regimes built into the choice. An optimizer wrote the winner to best_config.json, along with an optimized rules file.

A flip analyzer asked a narrower question: when is a very cheap price late in a market effectively dead, rather than a bargain waiting to flip?

Backtest realism

The simulator had to stop flattering me

The biggest improvement in this phase was not a new strategy. It was making the simulator stop flattering me.

Three rules changed everything. Inside a block, orders are processed in a fixed sequence (stop, then take-profit, then entry), so a position cannot be opened and closed on the same future information. Every decision waits out a reaction latency before it can trade. And every fill pays for execution through fees, slippage and a liquidity filter.

The base simulator carried realistic frictions. Stress runs pushed much harder, to see whether an edge survived pain.

The strict simulator charged for time, fees and thin books

Order inside a block
Stop → take-profit → entry.
Reaction latency
300–1000 ms between seeing a price and acting on it.
Fees
fee_bps = 30 in the base simulator.
Slippage
slippage_bps = 10 in the base simulator; 2% in stress runs.
Liquidity filter
min_ask_size = 500, so a thin ask cannot fill a simulated order.

Each layer of realism removed more of the apparent edge

Adding pain one layer at a time changed the picture. The general method behind this table is in How a real backtest works.

Simulator settingHit rate and resultReading
No reaction latencyExtremely high, sometimes near 98%Too optimistic to trust
300 ms reaction latencyFalls sharply, often near the mid-50sMuch closer to reality
300 ms + 2% slippage + liquidity filterOften weak or negativeNow the strategy has to survive real market structure

Shapes seen across runs, not a single benchmark.

Execution

Live trading was mostly synchronization

The live bot placed limit orders on Polymarket's central limit order book (CLOB) from a server, so it could run around the clock. It made one thing clear very quickly: execution logic is mostly synchronization logic.

WebSockets had to be the source of truth for prices and fills, with roughly 50 ms responsiveness against about 500 ms for REST-style polling. REST stayed useful for reconciliation, not for pretending to be real time. Open orders and real inventory had to be tracked separately.

Partial fills, ghost orders and the rollover from one 15-minute market to the next were normal conditions, not exceptions. The general lessons are in The reality of bots on Polymarket; the loop below is the one this bot ran.

Positions open and close on fills, not on intentions

Every 15-minute market went through the same five states. A position opens only on a fill event, never because an order was sent, and it closes only on a fill or on a timeout followed by cleanup.

  1. FLATNo position.
  2. entry condition metthen
  3. ENTRY_WORKINGLimit entry order sent; waiting for a fill.
  4. fill event on the WebSocketthen
  5. OPENFilled. Take-profit and stop orders placed.
  6. take-profit or proactive stopthen
  7. EXITINGClosing the position.
  8. interval endsthen
  9. ROLLING_OVERThe market ends; switch to the next 15-minute slug.

Other transitions

  • EXITINGFLAT— exit filled, or timeout and cleanup
  • ROLLING_OVERFLAT— next 15-minute market

WebSocket fill events move the bot between states; REST calls only reconcile.

Live trading needed rules no backtest asks for

Desync between the bot and the exchange forced most of these layers.

  1. 01

    Send the stop early

    The stop order goes out when price is about 0.10 away from the stop level, not 0.02, so the order already exists in the book when it is needed.

    0.10, not 0.02

  2. 02

    No re-entry in free fall

    After a stop, the bot does not enter again until price has moved away from the last stop.

  3. 03

    Force-clean ghost orders

    The exchange could close an order that the bot still showed as active. Orders past an age threshold whose exchange status was no longer ACTIVE were detected and cleaned up.

  4. 04

    Dry run before live keys

    An explicit dry-run validation step came before any use of live credentials.

Web dashboard of the 15-minute bot: status Running, uptime, balance, realized PnL, one open take-profit sell order for SOL Up at 0.46 against a current price of 0.001, and one open SOL position.
The web dashboard I used to monitor the bot remotely. In this frame the market has no time left, yet a take-profit sell at 0.46 is still active against a price of 0.001: the kind of rollover edge case the bot had to clean up.

Verdict

The market was better calibrated than my thesis

Once the simulator was rigorous (walk-forward selection, look-ahead fixed, latency and costs charged), each question behind the thesis got a plain answer.

QuestionWhat the data said
Are 15-minute prices systematically wrong?No. Implied probabilities were well calibrated.
Is expected value left after costs?Around zero on average. Fees and execution cost ate any marginal gain.
Does better entry and exit timing create an edge?Not a durable one.
Are very cheap late-minute prices bargains?Often not. The flip analyzer marked them as dead zones.
The strongest output of the 15-minute system was the confidence to stop it.

Stopping on evidence is a result, not a failure

That conclusion was disappointing emotionally and excellent technically. The system had become honest enough to reject the thesis instead of flattering it.

The live bot's hardest problems were synchronization, not entry and exit logic. But it did not stop because the code was broken. It stopped because the premise of an edge was wrong for this market.

Stopping when the data says there is no edge is a result. It only counts as one because the measurement had been made hard to fool.

Aftermath

The strategy stopped; the infrastructure stayed

Carried forward

  • A reusable backtesting and optimization framework: vectorized sweeps, walk-forward selection, optimizer
  • A live execution state machine with fill-driven transitions and cleanup paths
  • Server-ready packaging with start, stop, status and log scripts, plus terminal and web dashboards
  • Clearer thinking about fill logic, volatility regimes and synchronized state

Left behind

  • The 15-minute strategy itself: no durable edge after fees
  • Hit rates measured without reaction latency
  • The idea that very cheap late prices are bargains

A system strong enough to say no

This phase did what a research phase should do: it reduced uncertainty with a system strong enough to say no. It did not leave me a strategy worth running. It left a repeatable proof that the easy edge was not there.

The next question was narrower: could second-scale signals still hold something the 15-minute window did not? Answering it meant recording my own data, moving the hot path to Rust and treating timestamps as seriously as fills. That is part 3, HFT on Polymarket.

Recorded 15-minute SOL window: the Binance ask (blue, left axis, 78.4 to 78.9) plotted against the Polymarket Up bid (orange, right axis, 0 to 0.9) over 900 seconds

Market systems2025–2026Stopped

Polymarket trading systems

Prediction-market research: a dataset builder, a 15-minute bot and a Rust + Python HFT recorder.

Trading systems

A trading bot is infrastructure, not an edge

Why a trading bot is mostly coordination software: explicit states, fills as the only proof of execution, WebSockets as truth and API calls kept off the hot loop.

Get in touch

Working on something similar? I am happy to compare notes.