Market systems research2025–2026Stopped
Building Polymarket trading systems until the evidence said stop
Three phases of research on Polymarket: a dataset builder, a 15-minute execution stack and a second-scale recorder with Rust and LightGBM. Each ended on evidence: the cost of data, then expected value near zero after fees, then a classifier that predicted well and still lost money as a maker.
- Role
- Independent research and engineering
- Timeline
- 2025–2026, three phases
- Stack
- Python, Rust (PyO3), Polars, LightGBM, WebSockets
- Scope
- Data collection, backtesting, live execution, HFT recording
- Outcome
- Stopped at expected value near zero after fees
- Python
- Rust
- PyO3
- Polars
- LightGBM
- WebSockets
- Parquet
- Backtesting
- +2

- Question
- Could I find an edge in Polymarket's binary markets that survived fees, latency and real fills: first over long horizons, then 15 minutes, then seconds?
- What I built
- A Gamma and Data API dataset builder with holder-level features, a vectorized 15-minute backtester with a live bot on the order book, and a Rust + Python recorder feeding LightGBM.
- Outcome
- The measurement became strict enough to say no: 15-minute prices were well calibrated, and a 0.921-accuracy classifier still lost money as a maker. I stopped and kept the infrastructure.
- Average expected value after fees in 15-minute markets
- ≈ 0
- Phase 02, strict simulator
- Backtest hit rate before and after a 300 ms reaction latency
- ~98% → ~55%
- The stale-quote effect
- Test accuracy of the 1-second adverse-selection model
- 0.921
- Maker-style PnL still negative
- Rows recorded in one 15-minute window
- 58,613
- 57 columns, local receipt timestamps
Hit rates are approximate: they describe the typical shape of short-horizon backtests in Phases 02 and 03, as noted in the code's quant audit. Accuracy comes from the LightGBM test evaluation; the row count from recorder slug sol_1771810200.
Each phase started where the last one got stuck
I moved from long-horizon markets to 15-minute windows to second-scale signals to get a faster, cleaner feedback loop. Each move exposed a harder bottleneck than the one before.
Build the dataset that did not exist
Phase 01 · Long-horizon prediction · Python, Gamma API, Data API, CLOBThere was no public bulk dump of Polymarket trade history, so I built one: Gamma API for events, Data API for trades, optional order-book features, and a holder layer with the top 20 holders per market, a five-day trader-metrics cache and a whale-weighted market lean. Resume offsets, ID normalisation and 429 backoff kept long runs alive.
Collection, not modelling, became the bottleneck, and long-horizon markets resolve slowly. The phase worked technically but cost too much for the feedback it gave, so I paused it. Phase 01 note
Make the simulator stop flattering me
Phase 02 · 15-minute Up/Down markets · Python, WebSockets, CLOB15-minute markets gave a fast loop and clean labels. I built a vectorized backtester with block configurations, strategy modes such as FADE, MOMENTUM, REVERSE and DUAL_ENTRY, walk-forward selection and per-slug volatility regimes, then a live bot with a WebSocket-driven state machine, a web dashboard and a server package.
Once the simulator was strict, prices looked well calibrated and expected value sat near zero after fees. Phase 02 note
Record my own data and trust my own timestamps
Phase 03 · Second-scale signals · Rust, Python, Polars, LightGBMThe last question was narrower: did second-scale signals, such as Polymarket lagging Binance, still contain something tradable? I moved WebSocket handling and the feature path into Rust, recorded both feeds with local receipt times, and kept the Parquet dataset pipeline and LightGBM training in Python.
The models reached good accuracy, but maker-style PnL in evaluation was still negative. The HFT stack never traded live. Phase 03 note
Phase 02 · Measurement
The backtest had to hurt before I trusted it
The biggest improvement in Phase 02 was not a strategy. It was a simulator that stopped flattering me, built on three rules: process stop, then take-profit, then entry inside each block; model a 300–1000 ms reaction latency; and charge fees, slippage and liquidity limits, with 30 bps fees and 10 bps slippage as the baseline. The full method is in How a real backtest works.
| Backtest setup | Hit rate | What it meant |
|---|---|---|
| No reaction latency | Up to ~98% | Buying quotes that were already gone. Flattering, not actionable. |
| 300 ms reaction latency | Around the mid-50s | A more honest baseline. |
| 300 ms + slippage + liquidity filter | Often weak or negative | Where many apparent edges disappeared. |
Typical shape across short-horizon runs, not a single benchmark. Stress runs went further: 1 s latency, 2% slippage and a minimum ask size of 500.


