Deterministic Rust (tokio) Polymarket sports edge engine with independent probability models.
Early-stage open-source project focused on reproducible market evaluation and candidate order generation for Polymarket sports markets.
This repository is opinionated about three things:
- deterministic model output for the same input state
- explicit JSON contracts for downstream tooling
- risk-aware candidate generation instead of unconstrained trade chasing
Most prediction-market tooling either copies market prices, hides model logic behind opaque services, or mixes research and execution state in ways that are hard to audit. pm_edge_engine is meant to be a transparent baseline that keeps pricing, calibration, mapping, and order filtering inspectable.
- Independent probabilities (no LLM, no price->prob copying)
- Data ingestion:
- Polymarket Gamma API (
/markets,/markets/slug/{slug}) - football-data.org v4 matches/results
- OpenLigaDB public fallback for supported competitions when
FOOTBALL_DATA_TOKENis absent - TheSportsDB targeted event lookup for missing match mappings in selected leagues
- Polymarket Gamma API (
- SQLite cache with WAL mode
- Models:
- ELO baseline (time decay)
- League Poisson attack/defense model (weighted MLE-like optimization)
- Hybrid blend (V1 default 0.55 Poisson + 0.45 ELO)
- V2 upgrades:
- Odds fusion plugin interface (
odds_provider.rs) - Calibration (
isotonic/plattincalibration.rs) - Match confidence gate in market mapping
- Dynamic cost model and dynamic min-edge in order engine
- League-wise Poisson auto-degrade (<800 matches => ELO-only)
- Odds fusion plugin interface (
This repository does not claim guaranteed profitability. It is an execution-support engine with explicit filters and conservative defaults.
- Invalid market state should resolve to no action.
- Model confidence and market liquidity gates are enforced before order generation.
- Near-event and high-cost setups are filtered out.
- Generated orders are candidate outputs; production deployment still requires separate operational controls, monitoring, and secrets handling.
- Rust stable
- Optional environment variable:
FOOTBALL_DATA_TOKEN(used as the primary football-data source when present)
Without FOOTBALL_DATA_TOKEN, fetch can fall back to OpenLigaDB for supported competitions such as Bundesliga and selected UEFA competitions.
cargo build
cp config.toml.example config.tomlcargo run -- fetchcargo run -- traincargo run -- predict --markets_file examples/markets_input.json > fair_probs.jsoncargo run -- candidates --markets_file examples/markets_input.json --equity_usd 50 > orders.jsoncargo run -- shadow --markets_file examples/markets_input_wait.json --equity_usd 100This prints a machine-readable JSON report containing:
- candidate decisions and reason codes
- any generated orders
- resolved shadow PnL for orders whose mapped match already has a final score
- summary metrics such as
buy_count,settled_orders,total_pnl_usd, androi_pct
cargo run -- diagnose --markets_file examples/demo_real_market_input.json > mapping_diagnostics.jsonTo generate a paste-ready GitHub issue body instead:
cargo run -- diagnose --markets_file examples/demo_real_market_input.json --issue-body > mapping_issue.mdThis prints a machine-readable JSON report containing:
- mapping state for each market
- reason codes, match confidence, and match references
- summary counts for mapped, unmapped, remote-lookup, and no-match rows
Use this when filing the mapping-miss report or when you need a compact view of why a market did not map cleanly.
cargo run -- backtest --snapshots_file examples/backtest_input_inline.json --equity_usd 200or use a manifest that references many snapshot files:
cargo run -- backtest --snapshots_file examples/backtest_manifest.json --equity_usd 200or run the tail-window replay example with local odds snapshots:
cargo run -- backtest --snapshots_file examples/backtest_tail_manifest.json --equity_usd 200or run the 5/10/15-minute tail replay example with replay-only overrides:
cargo run -- backtest --snapshots_file examples/backtest_tail_515_manifest.json --equity_usd 200or build a manifest from live archived sports-tail snapshots and replay that:
cargo run -- tail-manifest \
--snapshots_dir ../polymarket-bot/state/pm_edge_tail_history/snapshots \
--manifest_out ../polymarket-bot/state/pm_edge_tail_history/manifests/latest_tail.json \
--from_utc 2026-03-01 \
--to_utc 2026-03-12
cargo run -- backtest --snapshots_file ../polymarket-bot/state/pm_edge_tail_history/manifests/latest_tail.json --equity_usd 200This prints a machine-readable JSON report containing:
- per-snapshot counts for
BUY/WAIT, newly entered orders, and settled trades - a trade ledger with entry price, size, mapped match, and realized PnL
- breakdowns by entry date and by league with realized PnL and hit rate
- a
by_minutes_to_startsection for pre-kickoff bucket analysis (5-9,10-14,15-29, etc.) - bankroll summary metrics such as
trades_entered,settled_trades,total_pnl_usd,roi_pct, andmax_drawdown_usd
Usage notes:
backtestaccepts a self-contained file withmatchesplussnapshots, or a snapshots-only file that reuses your local sqlite match cachebacktestalso accepts a manifest withmatches_filesandsnapshot_files; relative paths resolve from the manifest file locationbacktestaccepts optionaltail_windowfilters so you can restrict replay to late-market samples before kickoffbacktestaccepts optional inlineoddsor manifestodds_files; replay uses the freshest odds snapshot at or before eachas_of_utcbacktestaccepts optional replay-onlyoverridesso you can relax timing or model gates for controlled experiments without changingconfig.toml- if replay ends with open trades but the mapped match result is already known,
backtestnow settles them at the end of the run instead of leaving them artificiallyOPEN tail-manifestscans an archive directory of one-snapshot JSON files and writes a standardbacktestmanifestpolymarket-bot/scripts/live/clawx_signal_pm_edge.shnow auto-archives sports-tail snapshots intostate/pm_edge_tail_history/snapshots/when that signal path runs- with the default
max_single_trade_equity_pct=0.0075and a1 USDminimum order size, bankrolls below about133.34 USDwill naturally produce no trades
cargo run -- runScheduler behavior:
- every 15 minutes: refresh Polymarket markets
- every 60 minutes: refresh football-data or public fallback + retrain
- after refresh: writes
fair_probs.json+orders.json(if enabled)
- Fetch market and match data.
- Train or refresh league models.
- Map Polymarket markets to football fixtures.
- Produce fair probabilities.
- Generate candidate orders only when edge, confidence, liquidity, and timing filters pass.
pm_edge_engine fetchpm_edge_engine trainpm_edge_engine predict --markets_file input.jsonpm_edge_engine candidates --markets_file input.jsonpm_edge_engine shadow --markets_file input.jsonpm_edge_engine diagnose --markets_file input.jsonpm_edge_engine diagnose --markets_file input.json --issue-bodypm_edge_engine backtest --snapshots_file input.jsonpm_edge_engine tail-manifest --snapshots_dir dir --manifest_out filepm_edge_engine run
Either:
[
{
"market_slug": "...",
"question": "...",
"outcomes": ["Yes", "No"],
"prices": [0.47, 0.53],
"best_bid": 0.46,
"best_ask": 0.48,
"spread": 0.02,
"liquidity": 5000,
"volume": 12000,
"volume_5m": 900,
"start_time_utc": "2026-02-18T18:00:00Z",
"event_title": "Team A vs Team B",
"event_slug": "...",
"event_home_team": "Team A",
"event_away_team": "Team B",
"league_hint": "PL",
"active": true,
"closed": false,
"accepting_orders": true
}
]or:
{ "markets": [ ... ] }backtest accepts:
{
"matches": [
{
"id": "fixture-match",
"league": "PL",
"season": "2026",
"datetime_utc": "2026-02-18T18:00:00Z",
"home_team": "Team A",
"away_team": "Team B",
"home_goals": 2,
"away_goals": 0,
"status": "FINISHED"
}
],
"snapshots": [
{
"as_of_utc": "2026-02-18T12:00:00Z",
"markets": [ ... ]
}
]
}Notes:
matchesis optional; when omitted,backtestuses the local sqlite cache for historical match rowsoddsis optional; when present, rows are matched by league/team names and event time, then filtered to snapshots fetched at or beforeas_of_utctail_windowis optional and can restrict replay to markets whosestart_time_utcfalls within a chosen minute rangesnapshots[*].as_of_utcanchors both model training decay and execution filters to that point in timemarketsuses the sameMarketRecordshape aspredictandcandidates
It also accepts a manifest:
{
"tail_window": {
"min_minutes_to_start": 5,
"max_minutes_to_start": 15,
"require_start_time": true
},
"overrides": {
"engine": {
"min_time_to_event_minutes": 5
},
"model": {
"poisson_min_matches": 200
}
},
"matches_files": ["backtest_batch/matches.json"],
"odds_files": ["backtest_tail_batch/odds.json"],
"snapshot_files": [
"backtest_batch/2026-02-18T12-00-00Z.json",
"backtest_batch/2026-02-18T22-00-00Z.json"
]
}Manifest notes:
matches_filesandsnapshot_filesare optional lists that are merged with any inlinematches/snapshotsodds_filesis an optional list merged with any inlineoddsoverridesis optional and only affects the replay process; it does not modify your persistent runtime config- match files may be either a raw array or
{ "matches": [...] } - odds files may be either a raw array or
{ "odds": [...] } - snapshot files may be either a single
{ "as_of_utc": ..., "markets": [...] }object or{ "snapshots": [...] } - relative paths resolve from the manifest file's directory
{"results":[{"market_slug":"...","fair_probs":[0.5,0.5]}]}{"orders":[{"market_slug":"...","side":"BUY","outcome_index":0,"limit_price":0.42,"size_usd":5.0,"order_type":"maker"}]}Schema and compatibility notes:
- See docs/JSON_CONTRACT.md for schema/versioning expectations.
- Machine-readable reference schemas live under
schemas/. - Consumers should join on
market_slugand treat example numeric values as illustrative, not frozen snapshots.
See examples/README.md for:
- minimal and extended market input payloads
- annotated notes for the extended example
- a deterministic WAIT fixture and empty-order example
- a self-contained backtest snapshot example
- a batch backtest manifest example
- a tail-window backtest manifest with local odds snapshots
- example fair-probability and order outputs
- copy-paste commands for local prediction and candidate generation
- JSON schema references for downstream tooling
See docs/DEMO.md for a short walkthrough with captured CLI outputs from a real-team sample payload.
See config.toml.example.
Runtime env overrides:
PM_EDGE_CONFIGPM_EDGE_DB_PATHFOOTBALL_DATA_TOKENFOOTBALL_COMPETITIONSPM_EDGE_PUBLIC_FOOTBALL_FALLBACK_ENABLEDPM_EDGE_SPORTSDB_LOOKUP_ENABLEDPM_EDGE_BASE_MIN_EDGEPM_EDGE_MIN_MATCH_CONFIDENCEPM_EDGE_ODDS_ENABLED
- No API keys are hardcoded.
- Model output is deterministic for the same input state.
- If
FOOTBALL_DATA_TOKENis missing and public fallback is enabled,fetchuses OpenLigaDB for supported competition codes (BL1,CL/UCL,EL/UEL, best-effortPL). - Unsupported fallback competition codes remain explicit skips, not silent substitutions.
- If a market still fails local match mapping,
predict/candidatescan query TheSportsDB by event name for supported leagues (PL,PD,SA,FL1,BL1) before falling back toNO_MATCH_MAPPING. - TheSportsDB is used here as a low-volume mapping repair path, not as a bulk historical training source.
- If
FOOTBALL_DATA_TOKENis missing and public fallback is disabled,fetchskips football ingestion without crashing.
Run the local validation loop:
cargo fmt --all
cargo check --all-targets
cargo test --all-targetsCI runs the same checks on pushes to main and on pull requests.
The test suite now includes example-driven fixture coverage for both a mapped predict flow and a deterministic WAIT candidate path.
Dependabot tracks Cargo and GitHub Actions updates weekly, and CodeQL runs on pushes, pull requests, and a scheduled scan.
- Expand unit and fixture-based test coverage across market mapping and calibration flows.
- Add more examples for input preparation and output interpretation.
- Add release notes and tagged versions as the CLI and JSON contracts stabilize.
- Broaden odds-provider integrations while keeping deterministic fallbacks.
Open roadmap issues:
- #1 Expand fixture-based end-to-end tests
- #2 Add deterministic odds-provider fixtures
- #3 Expand public examples and schema notes
- #4 Broaden market mapping coverage
Current milestone:
Good ways to contribute right now:
- Mapping miss report guide
- #2 Add deterministic odds-provider fixtures
- #4 Broaden market mapping coverage
- #5 Looking for sample markets and mapping misses
- Q&A discussion
See CONTRIBUTING.md.
Project policies:
Use the GitHub issue templates for bugs and feature requests. Include repro steps, example payloads, and the commit or release you tested against.
For mapping misses, use the dedicated mapping-miss template and include the expected fixture plus the raw market payload.
If you have a local payload, run cargo run -- diagnose --markets_file <file> --issue-body and paste the Markdown into the issue body. Use --issue-body only when you want a ready-to-edit report; otherwise attach the raw JSON diagnostics.
Discussion entry points:
- Announcements / feedback thread
- Q&A for setup and output interpretation
- Feedback issue for sample markets and mapping misses
- Mapping miss report guide
If you want to share the project externally, see docs/OUTREACH.md for ready-to-post copy. If you need to integrate the CLI into another tool, start with docs/JSON_CONTRACT.md.
See CHANGELOG.md.
MIT