Kalshi history¶
The maker's record on Kalshi, filled only by Kalshi's real taker trades. Everything here is off by default: nothing changes until the Kalshi tape is synced and one of the commands below is run.
The data¶
markets/kalshi/ stores Kalshi's F1 markets in the tables Polymarket uses (market_links with
exchange = 'kalshi', market_trades, market_price_history). The cloud can't reach Kalshi, so the pull
runs on a machine that can:
racinglines markets --exchange kalshi sync --year 2025 --closed
racinglines markets --exchange kalshi sync --year 2026 --closed
racinglines markets --exchange kalshi trades --events KXF1RACE-... ...
racinglines markets --exchange kalshi history --events KXF1RACE-... --start ... --end ... --period 60
Archived Kalshi rows live in data/archive/markets/kalshi/ (store.root_for("kalshi")), apart from
Polymarket's; markets archive sends Kalshi's tokens there. One difference matters everywhere: a Kalshi link's condition_id is its event ticker,
shared by every driver's market of the event, so Kalshi code reads and groups the tape by market ticker
(token_id), never by condition.
The same tables and archive take the other sports (NASCAR Cup, MotoGP, IndyCar, and since PR #50 UCI road cycling, Le Mans and SailGP): synced only when named
(markets --exchange kalshi --sport nascar sync), under their own competitions, every link unmodeled, so the F1
replays below (which read F1's modeled links) never see them. NASCAR and MotoGP have their own taker replay
(racinglines strategy replay nascar|motogp, CLI); its results are in Sports and exchanges.
Data, CLI.
Sprint weekends¶
On by default since 2026-10-06 (owner); RACINGLINES_KALSHI_SPRINTS=0 in the sync's environment turns it off.
markets --exchange kalshi sync classifies the sprint winner (KXF1RACESPRINT-<GP>26, "Dutch Grand Prix: Sprint Race Winner") as
race_sprint_win and sprint pole (KXF1SPRINTPOLE-<GP>26, "Sprint Qualifying: Pole Position") as
race_sprint_pole, linked to the weekend's race and driver like the main-race markets, so model_prob
prices them: from extra.sprint_win_prob / sprint_pole_prob when a model run stores them, else from the
race win and pole probabilities (the sprint priced as the model's race, as the season forecast already does
for sprint points). They close when SQ / the Sprint starts (sports/f1.toml [stages.closes]) and settle
after SQ / after the Sprint. The sprint's fastest lap, top 5, top 10 and top constructor stay unmodeled.
At 0, the sync writes the pre-U5 rows (tests/test_kalshi.py, on the archived 2026 Dutch GP
links). Re-running the sync with sprints on re-classifies the existing rows in place; at 0 they go back.
The maker replay's kinds (maker_replay.MODELED) don't include the sprint kinds, so demo-history --venue
kalshi is unchanged.
Kalshi's historical feed vs Polymarket's¶
The full comparison (API, contract shape, grouping, trades and wallets, price history, books, fees, tick, coverage) is in Data; what was pulled is in Data changes.
What this means for the replays:
- The maker replay's market kinds (
maker_replay.MODELED) are win, podium, head-to-head and top constructor on both venues. Kalshi's top 10 and fastest lap are synced but not replayed. - The fill rule is the same on both. A taker trade after our quote, at or through our price, fills us up to the trade's size. On Polymarket a trade on either token is first turned into YES terms. Kalshi's tape is already in YES terms.
- The liquidity filter (
min_volume_24h) is per market on Kalshi. Summing bycondition_idwould count the whole event's volume toward each driver.
The maker's record on Kalshi¶
racinglines strategy demo-history f1 --venue kalshi [--reset]
This runs the same profiles as the Polymarket record (M1-M3 in 2025, C in 2026) and the same maker replay,
filled by Kalshi's real taker trades, with Kalshi's maker fee (maker_replay.KALSHI_MAKER_FEE: 0.0175 x C x P x (1 − P),
rounded up to the cent, per fill). The results are stored as paper_positions.venue = 'kalshi', with signals
flagged detail.venue = 'kalshi'. The Polymarket record, its --reset and live signals leave these rows
alone, and the reverse holds too.
Check (2026-09-28): Baku 2026's Polymarket tape, copied into Kalshi's shape (one ticker per question, one event ticker per kind), replays to the same 72 positions as the Polymarket replay. Only the maker fee differs (net cash $30.55 → $29.86).
First real run (2026-09-28)¶
On the pulled Kalshi tape (Data changes), in a cloud copy of the database, the demo maker ran its profiles over all 24 Grands Prix of 2025 (M1–M3) and the 15 of 2026 so far (C). The Polymarket record was run on the same copy for comparison, and matches Paper trading to the cent.
| Profile (weekends) | Kalshi | Kalshi, no maker fee | Polymarket |
|---|---|---|---|
| M1, 2025 R1–8 | +$521.61 (8) | +$545.73 | +$169.02 (7) |
| M2, 2025 R9–16 | −$646.13 (8) | −$553.26 | +$214.68 (8) |
| M3, 2025 R17–24 | +$317.91 (8) | +$373.74 | −$98.85 (8) |
| 2025 total | +$193.39 | +$366.20 | +$284.85 |
| C, 2026 R1–15 | −$118.84 (15) | −$54.45 | +$653.03 (15) |
"No maker fee" is the same replay with the fee set to zero. It isn't the with-fee P&L plus the fees: the fee changes the maker's cash, and so a few later quotes.
2025 on Kalshi is win and podium only (pole and fastest lap were listed for the last three races, and the profiles don't quote them). Kalshi opened its 2025 race markets only two to four days before each race, so the tape is thinner than 2026's (median trades per market: win 19, podium 26). The maker still filled in 93 of 481 win markets and 100 of 441 podium markets. By kind, without the fee: win +$356.27, podium +$9.94. Hungary 2025 (M2, −$508) is most of M2's loss.
2026, profile C¶
| 2026, profile C | Kalshi | Polymarket |
|---|---|---|
| Paper P&L, settled | −$118.84 (−$66.30 before $52.54 of maker fees) | +$653.03 (as in Paper trading) |
| Fills | 1,263 | 843 |
| Quoted markets filled: win | 75 of 87 (86%) | 64 of 74 (86%) |
| Quoted markets filled: podium | 71 of 80 (89%) | 46 of 62 (74%) |
| Quoted markets filled: top constructor | 10 of 17 | 5 of 8 |
| Quoted markets filled: head-to-head | 2 of 3 | 11 of 20 |
| P&L by kind: win / podium / constructor / h2h | −95.29 / −78.43 / +41.78 / +13.10 | +424.01 / +216.04 / +22.02 / −9.05 |
- Kalshi's tape fills the maker reliably. Most quoted win and podium markets fill, so the record needs no simulated takers. Head-to-heads are the exception, since Kalshi lists few of them.
- The same maker loses on Kalshi even before fees. Profile C was tuned on Polymarket's flow. On Kalshi it fills more often (podium: 647 fills vs 222) and does worse on those fills. That suggests Kalshi's takers are better informed against our quotes, or Kalshi's prices are closer to the result. Whether a Kalshi-tuned maker (spread, disagreement filter) does better is a question for a sweep, and nothing here tunes for it.
- Weekend by weekend (Kalshi, including fees): Australia −25, China +141, Japan +225, Miami −91, Canada +120, Monaco −50, Barcelona −301, Austria −237, Britain −209, Belgium +32, Hungary +137, Netherlands +37, Italy +165, Spain 0, Azerbaijan −62.
In the signal engine¶
A strategy profile paper-trades Kalshi when its settings say venue = "kalshi" (roadmap U1; the setting
is optional, unset by default and left out of every settings key, so every Polymarket profile and saved
sweep is untouched). Then racinglines strategy signals f1, f1 sweep --venue kalshi and the Lab's sweeps read
the race's exchange='kalshi' links, one market per ticker (never grouped by condition_id, the event
ticker), Kalshi's tape per market (its own 24 h volume), and the maker pays KALSHI_MAKER_FEE; the taker
strategies (taker_weekend) and the maker replay run unchanged on top. Signals carry
detail.venue = 'kalshi', positions venue = 'kalshi', apart from the Polymarket rows as the maker's
record above. Live, a Kalshi profile refreshes Kalshi's minute candlesticks and trades for the event
(markets/kalshi/sync.py) before pricing, as a Polymarket profile refreshes Polymarket's. No order is
ever sent and KALSHI_TRADING_ENABLED is never read by the engine.
Checks (2026-09-29, Azerbaijan 2026, scripts/signals_parity.py --venue kalshi; see
Paper trading): profile A's Kalshi replay through the engine
gives the sweep's 23 taker trades exactly; profile C's gives 41 fills and −$61.90 settled, the sweep
maker's figures and the −$61.90 Azerbaijan line of the record above, to the cent. Profile A on Polymarket
gives the same 11 trades before and after the change.
In the app¶
Kalshi is at the same level as Polymarket across the app, on by default (the owner's call, 2026-09-28).
RACINGLINES_KALSHI_VENUE=0 turns it off: nothing below appears, Kalshi shows as "soon" on the board and race
pages, its rows stay out of Positions, and /markets/kalshi is a 404.
| Page | With Kalshi on |
|---|---|
| Markets (board) | Kalshi as a live venue chip per race; the winner's Kalshi price next to Polymarket's in recent results; a link to the Kalshi list |
| Race and season pages | a Kalshi column with its bid–ask (open races) or its price as of our pricing (past races); the price-history tab plots Polymarket or Kalshi; a Kalshi mirror button and link |
/markets/kalshi |
every listed Kalshi event, as /markets/polymarket: outcomes, bid/ask/last, our fair, edge, a quote at ± spread/2, mirror into my book, refresh from Kalshi (read-only) |
| Positions | a Kalshi P&L tile (a filter, like the others), the Kalshi curve in the P&L history, ?venue=kalshi in the venue menu, Kalshi pills on rows and fills in the all-venues view, Coming up lists Kalshi's open markets with the maker's quotes |
| Strategy | a Polymarket / Kalshi switch; ?venue=kalshi shows the maker's Kalshi record, weekend by weekend, with its signals and positions |
| Lab | Event diagnostics get Kalshi fills / P&L columns (the same replay on Kalshi's tape, with its maker fee); a diagnostic's maker replay has an Exchange selector |
The Polymarket record, its pages and the private book render exactly as before with Kalshi off. A Kalshi
condition_id is the whole event, so the race page, the mirror and the Coming-up volume group Kalshi's rows by
market ticker (token_id), where Polymarket's group by condition.
Profile K¶
K · Kalshi maker is the maker tuned on Kalshi's own tape: the queue sweeps/kalshi-maker-k.toml
(racinglines strategy search f1, then search-report), every job with venue = "kalshi", so each run replays the
five maker strategies on Kalshi's markets and trades with its maker fee (f1 sweep --venue kalshi) against a
default-settings baseline on the same venue. Judged as params-4h was: 2026 the target, 2025 held out, maker
noise floor ±350, ranked by 2026 P&L less any 2025 loss.
The grid (2026-09-29, complete). Round 1 = model (gbm, gridq+pretrain) × half_spread (0.01–0.04)
× max_disagree (0.05, 0.07, 0.10, 0.15), 25-share quotes. Round 2 = quote size (10, and the default 50) and
the maker's per-market maker_min_volume_24h (50 / 200 / 400; unset = the replay's own $100) at 2¢ quotes, with
max_disagree 0.05 and 0.10. Every combo ran in both seasons: 104 jobs plus the two Kalshi baselines, 3 at a
time on 4 cores, about an hour. The top 3, and the partial grid's pick, were then re-run at 16,000 simulations in
both seasons against a 16k baseline. Sanity checks: the grid's gbm · max_disagree 0.05, size 25 (profile C) on
2026 is −118.84, the same as demo-history --venue kalshi above, and every combo the partial grid ran came out
to the cent.
Conservative maker, Kalshi, with the maker fee (P&L in $; 16k = the same combo at 16,000 simulations):
| Settings (rest default) | Label | 2026 | vs baseline | 2025 | vs baseline | 16k: 2026 / 2025 |
|---|---|---|---|---|---|---|
| gbm · max_disagree 0.10, size 25, min volume $400 (K) | robust, confirmed | +490 | +1,360 | +659 | +846 | +453 / +741 |
| gridq+pretrain · half_spread 0.04, size 25 | robust, confirmed | +710 | +1,581 | +202 | +389 | +649 / +47 |
| gbm · half_spread 0.04, max_disagree 0.10, size 25 | robust, confirmed | +469 | +1,340 | +477 | +664 | +219 / +537 |
| gbm · half_spread 0.03, max_disagree 0.10, size 25 (the partial grid's K) | robust, confirmed | +450 | +1,321 | +529 | +716 | +228 / +560 |
| gbm · max_disagree 0.10, size 25, min volume $200 | robust | +389 | +1,259 | +637 | +824 | |
| gbm · max_disagree 0.10, size 25 | robust | +223 | +1,094 | +623 | +810 | |
| gbm · max_disagree 0.10, size 10 | robust | +164 | +1,035 | +530 | +717 | |
| gbm · max_disagree 0.10 (size 50) | robust | −78 | +793 | +588 | +775 | |
| gbm · max_disagree 0.05, size 25 (C) | robust | −119 | +752 | +510 | +696 | |
| baseline (defaults) | −871 | −187 | −1,261 / −185 |
The pick. Under the held-out rule the top score at 4k simulations is gridq+pretrain · half_spread 0.04
(+710 in 2026), but its 2025 is thin (+202, +389 vs baseline, just over the noise floor) and at 16k it is +47,
+232 vs the 16k baseline, inside the noise floor. K is the next one down: gbm, 2¢ quotes, out of markets where
the model and price disagree by more than 10 points, and out of markets with less than $400 traded in the prior
24 hours. It holds up best at 16k in both seasons (+453 / +741, against +228 / +560 for the partial grid's pick),
and it has the best 2025 of the top four (Sharpe 1.07). K beats C on Kalshi by +609 in 2026 (beyond the noise
floor) and +150 in 2025 (within it).
2025 corrected (2026-09-29). The first version of this table read 2025 through Polymarket's markets for the
takers, so a weekend Polymarket didn't list was skipped for the Kalshi maker too: Emilia-Romagna 2025 (round 7),
where Kalshi did list the race. The sweep now reads the venue from its settings (venue = "kalshi": Kalshi's own
markets for every strategy), so that weekend is traded: K's 2025 moves from +855 to +659 (−195 at Imola), and
every 2026 number is unchanged. The ranking and the pick are the same.
What the grid says: gbm is the maker's model on Kalshi, as on Polymarket; gridq+pretrain is good in 2026 and
loses or barely wins in 2025 almost everywhere. A looser disagreement filter (10 points) beats C's 5 on Kalshi.
Staying out of thin markets (the $400 volume floor) does what the wider 3–4¢ quote did in the partial grid, with
less 16k slippage. Quote size matters less: 25 beats 10 and 50.
Caveats. 2026's Sharpe is modest (0.62) and its max drawdown ($499, $504 in 2025) is larger than C's on Polymarket. Without
its best weekend K's 2026 is −145, so the season's profit rests on a few weekends. One seed per combo (the noise
floor is params-4h's ±350, not measured here). Freezing K is the owner's call. signals.maker_call now reads a
profile's maker_min_volume_24h, so a live K quote uses the same $400 floor as the replay (unset = $100, as
before).
The full table of every combo × maker strategy, with the 16k columns, is in the project files
(roadmap-spinup/u3-kalshi-maker-k/).
Assigning K is an explicit step. racinglines strategy profiles f1 creates K as a Lab candidate
(params.venue = "kalshi"); racinglines strategy profiles f1 --assign-demo --venue kalshi stores it as the demo
maker's Kalshi profile (users.prefs["strategy_profile_kalshi"]), beside its Polymarket profile C, which it
leaves alone. Nothing reads that key until the signal engine runs Kalshi, so the deployed app doesn't change.
Cross-venue disagreement log¶
Roadmap U7. racinglines markets disagree --event … (CLI) keeps, for every F1
outcome linked on both Polymarket and Kalshi (a race's win, podium, top 10, pole, top-constructor and head-to-head
markets, and the season's drivers' and constructors' champion markets), one row per tick in market_disagreements
(racinglines/markets/disagree.py, migration c8e3f6a2d4b1):
| Column | Meaning |
|---|---|
pm_mid, pm_bid, pm_ask / kalshi_* |
Each venue's price and top of book at the tick, as the outcome's YES side (an inverted Polymarket link is flipped). The mid is the venue's last stored price at or before the tick, no older than 6 h; a book recorded within one step replaces it with (bid + ask) / 2 and gives the top of book. |
fair, run_id |
Our fair value from the run in force at the tick: for a race, the latest diagnostic or forecast with predictions for it, as of its cutoff; for the season, the latest forecast. Before the first run there is no fair and no edge (the gap columns still fill). |
pm_edge, pm_side / kalshi_edge, kalshi_side |
The better taker trade on that venue vs fair, net of its taker fee: buy YES at the ask (fair − ask − fee) or sell at the bid (bid − fair − fee); without a book, at the mid. Kalshi's taker fee is 0.07 × P × (1 − P) per contract (venue_replay.Kalshi.TAKER_FEE); Polymarket charges takers nothing on these markets. |
gap, gap_net |
kalshi_mid − pm_mid, and |gap| net of both taker fees at their mids: above 0, the venues disagree by more than it costs to take both sides. |
pm_vol24, kalshi_vol24 |
USD traded on each venue in the 24 h before the tick (market_trades; null where no tape is stored). A row is liquid when neither venue's tape shows nothing traded: Polymarket's stored history is a mid, and an empty book reads 0.5, so a gap on a dead market is on paper. The report and the panel count liquid rows. |
The backfill reads the archived prices, books and tapes (data/archive/markets/{polymarket,kalshi}/), so it runs
without exchange access; the window defaults to where both venues have prices, on an hourly grid, and for a race ends
at the race start (after it the venues settle at different speeds). With RACINGLINES_DISAGREE=1 the recorder writes
one tick per pass from the latest stored quotes, and the Markets page shows the log's latest tick
(Web app). A price jump inside one step shows as a one-tick gap (Kalshi's hourly candle closes
at the tick, Polymarket's sample can be a few minutes older), so read the share of rows above fees and the median,
not the maximum.
2026 championship markets (33 outcomes, 2026-01-12 to 2026-09-27, hourly): 141,220 rows, 81,919 liquid (Kalshi's tape is stored; Polymarket's champion tape isn't, so the Polymarket side is taken as liquid), 65,250 of them above fees (80%). The typical net gap is small, 0.3 to 0.6 points, so most "above fees" rows are a mid-to-mid difference of a cent or two on low-priced outcomes, where Kalshi's fee at P(1 − P) is almost nothing; the widest liquid gaps are one-tick jumps on race days (Antonelli's championship, 2026-05-24 21:00: Polymarket 0.357 vs Kalshi 0.640). Our fair exists only from the 2026-09-27 forecast on, which the stored prices end before, so this backfill carries no edge columns; the recorder's ticks will. Day by day, the report is in the pull request that added it.
Azerbaijan 2026 (round 15), 55 outcomes, 2026-09-22 to the race start on the 26th: 4,922 rows, 1,879 liquid, 1,221 above fees (65%; median net +0.3 points). The largest liquid gaps are Polymarket's top-constructor and long-shot podium markets reading ~0.5 (a one-sided book) against Kalshi's 0.5 to 5 cents, which a recorded book would have shown as a spread, not a price.
Taker prices (2026-10-05). A Kalshi taker in the sweep, the Lab and live signals buys at the ask and sells at the bid: the recorded order book when a snapshot is at most 10 minutes old, else the hourly candle's closing quote (venue_replay.Kalshi.quote), else the last trade ± cost as before. Taker results on this page from before that date used the last trade. See the F1 roadmap decision log; racinglines markets --exchange kalshi coverage shows how much stored history carries a quote.
First taker results at the touch (2026-10-05)¶
The first Kalshi taker sweep worth reading: before bid/ask candles were stored, the taker replay traded at the last
trade and wasn't usable. Run on the VM at 7ba8fd0 (PR #55's merge) with racinglines strategy sweep f1 --year <year>
--venue kalshi --no-fetch, baseline settings, nothing saved.
Stored history behind it (racinglines markets --exchange kalshi coverage): 3,855 markets, 600,409 price rows from
2025-02-12 to 2026-10-05, 319,700 with both bid and ask. 12,390 of those (3.9%) have a last trade outside the
closing quote, 745 by more than 5¢.
| Season | update |
hold |
last |
early |
maker (baseline) |
|---|---|---|---|---|---|
| 2026, 15 weekends | +$1,391.65 ($9,842, 5 up) | −$162.45 ($6,425, 7 up) | +$523.32 ($1,921, 4 up) | +$1,602.06 ($8,013, 6 up) | −$870.68 ($12,394, 7 up) |
| 2025, 24 weekends | +$1,167.41 ($7,224, 6 up) | +$847.29 ($5,225, 8 up) | +$1,823.86 ($1,938, 5 up) | −$1,191.17 ($5,460, 4 up) | −$186.79 ($13,262, 9 up) |
P&L (amount traded, weekends up). Maker variants: 2026 flat −$866.48, skew −$834.91, widen −$608.82, all −$585.38; 2025 flat −$1,109.88, skew +$59.71, widen −$80.03, all −$727.25. The maker fills on Kalshi's tape, so PR #55 doesn't change these.
What this does and doesn't show.
updateis the only taker mode up in both seasons, and by more than the ±$400 noise floor measured for it on Polymarket.earlyandholdflip sign between seasons, so they say nothing yet.- The wins are concentrated:
updatewas up on 5 of 15 weekends in 2026 and 6 of 24 in 2025. A couple of weekends carry most of the result. The per-weekend tables are in the run's journal, not on this page. - These are baseline settings at the default simulation count, not profile A at 16k, so they aren't comparable with the Polymarket taker figures (A: +$1,389 / +$1,432 at 16k).
- Every fill here is at a candle's closing quote. Recorded books start on 2026-09-30, after round 15, so no weekend in either sweep used one. An hourly close can be up to an hour older than the stage it fills.
Profile A on Kalshi at 16k (2026-10-05)¶
Profile A's own settings (--variant gridq+pretrain+reset, --min-edge 0.10 --min-edge-h2h 0.05, live stages only,
--sims 16000), as behind its Polymarket figures, on Kalshi at the touch. Run on the VM at 38591d8, --no-fetch,
nothing saved; logs and CSVs in data/runs/f1/sweeps/kalshi-a16k-<year>* on the VM (backup before it:
racinglines-before-kalshi-profile-a-16k-20261005T223203Z.sql.gz, since priced stages are stored).
| Season | update (A) |
Without the best weekend | Without the best two | Polymarket, A at 16k |
|---|---|---|---|---|
| 2026 | +$2,332.44 on $5,972, 8 of 16 up | +$930.98 | −$117.18 | +$1,389 |
| 2025 | +$1,256.67 on $5,010, 5 of 24 up | −$67.76 | −$1,024.76 | +$1,432 |
Best weekends: 2026 Australia +$1,401.46 (on $290 traded) and Britain +$1,048.16; 2025 the Netherlands +$1,324.43
and Azerbaijan +$957.00. By market, podium carries it in both seasons (+$1,607.67 / +$1,638.51); win is +$458.13 in
2026 and −$340.73 in 2025. The other modes: hold +$1,725.96 / +$1,084.90, last +$926.40 / +$1,955.95, early
+$1,677.89 / −$671.70. 2025 rounds 1–4 have nothing tradeable on Kalshi.
Verdict: no Kalshi taker edge shown yet. The totals beat A on Polymarket in 2026, but in both seasons two weekends make more than the whole result, and the rest lose money. That is a few long-shot podium hits, not a steady edge at the touch. Nothing changes: no Kalshi taker profile, and A stays a Polymarket profile.