Advanced Kalshi Rules
Kalshi custom strategies can extend a rule in three ways. Composite orders rest limit buys priced from the live order book. Per-fill exits manage each purchase at its own price. Rule-level schedules give one entry rule its own trading hours. This section covers every key, how it behaves live, in paper trading and in backtests, and the combinations the validator refuses.
The section is split into the pages listed under In this section: one page per feature, plus a page of complete example strategies. This page covers what applies to all three features: where the keys work and which ones can be combined, rule order, save-time and deploy checks, validator messages, and how each feature runs in paper trading and backtests.
Everything here builds on the rule model in Custom Rules: every loop, rules run top to bottom and the first rule that matches acts.
Note: The examples use small, illustrative sizes, prices and thresholds to show how each key works. They are not recommendations, and nothing in this section suggests a strategy will make money. Backtest and paper trade any strategy before you deploy it live, and keep first deployments small.
In this section
- Composite Orders and max_combined_price: Composite
ordersthat rest one or two limit buys priced from the live book: how legs are priced and sized, what one firing does, keeping a quote in the queue, themax_combined_pricecap for paired YES/NO quotes, and Deep Research sweeps for paired quotes. - Per-Fill Profit Targets and Stops: Per-fill exits on
sell_yes/sell_norules: how fills become lots,entry_profit_offset,entry_stop_loss_offset,entry_profit_targetstables,entry_profit_min_time_to_expiry, and when the Bot blocks trading because it can't trust its lots. - Rule-Level Trading Schedules: A
trading_scheduleon individualbuy_yes/buy_norules for time-varying entry thresholds: how it combines with the top-level schedule, orders at window edges, overnight and overlapping windows, and format rules. - Advanced Kalshi Rule Examples: Complete strategies that pass Studio's save checks: a paired YES/NO quote with a combined cap, a NO bid under the ask, per-fill targets and stops, an entry-price profit table, time-varying entry thresholds and one target table for YES and NO fills. The last example shows a combination the validator rejects.
Overview
| Feature | Keys | Goes on | Orders it sends |
|---|---|---|---|
| Composite orders | orders, max_combined_price | A rule with no action | Resting limit buys (maker), priced from the book |
| Per-fill exits | entry_profit_offset, entry_stop_loss_offset, entry_profit_targets, entry_profit_min_time_to_expiry | sell_yes or sell_no rules | Immediate-or-cancel, reduce-only sells (taker) |
| Rule-level schedules | trading_schedule inside a rule | buy_yes or buy_no rules | No new orders: limits when that rule may buy |
All of them need platform: kalshi and strategy: custom.
Where advanced rules run
By run mode
| Feature | Live | Paper | Historical backtest |
|---|---|---|---|
| Composite orders and max_combined_price | YesResting maker buys | YesA resting leg fills once the opposite quote moves through it | Maker replayKalshi only; refused with risk.max_notional |
| Per-fill exits | YesImmediate-or-cancel sells, checked each loop | YesEach exit fills in full or not at all | Taker onlyFlat starting account required |
| Rule-level trading_schedule | Yesbuy_yes and buy_no rules | YesIncluding exchange-side expiry | Taker backtests |
By strategy type
| Feature | Kalshi custom | Kalshi or Polymarket built-in | Polymarket custom, any Polymarket US strategy | Kalshi Perps, Arbitrage |
|---|---|---|---|---|
| Composite orders and max_combined_price | Yes | No live effectNot checked at save, but changes the backtest (a Polymarket backtest fails) | Rejected at saveA Polymarket US built-in rejects any rules block | IgnoredNo error and no effect |
| Per-fill exits | Yes | Rejected at save | Rejected at save | IgnoredNo error and no effect |
| Rule-level trading_schedule | Yes | Rejected at save | Rejected at save | IgnoredNo error and no effect |
platform: kalshi and strategy: custom. Composite orders can't share a strategy with per-fill exits or rule-level schedules; a top-level trading_schedule is fine.Where these keys don't work
- Custom Polymarket and Polymarket US strategies reject them when you save.
- Built-in strategies such as
spread_capturereject per-fill exits and rule-level schedules. A Polymarket US built-in rejects anyrulesblock, so composite orders fail at save there too. Kalshi and Polymarket built-ins don't checkordersormax_combined_price, but those keys never trade live on a built-in strategy. Leave them out: a strayordersblock still changes the backtest (a Kalshi backtest switches to the maker replay, and a Polymarket backtest fails). - Kalshi Perps and Arbitrage strategies ignore these keys without an error. Perps has its own exits; see Kalshi Perps.
What can be combined
Advanced strategies come in two shapes that don't mix. A quoting strategy rests composite orders and exits whole positions with sell_all, pulling quotes with cancel_all. A taking strategy enters with buy_yes/buy_no, and can add per-fill exits and rule-level schedules. The validator enforces the split:
| If you write | You get |
|---|---|
Both action and orders on one rule, or neither | "set exactly one of action or orders". orders: [] counts as no orders. |
orders anywhere plus any per-fill exit key anywhere | "entry-relative exits do not support composite maker orders" |
orders anywhere plus a rule-level trading_schedule anywhere | "cannot be combined with composite order intents". A top-level trading_schedule is allowed with orders. |
A rule-level trading_schedule on a rule that isn't buy_yes or buy_no | "supported only on Kalshi custom buy_yes/buy_no rules" |
A rule-level trading_schedule plus a top-level active_window | "cannot be combined with active_window" |
entry_profit_offset and entry_stop_loss_offset on one rule | "use separate rules for entry_profit_offset and entry_stop_loss_offset" |
entry_profit_targets plus either offset on one rule | "use separate rules for table targets and offsets" |
A per-fill exit with size, or on an action other than sell_yes/sell_no | "requires sell_yes or sell_no without size or orders; quantity comes from each fill" |
max_combined_price without exactly one YES and one NO leg | "requires exactly one YES and one NO order" |
entry_profit_min_time_to_expiry without entry_profit_targets on the same rule | "requires a positive whole-number s, m, or h duration and entry_profit_targets" |
Rule order and requirements
First match wins, so the order of rules decides what happens when several could act. A reliable order, top to bottom:
- Safety exits. Whole-position
sell_allorcancel_allrules for conditions where you want out regardless, such as close to expiry or around a scheduled announcement. - Per-fill stops.
entry_stop_loss_offsetrules. - Per-fill profit targets.
entry_profit_offsetandentry_profit_targetsrules. - Entries.
buy_yes/buy_norules, with or without their own schedules, or a composite quote rule. - Fallback cancel. A
cancel_allthat clears resting orders once the entry conditions stop holding.
Exits sit above entries so an exit always wins over new risk in the same loop. Per-fill rules fall through when no lot qualifies (how a per-fill rule matches), so they don't block the entries below them. A scheduled entry falls through when its window is closed (how the two schedules combine).
rules:
- name: exit_all_near_close
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: time_to_expiry, op: "<=", value: "10m"}
action: sell_all
- name: stop_each_fill
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_stop_loss_offset: 0.10
- name: take_profit_each_fill
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_profit_offset: 0.05
- name: enter_yes
when:
all:
- {field: order_count, op: "==", value: 0}
- {field: yes_best_ask, op: "<=", value: 0.45}
- {field: yes_position_size, op: "<", value: 3}
action: buy_yes
size: 1
- name: cancel_unfilled_entry
when:
all:
- {field: order_count, op: ">", value: 0}
action: cancel_allTwo limits on the fallback. First, a composite rule that stops with an error skips the rest of that market's rules for the loop, fallback included. Errors include a missing book side, a post-only cross, a leg Kalshi rejects, and a leg refused by a deployment risk limit or your buying power; see What one firing does. Second, a matched sell_all doesn't cancel resting orders, so pair it with a cancel_all where needed.
Save-time behavior checks
Studio's behavior checks treat advanced rules differently:
- A
buy_yesorbuy_norule with its owntrading_scheduleis exempt from "entry predicate is exhaustive". An entry whose conditions always match is allowed when its window limits it. - Rules with a rule-level schedule or a per-fill exit key never make the rules below them unreachable. Per-fill rules also don't count toward "exit rules collectively match every held-position state" or "exit predicate fires in profitable, neutral, and losing states". That's why a per-fill rule gated only on
yes_position_size > 0saves. - An earlier rule without a schedule or per-fill key can still make a scheduled or per-fill rule below it unreachable.
- Composite
ordersrules aren't checked as entries, so a quote rule whose conditions always match still saves. A composite rule can still make the rules below it unreachable, and it never counts as the required exit rule.
Before you deploy
- Start flat in every market a per-fill strategy may trade, with no resting orders there. How fills become lots explains why.
- Keep those markets to this Bot. No manual trades and no other Bot on the same account and market.
- Check the deploy dialog limits. Studio prefills Max contracts per order from your largest buy or leg (see Leg
size). Make sure Max open contracts andmax_positionboth cover held + resting + new legs, and remember per-fill exits sell at most Max contracts per order per loop. Deploys through MCP size their defaults from the strategy the same way. - Include an exit rule. Every custom strategy needs at least one
sell_all,cancel_all,sell_yesorsell_norule. - Mind
max_entries_per_marketwith pairs. Each leg uses one entry, so a paired quote needs at least 2.
Common validator messages
Field-level mistakes in composite legs, per-fill exits and rule schedules, and the message each one gets when you save. For combinations the validator refuses, see What can be combined. Mistakes specific to max_combined_price are listed on the composite orders page.
| Mistake | Validator message |
|---|---|
side: YES, or anything other than yes/no | "side must be yes or no" |
action: sell in a leg | "only buy order intents are currently supported" |
| Two YES legs on one rule | "duplicate order intent for yes/buy" |
size: 0 (or 0.9, which is cut to 0) | "must be > 0" |
reference: midpoint, or no reference | "unknown price reference" |
| An offset of 1 or more, or -1 or less | "offset must be between -1 and 1 dollars" |
price: 0.45, or an extra key inside price | A YAML error, or "unsupported price key" |
| A per-fill offset of 0, a negative value, or 1 or more | "must be finite and greater than 0 and less than 1 dollars" |
entry_profit_targets: [] | "must be a nonempty list of entry_price and target_price rows" |
| An off-cent, duplicate, or not-higher target row | "requires unique entry prices and higher target prices on the 1-cent grid between 0 and 1 dollars" |
timezone: EST, or trading_schedule: {} | "timezone must be an IANA name such as America/Chicago, or UTC" |
trading_hours: [] with no other window list | "provide between 1 and 32 windows" |
trading_hours: [] next to blackouts | "omit unused window lists instead of providing an empty list" |
start equal to end, or "24:00" as a start | "use distinct HH:MM start/end times; 24:00 is allowed only as end" |
| A repeated day | "days must be unique mon through sun" |
Backtests and paper trading
All three features run in paper trading. In backtests, each one needs a specific mode:
| Feature | Paper trading | Historical backtest |
|---|---|---|
Composite orders and max_combined_price | Yes. A resting leg fills in full at its limit once the live opposite best quote moves strictly through it. The post-only check behaves as live. | Kalshi maker replay only. Refused when the strategy sets risk.max_notional ("risk.max_notional enforcement is not simulated for maker backtests yet"). |
| Per-fill exits | Yes. Each exit fills in full or not at all. | Taker backtests only. The starting account must be flat. |
Rule-level trading_schedule | Yes, including exchange-side expiry | Taker backtests. A pending entry expires at the next window edge. |
Maker replay versus live
Any rule with orders sends the whole strategy to the Kalshi maker replay, the backtest mode for resting orders. It models your place in the queue from the historical book and trades. When complete historical market data isn't available, it falls back to an approximate model that assumes you're at the front of the queue, which is optimistic. Read replay results with these differences in mind:
- Queue position. The replay keeps an unchanged quote in place instead of cancelling and re-sending it every loop, so it can show queue priority a live quote rule without an
order_countgate never gets. - Only three kinds of rule act. Rules with
orders,cancel_allandsell_alldo something in the replay. A matchedbuy_yes,buy_no,sell_yes,sell_noorskiprule does nothing there but still ends evaluation for that moment. So asell_yesstop in a quoting strategy works live but does nothing in its backtest, and it also blocks the quote rule below it while it matches. Usesell_allandcancel_allfor exits in quoting strategies you want to backtest. sell_allcancels quotes in the replay, but not live. Add an explicitcancel_allso live behavior matches.- Limits count differently. The replay checks
risk.max_positionper outcome as each order arrives, and countsmax_entries_per_marketby filled orders only. Live counts resting orders too. - Prices use a finer grid than whole cents, so sub-cent offsets price differently.
- Post-only crosses are rejected when the order arrives, after modeled latency, instead of stopping the rule beforehand. A leg that isn't post-only and would cross also rests at its limit instead of trading immediately.
Per-fill exits in backtests
Per-fill exits run in taker backtests; the maker replay rejects them ("entry-relative exits are not supported by maker replay; use taker execution"). The simulated starting account must be flat; otherwise the backtest is refused with "entry-relative exits require a flat historical starting account". Stops and targets are judged on the bid, and an exit fills on the next event only if there is enough depth at its price.
Exit size per event is the smallest of the lot, 40, and the backtest's Max contracts per order. When the backtest isn't given risk limits, that limit defaults to risk.max_position. Live, it is the Max contracts per order you deploy with, which Studio prefills from your largest buy size or composite leg. Entries are capped by the same limit, so a lot normally sells in one order in both. Give the backtest the same risk limits as your deploy to compare like with like.