Two 15-minute SOL windows replayed by the backtester with the same entry levels (plot labels in Spanish). The second looks brilliant only because two 0.03 entries flipped to 0.50; the flip analyzer showed such cheap late zones were often dead zones, not bargains. Only the aggregate after fees counts, and it stayed near zero.
Phase 02 · Execution
Live trading was mostly synchronisation
The live bot moved through five states against the order book: FLAT, ENTRY_WORKING, OPEN, EXITING and ROLLING_OVER. The hard part was agreeing with the exchange. WebSocket events, at roughly 50 ms, were the source of truth for fills; REST, closer to 500 ms, only reconciled; filled inventory stayed separate from open orders.
Partial fills, 15-minute rollovers and ghost orders (closed on the exchange, still active in the bot) were normal conditions, not exceptions. I added forced cleanup for ghost orders, a proactive stop placed when price comes within 0.10 of the stop level instead of 0.02, and no re-entry until price has moved away from the last stop. More in The reality of bots on Polymarket.

Phase 03 · Architecture
Rust where timing matters, Python where iteration matters
The HFT phase split the stack by responsibility. Latency-sensitive code moved to Rust (poly_rust_core); everything that needed fast iteration (ETL, datasets, training, experiments) stayed in Python.
01
Rust hot path
poly_rust_core
- Binance and Polymarket WebSockets
- Event-driven wait on each Binance tick
- Single 40-feature path: spread, OBI, lags
- Recorder and latency primitives
02
Recorder and ETL
Python
- Recorder orchestration, token refresh
- Raw JSONL with local receipt time
- Incremental JSONL → Parquet
- Last-known-value alignment
- Freshness columns, leakage checks
03
Datasets
prepared_l1, prepared_l2
- Train, validation and test splits
- Rows filtered at 200–500 ms latency
- Checked in the feature explorer
04
Models
LightGBM
- Taker: hft_1s, hft_5s, hft_10s
- Maker: maker_1s
- Adverse: adverse_bid_1s
- Fair value and expiry variants
05
Execution
real_trading_hft
- Paper and live modes, one design
- No API calls in the hot loop
- Queue-backed API worker
- Ping checks, latency breaker, 0.10–0.90 price band
The execution layer supports paper and live modes; the HFT stack never traded live.
Inspect one recorded window yourself
This is the explorer I used to debug features: one recorded 15-minute SOL window (sol_1771810200, 58,613 rows, 57 columns). Plot Binance against the Polymarket book, switch the Hampel, EWMA and rolling-median filters, and see which features stay stable. There is also a full-page version.
Recorded L1 feature explorer, SOL 15-minute window
Open full screen
About 6 MB. Loads Plotly from its CDN and plots a 12,000-point sample of the window.
Phase 03 · Models
A 0.921-accuracy model still lost money as a maker
I trained LightGBM on several target families built from the same features: taker horizons, maker protection, adverse selection, fair value and expiry. The most telling case was the 1-second adverse-selection model: test accuracy around 0.921, Brier score around 0.068, log loss around 0.244, and inference in the tens of microseconds per batch.
Its maker-style PnL in the same evaluation was negative. Whether a prediction pays depends on queue position, reaction time and real fills, which even an honest simulator only approximates. I trust that result more than a headline metric: the pipeline built a decent classifier and still refused to call it an edge.
Why I stopped, and what I kept
By the end, each phase had answered its question. Phase 01 worked technically but cost too much for the feedback it gave. In Phase 02, 15-minute prices were well calibrated and timing did not unlock a durable edge after fees. In Phase 03, a good classifier still produced negative maker-style PnL.
Stopping on that evidence was a result, not a failure. What I kept is the infrastructure and the habit: a reusable backtesting and optimisation framework, a live state machine with server packaging, a recorder and ETL that respect timestamps, and a simulator used to disqualify ideas rather than fall in love with them.
The system had become honest enough to reject the thesis instead of flattering it.
Read the notes in order
- Phase 01: the prediction dataset. The scraper, holder features and the real cost of building data.
- Phase 02: 15-minute trading. Vectorized research, the live bot and the result that ended it.
- Phase 03: HFT, Rust and the 98% lie. Recorder-first design, prepared datasets and model evaluation.
- How a real backtest works. Look-ahead, stale quotes, payout bugs and fantasy fills.
- The reality of bots. States, fills, workers and risk rails in a live bot.
- HFT feature explorer. The recorded-window explorer at full size.
Next case study
Classifying gym exercises from wrist-worn IMU data
Related
To predict Polymarket outcomes, I first had to build the dataset
I set out to spot mispriced long-horizon markets on Polymarket. No public dataset had the history I needed, so I built the collector first, and learned that collection, not modelling, was the bottleneck.
Polymarket journey · 1/3
6 min
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.
Polymarket journey · 2/3
8 min read
HFT on Polymarket: Rust, timestamps and the 98% hit rate that wasn't
Second-scale trading research on Polymarket, built recorder-first: Rust on the hot path, Python for datasets and LightGBM. Good classifier metrics did not turn into profit, and a 98% backtest hit rate fell to about 55% with 300 ms of reaction latency.
Polymarket journey · 3/3
8 min
Get in touch
Working on something similar? I am happy to compare notes.