Repository navigation
Add prediction-market venue fee models (Kalshi, ForecastEx, Polymarket US) - #131
Merged
Merged
Conversation
…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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New module
ml4t.backtest.prediction_market_feeswith the published trading-fee schedules of three prediction-market venues, as vectorized functions for research and asCommissionModelimplementations for the engine.kalshi_fee,kalshi_order_feesKalshiCommissionforecastex_feeForecastExCommissionpolymarket_us_feePolymarketUSCommissionPrices are dollars per contract in
[0, 1]; buying NO atqis a purchase atq(the quadratic fees are symmetric). Functions accept scalars, NumPy arrays and Polars Series, including per-rowliquidity,side,fee_type,fee_multiplierandfee_coefficient, and return a float64 array.Fee rules
round_up(M * 0.07 * C * P * (1 - P)); maker 0 forquadratic, 0.25x forquadratic_with_maker_fees, 0.5x forquadratic_with_combo_maker_fees;flatraises (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_feesapplies the per-order accumulator that rebates overpayment in whole precision steps.theta * C * p * (1 - p)(default 0.0695; pass ml4t-data's per-marketfee_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, naiveround(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.mdis 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 withrun_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 plainnumpy.ndarray, becauseNDArray[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 timedengine.run()call.Checks run locally
ruff check,ruff format --check,ty check, fullpytest(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 theCompatibility Gatethat aggregates it fail ontests/contracts/test_correctness_evidence.py: pandas 3.1.0rc0 emitsPandas4Warning: Series.values returning an ndarray that drops timezone informationfrom inside exchange_calendars, andfilterwarnings = errorturns it into a failure. Dependabot PR #130 fails the same jobs. Reproduced locally with pandas 3.1.0rc0; a message-scopedignorefilter makes those tests pass, buttest_warning_policy_fails_new_warnings_and_has_no_global_suppressioncaps 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