Skip to content

Add prediction-market venue fee models (Kalshi, ForecastEx, Polymarket US) - #131

Merged
stefan-jansen merged 9 commits into
mainfrom
feat/prediction-market-venue-fees
Oct 8, 2026
Merged

stefan-jansen merged 9 commits into
mainfrom
feat/prediction-market-venue-fees

Conversation

@stefan-jansen

@stefan-jansen stefan-jansen commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What

New module ml4t.backtest.prediction_market_fees with the published trading-fee schedules of three prediction-market venues, as vectorized functions for research and as CommissionModel implementations for the engine.

Venue Vectorized Engine model
Kalshi kalshi_fee, kalshi_order_fees KalshiCommission
ForecastEx (via IBKR) forecastex_fee ForecastExCommission
Polymarket US polymarket_us_fee PolymarketUSCommission

Prices are dollars per contract in [0, 1]; buying NO at q is a purchase at q (the quadratic fees are symmetric). Functions accept scalars, NumPy arrays and Polars Series, including per-row liquidity, side, fee_type, fee_multiplier and fee_coefficient, and return a float64 array.

Fee rules

  • Kalshi: taker round_up(M * 0.07 * C * P * (1 - P)); maker 0 for quadratic, 0.25x for quadratic_with_maker_fees, 0.5x for quadratic_with_combo_maker_fees; flat raises (formula not published in machine-readable form). rounding="exact" is the trade fee rounded up to 0.000001; "cent" adds the non-direct-member balance alignment to 0.01 (1 contract at 0.50 pays 0.02, 100 contracts pay 1.75); "direct" aligns to 0.0001. kalshi_order_fees applies the per-order accumulator that rebates overpayment in whole precision steps.
  • ForecastEx: 0.01 per contract per side, no IBKR commission. Docstring flags the pre-2026-03-31 embedded-fee era and the block-trade waiver.
  • Polymarket US: taker theta * C * p * (1 - p) (default 0.0695; pass ml4t-data's per-market fee_coefficient), maker rebate -0.0125 * C * p * (1 - p) as a negative fee, optional per-fill cent rounding half to even. The engine model charges makers 0 because commissions must be non-negative; documented. Combo curve and volume rebates are not modeled.

Values that are exact in decimal (for example 0.07 * 100 * 0.25) are snapped before ceiling or rounding, so float noise cannot add a micro-dollar (1.750001) or move a half-cent (14.595 -> 14.59).

Sources (read 2026-10-08) are cited in each docstring: Kalshi fee schedule PDF, Kalshi OpenAPI FeeType, Kalshi fee-rounding page, ForecastEx CFTC filings, IBKR event-contract commissions, Polymarket US fee schedule.

Tests

tests/test_prediction_market_fees.py (49 tests): schedule values and published worked examples, decimal-arithmetic reference over the full cent price grid for Kalshi (all rounding modes, both sides) and Polymarket US, the per-order accumulator, input validation, protocol/deepcopy compliance, and an engine run that charges 0.02 and 1.75 on two fills.

Each of these planted defects makes at least one test fail: wrong taker rate, dropped series multiplier, maker charged on quadratic, wrong combo maker factor, trade fee rounded down, ceiling without float-noise snap, cent rounding per contract, no per-order rebate, naive round(x, 2), round-half-up, maker rebate sign dropped, ForecastEx half-penny fee.

Docs

User Guide section "Prediction-Market Venue Fees" in market-impact.md, API reference entry, regenerated compatibility snapshot for the two documented imports. CHANGELOG.md is generated and has not been updated by recent PRs, so it is untouched.

Retained validation evidence

The new module moves the engine tree digest that the retained parity evidence is pinned to, so the evidence is refreshed in this PR:

  • CORRECTNESS_RESULTS.json: re-derived with run_all_correctness.py; 66 pass, 2 unsupported, unchanged. Of 231,740 values, the 201 that differ are all digests, commits, durations or the timestamp.
  • REAL_STRATEGY_RESULTS.json: rebuilt from runs of all 17 required pairs; 17/17 pass. The 72 changed values are timings, output-manifest digests, platform, commit, engine digest and timestamp.
  • REAL_STRATEGY_PERFORMANCE.json: certified, not re-timed (the cycle ran twice: once more after switching the public return annotations to plain numpy.ndarray, because NDArray[np.float64] renders differently on the minimum and nightly NumPy and broke the compatibility snapshot). Nothing in the engine or the runners imports the fee module, so it cannot run inside the timed engine.run() call.

Checks run locally

ruff check, ruff format --check, ty check, full pytest (2261 passed, 14 skipped), pre-commit run --all-files, compatibility snapshot, changelog and parity-claim checks, documentation API audit, mkdocs build --strict, rendered link check, coverage gate.

Known failing check, not caused by this PR

Compatibility / Prerelease (Python 3.15) (3 OS) and the Compatibility Gate that aggregates it fail on tests/contracts/test_correctness_evidence.py: pandas 3.1.0rc0 emits Pandas4Warning: Series.values returning an ndarray that drops timezone information from inside exchange_calendars, and filterwarnings = error turns it into a failure. Dependabot PR #130 fails the same jobs. Reproduced locally with pandas 3.1.0rc0; a message-scoped ignore filter makes those tests pass, but test_warning_policy_fails_new_warnings_and_has_no_global_suppression caps the filter list at 5 and it already has 5, so the fix needs an owner decision and is left out of this PR.

🤖 Generated with Claude Code

…ymarket US)

