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

- 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.
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
02
Simulate
Research layer
- Vectorized backtests over block configurations
- 3D parameter sweeps
- FADE, MOMENTUM, REVERSE, DUAL_ENTRY
- Latency, fees, slippage, liquidity
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
04
Trade
Execution layer
- Five-state machine on the CLOB
- WebSocket prices and fills
- Limit orders, 5-share minimum
- Proactive stop, lock pairs
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 = 30in the base simulator.- Slippage
slippage_bps = 10in 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 setting | Hit rate and result | Reading |
|---|---|---|
| No reaction latency | Extremely high, sometimes near 98% | Too optimistic to trust |
| 300 ms reaction latency | Falls sharply, often near the mid-50s | Much closer to reality |
| 300 ms + 2% slippage + liquidity filter | Often weak or negative | Now 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.
- FLATNo position.
- entry condition metthen
- ENTRY_WORKINGLimit entry order sent; waiting for a fill.
- fill event on the WebSocketthen
- OPENFilled. Take-profit and stop orders placed.
- take-profit or proactive stopthen
- EXITINGClosing the position.
- interval endsthen
- 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.
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
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.
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
ACTIVEwere detected and cleaned up.04
Dry run before live keys
An explicit dry-run validation step came before any use of live credentials.

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.
| Question | What 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.
Related

Market systems2025–2026Stopped
Polymarket trading systems
Prediction-market research: a dataset builder, a 15-minute bot and a Rust + Python HFT recorder.
Backtest realism
The backtest that lies, and the rules that stop it
The rules that keep short-horizon backtests honest: stop, take-profit, entry inside every bar, reaction latency, slippage, and payout and leakage audits.
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.