Research system

Prediction-market research and trading system

A 24/7 platform that watches prediction markets continuously, and the machine-learning and evaluation stack built on top of it.

Self-built, end to end · May 2026 – ongoing · 718 commits as of 9 Aug 2026

  • 718commits, built solo
  • ~1.05 TBarchived, as of Aug 2026
  • 400resolved markets in the audited panel
  • 188,856trades in that panel

The build

What I built

Six scheduled collectors running every 15 to 60 minutes, plus real-time ingest over WebSockets, feeding an archive of ~1.05 TB as of August 2026 — that is the self-reported total on cloud storage; the server itself holds a 250 GB working set that fills and is periodically offloaded, which is its own small operational discipline. Every gap in the feed is logged and masked rather than interpolated, and continuity is accounted for rather than assumed — which is what makes the absence of activity evidence instead of missing data. Out of that corpus I built a licence-clean, hash-verified panel of 400 resolved markets and 188,856 trades.

On top sits the machine learning: gradient-boosted models with isotonic calibration running live inference and order execution, a walk-forward backtester with realistic fill simulation, and a behavioural clustering pipeline — UMAP, HDBSCAN and Gaussian mixtures — over 3,413 wallets.

collector status — hostnames and paths removed
$ systemctl --user list-timers 'poly-*'
UNIT                      ACTIVATES              EVERY
poly-books.timer          poly-books.service     15min
poly-trades.timer         poly-trades.service    15min
poly-markets.timer        poly-markets.service   30min
poly-resolved.timer       poly-resolved.service  60min
poly-wallets.timer        poly-wallets.service   60min
poly-crypto5m.timer       poly-crypto5m.service  15min
6 timers listed. 1 dormant unit not shown.

$ poly-status --audit
gaps  19 >1s over an 18-day window, 74.6 min total (~4.1 min/day)
replay median touch-match 0.9992 across 19 markets
structural audit: 219 markets, 0 defects
The collection pipeline Six scheduled collectors write to storage on a 15 to 60 minute cadence. A gap audit checks continuity, and the audited output is a panel of 400 resolved markets and 188,856 trades. scheduled collector 1, dispatched every 15–60 minutescollector 1 scheduled collector 2, dispatched every 15–60 minutescollector 2 scheduled collector 3, dispatched every 15–60 minutescollector 3 scheduled collector 4, dispatched every 15–60 minutescollector 4 scheduled collector 5, dispatched every 15–60 minutescollector 5 scheduled collector 6, dispatched every 15–60 minutescollector 6 6 scheduled + 1 dormant each collector runs on a systemd timersystemd timers15–60 min server working set; periodically offloaded to a ~1.05 TB archivestorage250 GB working set every feed gap is logged and masked, never interpolatedgap auditcontinuity accounting licence-clean and hash-verified; 382 used in the backtestsaudited panel400 markets · 188,856 trades every gap logged and masked — never interpolated offloaded to a ~1.05 TB archive (self-reported, Aug 2026)
What you're looking at: how the platform is wired. Six collectors run on their own timers, write to storage, and everything passes a gap audit before it becomes the panel the models are evaluated on. The audit is the part that matters: it is what lets a quiet market count as evidence rather than as missing data. Cadences and audit figures as of the 18-day server window · infrastructure detail deliberately omitted

What was hard

Reliability turned out to be an auditing problem rather than an uptime problem. A collector that misses an hour is not a disaster; a collector that misses an hour silently destroys the analysis, because in this work the absence of a price move is itself the signal. So the pipeline records what it failed to see and masks those windows, and one lost collection day is carried as a declared 45-hour gap rather than quietly interpolated away.

The second hard choice was in the backtester. One strategy needed the queue of resting buy and sell offers, which the historical data does not contain. Substituting a rough stand-in would have produced a number — and that number would have been fiction. It declines to run instead, and reports why.

What I found

Trader behaviour predicts profit. The behavioural clustering produced types that hold up against held-out profit and loss, which mattered beyond the result itself: it broke a circular definition — "informed traders are the ones who make money" — that had blocked two earlier research directions.

None of the four candidate strategies beat a random baseline. That is the answer the platform was built to produce, and it is worth as much as a positive would have been: it is a measurement, not a disappointment. The fourth strategy is the one I would point at in an interview — it abstained on every market rather than trade on a signal the data could not supply.

Four strategies against a random baseline Three strategies score no edge with p-values far above 0.05. The fourth is not plotted: it abstained on every market because the data lacks the signal it needs. p = 0.05 mean_reversion_v1 mean_reversion_v1: p = 0.498 vs random, 67 trades p = 0.498 NO_EDGE longshot_fade_v1 longshot_fade_v1: p = 0.601 vs random, 157 trades p = 0.601 NO_EDGE depth_imbalance_v1 no bar — abstained on every market NOT_APPLICABLE regime_conditioned_v1 regime_conditioned_v1: p = 0.613 vs random, 4 trades p = 0.613 NO_EDGE 0 0.25 0.5 0.75 how easily chance alone explains it (longer = more likely chance)

Chart scrolls sideways on a narrow screen — or open “Show the numbers” below.

Show the numbers
strategyn marketstrades takenp value vs randomverdict
mean_reversion_v1382670.4978NO_EDGE
longshot_fade_v13821570.6007NO_EDGE
depth_imbalance_v13820NOT_APPLICABLENOT_APPLICABLE
regime_conditioned_v138240.6127NO_EDGE
What you're looking at: four strategies, one row each. The bar length is the p-value — how easily random chance alone could have produced a result this good — so a short bar would be a real signal, and all three of these are long. The dashed line is the usual threshold for taking a result seriously; nothing comes close to it. The fourth row has no bar at all, because that strategy refused to run on data that lacked the information it needed. n = 382 resolved markets · 2,000 clustered resamples, seed 17 · data 2026-05-31 · pre-registered · out-of-sample · strategies.csv

strategies/depth_imbalance.py — the abstention path

    snap = market_state.get("latest_snapshot")
    if snap is None:
        return _abstain("latest_snapshot missing from state")
    dimb = getattr(snap, "depth_imbalance", None)
    if dimb is None:
        return _abstain("L2 depth_imbalance not available (L1-only snapshot)")
    mid = float(getattr(snap, "mid", 0.0))
    if not (self.extreme_lo < mid < self.extreme_hi):
        return _abstain(
            f"mid {mid:.3f} is at extreme; depth imbalance there is "
            f"artifactual not informational")

Here L2 means the full list of resting buy and sell offers, and L1 only the single best price on each side. Three separate reasons to decline, and no branch that guesses. _abstain returns "no trade" with the reason attached, so the refusal is recorded rather than silent.

The technical version

A complete research stack for a prediction-market venue: continuous collection, a modelling layer, and a fee-aware backtester. Six scheduled collectors feed an audited panel of 400 resolved markets and 188,856 trades, licence-clean and hash-verified. A predecessor execution line on BTC used gradient boosting with isotonic calibration across 35 tested hypotheses; its recorded limitation is that win rate is flat across edge magnitude, so the model captures direction but not conviction.

Headline result — negative

All four candidate strategies scored NO_EDGE against a random baseline under a market-clustered bootstrap: mean_reversion_v1 p=0.498, longshot_fade_v1 p=0.601, regime_conditioned_v1 p=0.613. Verdict bands were fixed in advance: below 0.05 beats random, 0.05–0.20 weak, at or above 0.20 no edge.

depth_imbalance_v1 returned NOT_APPLICABLE: the historical dataset carries no L2 depth — bid and ask are null on every snapshot per the data audit — so its only signal is missing everywhere and it abstains universally. Reported as not applicable rather than faked with a tape-derived proxy.

One positive, with its caveat

A behavioural trader typology reached BEHAVIOR_PREDICTS_PNL on held-out profit and loss — Kruskal–Wallis p=7.7e-07, with 6 of 45 pairs surviving Bonferroni correction. Cluster stability was only MIXED: mean bootstrap adjusted Rand index 0.41. Reported with that caveat attached rather than on the strength of the p-value alone.

Real-time WebSocket pipelines, XGBoost with isotonic calibration, live inference and order execution, walk-forward backtesting with realistic fill simulation, Linux, systemd.