Prediction-market research needs fee-adjusted returns at executable prices,
and backtests need the same fees inside the engine. Adds
ml4t.backtest.prediction_market_fees with:

- vectorized fee functions over NumPy arrays or Polars Series:
  kalshi_fee, kalshi_order_fees, forecastex_fee, polymarket_us_fee
- CommissionModel adapters: KalshiCommission, ForecastExCommission,
  PolymarketUSCommission

Kalshi: taker round_up(M * 0.07 * C * P * (1 - P)), maker factor 0 / 0.25 /
0.5 by series fee_type, trade fee rounded up to 0.000001, optional balance
alignment to 0.01 (non-direct) or 0.0001 (direct) and the per-order
rebate of accumulated overpayment. ForecastEx: 0.01 per contract per side.
Polymarket US: taker theta (per-market fee_coefficient, default 0.0695) and
maker rebate 0.0125, per-fill cent rounding half to even. Exact decimal
inputs are snapped before ceiling/rounding so float noise cannot move a fee
by a micro-dollar or a cent. Docstrings cite the venue documents read on
2026-10-08.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 18:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

stefan-jansen and others added 7 commits October 8, 2026 15:08
…ource

Adding prediction_market_fees.py moves the engine tree digest that
provenance.py stamps into every retained record, so all 68 read stale.
Re-run with validation/run_all_correctness.py from the clean tree at
59bf1c4: 66 pass, 2 unsupported, unchanged. Leaf-by-leaf against the
previous file: 231,740 values, 201 differ, all of them engine digests,
commits, durations or the generation timestamp. No correctness value moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ource

REAL_STRATEGY_RESULTS.json has a hard equality on the engine tree digest, which
the new prediction_market_fees.py module moves. Rebuilt all 17 required pairs
with build_real_strategy_evidence.py from the clean tree at a1649d9 and
compared them with real_strategy_evidence.py: 17/17 pass. Leaf-by-leaf against
the previous file, 72 of 1,061 values differ: engine timings, output-manifest
digests, platform string, commit, engine digest and generation timestamp. No
compared trading value moved.
…ource

The published engine timings were measured under an earlier engine tree. The
only source change is the new prediction_market_fees.py module, which nothing
in the engine or the real-strategy runners imports, so it cannot execute inside
the timed engine.run() call. Certified with real_strategy_benchmark.py
--certify-source against the rebuilt correctness evidence (17/17 pass); no
timing was re-measured.
…rs the same on every NumPy

NDArray[np.float64] expands differently across NumPy releases (NDArray[...] on
the locked version, ndarray[tuple[Any, ...], dtype[float64]] on the minimum and
nightly builds), so the compatibility snapshot failed on the Minimum
Dependencies and Python 3.15 jobs. Plain numpy.ndarray renders identically.
The ndarray annotation change moves the engine tree digest again. Re-run with
run_all_correctness.py from the clean tree at 16e7ffa: 66 pass, 2 unsupported,
unchanged; only digests, commits, durations and the timestamp differ.
Rebuilt all 17 required pairs with build_real_strategy_evidence.py from the
clean tree at e76e95d and compared with real_strategy_evidence.py: 17/17 pass.
…urce

Certified with real_strategy_benchmark.py --certify-source against the
rebuilt correctness evidence (17/17 pass). The only engine source change is the
new prediction_market_fees.py module, which nothing in the timed engine.run()
call imports; no timing was re-measured.
@stefan-jansen
stefan-jansen merged commit c1ee19c into main Oct 8, 2026
43 checks passed
@stefan-jansen
stefan-jansen deleted the feat/prediction-market-venue-fees branch October 8, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants