Advanced Kalshi Rule Examples
These are complete Kalshi custom strategies (platform: kalshi, strategy: custom) that use the advanced rule keys: composite orders, per-fill exits and rule-level schedules. Every complete example on this page passes the same checks as a Studio save, except the last, Rejected: quotes plus per-fill exits, which shows a combination the validator refuses and the error it gets.
Market tickers, prices, sizes and thresholds are illustrative only. They show how the keys work, not what to trade, and they are not recommendations. Backtest and paper trade any strategy before you deploy it live.
Paired YES and NO quote with a combined cap
Quotes both outcomes one cent above their best bids when the pair's combined cost stays at or under $0.98. The pair rests instead of being re-sent every loop. It is pulled from about 15 minutes before to 15 minutes after an FOMC or CPI release, in the last 10 minutes, and when other traders' bids push the combined best bids above 0.985.
version: 1
platform: kalshi
strategy: custom
strategy_name: CPI paired YES/NO maker with 98c cap
strategy_name_origin: generated
market:
series_ticker: KXCPI
risk:
max_position: 2
max_portfolio_positions: 3
price_floor: 0.03
price_ceiling: 0.97
loop:
interval: 15
edge:
macro:
provider: economic_calendar
event: FOMC,CPI
fields: [seconds_to_next, seconds_since_previous]
refresh: 5m
rules:
- name: announcement_blackout
when:
any:
- {field: edge.macro.seconds_to_next, op: "<=", value: 900}
- {field: edge.macro.seconds_since_previous, op: "<=", value: 900}
action: cancel_all
- name: cancel_near_close
when:
all:
- {field: time_to_expiry, op: "<=", value: "10m"}
action: cancel_all
- name: pull_when_outbid
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: order_count, op: ">", value: 0}
- {field: paired_best_bid_sum, op: ">", value: 0.985}
action: cancel_all
- name: quote_pair
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: order_count, op: "==", value: 0}
- {field: paired_best_bid_sum, op: "<", value: 0.965}
max_combined_price: 0.98
orders:
- side: yes
action: buy
size: 1
price: {reference: best_bid, offset: 0.01}
post_only: true
- side: no
action: buy
size: 1
price: {reference: best_bid, offset: 0.01}
post_only: trueWhat to notice:
order_count == 0lets the pair rest and keep its queue place. The threecancel_allrules above it decide when it comes down.- The quote gate reads the raw bids;
max_combined_pricere-checks the actual leg prices after rounding and clamping. pull_when_outbidcan't tell your bids from other traders', so it watches the sum, which includes your own quotes. Pulling needs other bids to push the sum above 0.985.- If only one leg fills,
position_size > 0stops re-quoting, and the other leg rests untilannouncement_blackoutorcancel_near_closepulls it. This strategy has no sell rule, so a lone filled leg is held until the market settles. That is legging risk; add asell_allrule if you want a way out. risk.max_entries_per_marketis left out: each leg uses an entry, so a value of 1 would block every pair. Studio prefills Max contracts per order from the largest leg (1), and Max open contracts asrisk.max_position(2), which covers both legs. The validator also checks that the two legs together fitmax_position.- Live, the calendar values refresh every 5 minutes and don't count down in between, so the announcement pull can start and end up to 5 minutes late. Backtests compute them exactly. For a wider live margin, raise the 900-second thresholds.
- Backtests run in the Kalshi maker replay and need economic-calendar data for the whole window.
NO bid one cent under the ask, weekday sessions
A single post-only NO bid one cent under the NO ask, only on weekdays from 13:00 to 24:00 UTC. It exits the whole position at a profit or stop level with sell_all.
version: 1
platform: kalshi
strategy: custom
strategy_name: NYC high temp NO bid one tick under the ask
strategy_name_origin: generated
market:
series_ticker: KXHIGHNY
risk:
max_position: 3
price_floor: 0.05
price_ceiling: 0.60
max_entries_per_market: 3
loop:
interval: 20
trading_schedule:
timezone: UTC
trading_hours:
- days: [mon, tue, wed, thu, fri]
start: "13:00"
end: "24:00"
rules:
- name: take_profit
when:
all:
- {field: no_position_size, op: ">", value: 0}
- {field: no_best_bid, op: ">=", value: 0.60}
action: sell_all
- name: stop_out
when:
all:
- {field: no_position_size, op: ">", value: 0}
- {field: no_best_bid, op: "<=", value: 0.25}
action: sell_all
- name: pull_quote_late
when:
all:
- {field: time_to_expiry, op: "<=", value: "2h"}
action: cancel_all
- name: requote_when_ask_moves
when:
all:
- {field: order_count, op: ">", value: 0}
- {field: spread, op: ">", value: 0.015}
action: cancel_all
- name: quote_no_under_ask
when:
all:
- {field: order_count, op: "==", value: 0}
- {field: no_position_size, op: "<", value: 3}
- {field: spread, op: ">", value: 0.015}
- {field: no_best_ask, op: ">=", value: 0.35}
- {field: no_best_ask, op: "<=", value: 0.50}
orders:
- side: no
action: buy
size: 1
price: {reference: best_ask, offset: -0.01}
post_only: trueWhat to notice:
best_askon a NO leg is the NO ask: 1 minus the best YES bid.- While the bid rests one cent under the ask, the spread is exactly one cent. If the ask moves up, the spread widens,
requote_when_ask_movescancels, and the next loop bids one cent under the new ask. - A top-level schedule is allowed with
orders. When it closes at 24:00 UTC, the Bot cancels the resting bid. Exits still run at any hour. - The exits use
sell_all, notsell_yesorsell_no, so the strategy backtests the same way in the maker replay. Live,sell_alldoesn't cancel the resting bid.
Per-fill 5-cent profit and 10-cent stop
Buys YES in a price band, then manages each fill on its own: sell a fill when the YES bid is 5 cents above what that fill cost, or stop it out when the bid is 10 cents below. This is the full rule order from Rule order and requirements.
version: 1
platform: kalshi
strategy: custom
strategy_name: BTC daily per-fill 5c target and 10c stop
strategy_name_origin: generated
market:
series_ticker: KXBTCD
risk:
max_position: 3
price_floor: 0.20
price_ceiling: 0.80
max_entries_per_market: 3
loop:
interval: 15
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_yes_fill
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_stop_loss_offset: 0.10
- name: take_profit_each_yes_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.30}
- {field: yes_best_ask, op: "<=", value: 0.45}
- {field: time_to_expiry, op: ">", value: "30m"}
- {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_allWhat to notice:
- Worked numbers: a 0.38 fill has a 0.43 target and a 0.28 stop trigger. A 0.42 fill has a 0.47 target and a 0.32 trigger. Each fill is managed at its own price, not the average.
buy_yesplaces a limit at the ask. If it doesn't fill right away,cancel_unfilled_entrypulls it on the next loop, and the entry can fire again at the new ask.- Each loop sells from at most one fill, capped at Max contracts per order, which Studio prefills as 1 here.
- Backtest with a taker backtest and a flat starting account. See Per-fill exits in backtests.
Entry-price profit table with a time gate
Buys YES between 85 and 88 cents. Each fill gets a target from the table, but only fills made more than 30 minutes before close. Every fill keeps an 8-cent stop.
version: 1
platform: kalshi
strategy: custom
strategy_name: Favorite scalper with price-mapped targets
strategy_name_origin: generated
market:
series_ticker: KXBTCD
risk:
max_position: 4
price_floor: 0.80
price_ceiling: 0.92
max_entries_per_market: 4
loop:
interval: 15
rules:
- name: stop_each_fill_8c
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_stop_loss_offset: 0.08
- name: table_targets_for_early_fills
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_profit_targets:
- {entry_price: 0.85, target_price: 0.95}
- {entry_price: 0.86, target_price: 0.95}
- {entry_price: 0.87, target_price: 0.96}
- {entry_price: 0.88, target_price: 0.97}
entry_profit_min_time_to_expiry: 30m
- name: buy_favorite
when:
all:
- {field: order_count, op: "==", value: 0}
- {field: yes_best_ask, op: ">=", value: 0.85}
- {field: yes_best_ask, op: "<=", value: 0.88}
- {field: yes_position_size, op: "<", value: 4}
action: buy_yes
size: 1
- name: cancel_unfilled_entry
when:
all:
- {field: order_count, op: ">", value: 0}
action: cancel_allWhat to notice:
- A fill at a price with no row, such as 0.84 after price improvement or a sub-cent 0.8501, is protected by the stop only.
- The 30-minute gate is judged when the fill happens. A fill at exactly 30 minutes before close is excluded. An earlier fill keeps its target through the final 30 minutes.
- Studio prefills Max open contracts from
risk.max_position(4). A deploy with a lower Max open contracts stops the Bot below 4 contracts; a higher one still stops it at 4.
Time-varying EMA-distance entry thresholds
On weekdays, buys YES in the BTC daily series when spot BTC trades far enough above its 26-period, 5-minute EMA. How far is a strict threshold from 09:30 to 11:00 ET and a looser one from 11:00 to 16:00 ET. A short top-level blackout pauses entries around 10:00 ET. Per-fill exits run at all hours.
version: 1
platform: kalshi
strategy: custom
strategy_name: BTC EMA-distance entries with session thresholds
strategy_name_origin: generated
market:
series_ticker: KXBTCD
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.90
max_entries_per_market: 2
loop:
interval: 15
edge:
btc:
provider: coinbase
symbol: BTC-USD
fields: [ema_26_5m_distance_pct]
trading_schedule:
timezone: America/New_York
blackouts:
- days: [mon, tue, wed, thu, fri]
start: "09:55"
end: "10:05"
rules:
- 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.08
- name: open_strict_threshold
trading_schedule:
timezone: America/New_York
trading_hours:
- days: [mon, tue, wed, thu, fri]
start: "09:30"
end: "11:00"
when:
all:
- {field: edge.btc.ema_26_5m_distance_pct, op: ">=", value: 0.004}
- {field: order_count, op: "==", value: 0}
- {field: yes_best_ask, op: "<=", value: 0.85}
- {field: yes_position_size, op: "==", value: 0}
- {field: time_to_expiry, op: ">", value: "1h"}
action: buy_yes
size: 1
- name: midday_looser_threshold
trading_schedule:
timezone: America/New_York
trading_hours:
- days: [mon, tue, wed, thu, fri]
start: "11:00"
end: "16:00"
when:
all:
- {field: edge.btc.ema_26_5m_distance_pct, op: ">=", value: 0.0025}
- {field: order_count, op: "==", value: 0}
- {field: yes_best_ask, op: "<=", value: 0.85}
- {field: yes_position_size, op: "==", value: 0}
- {field: time_to_expiry, op: ">", value: "1h"}
action: buy_yes
size: 1
- name: cancel_unfilled_entry
when:
all:
- {field: order_count, op: ">", value: 0}
action: cancel_allWhat to notice:
ema_26_5m_distance_pctis a fraction:0.004means spot is 0.4% above the EMA. See Edge Data.- Each entry needs its own conditions, its own window, and the top-level schedule. The 09:55 to 10:05 blackout cuts into the strict window.
- Every rule schedule names its own
timezone; nothing is inherited from the top level. - The Bot cancels pending entry orders at 09:30, 11:00 and 16:00, while the blackout is in force, and on restart. See Orders at window edges.
- If the Coinbase feed can't be fetched (beyond its staleness allowance), the Bot blocks new entries on that market and cancels its resting entry orders there; the stop and take-profit rules keep running. If only the EMA field is unavailable, for example after a late 5-minute candle, conditions on it are false: neither entry rule fires, and the stop and take-profit rules still run.
yes_best_ask <= 0.85matters:buy_yesbids at the ask, clamped toprice_ceiling, so without it an ask of 0.95 would leave a resting 0.90 bid.
One target table for YES and NO fills
Buys whichever outcome is the 85 to 88 cent favorite, never both. Every YES and NO fill gets an 8-cent stop and one shared target table. Entries are allowed on weekdays only, through a reusable rule-level schedule made of blackouts.
version: 1
platform: kalshi
strategy: custom
strategy_name: Either-side favorite scalper with one shared target table
strategy_name_origin: generated
market:
series_ticker: KXBTCD
risk:
max_position: 2
price_floor: 0.80
price_ceiling: 0.92
max_entries_per_market: 4
loop:
interval: 15
rules:
- name: stop_yes_fills_8c
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_stop_loss_offset: 0.08
- name: stop_no_fills_8c
when:
all:
- {field: no_position_size, op: ">", value: 0}
action: sell_no
entry_stop_loss_offset: 0.08
- name: yes_table_targets
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_profit_targets: &favorite_targets
- {entry_price: 0.85, target_price: 0.93}
- {entry_price: 0.86, target_price: 0.94}
- {entry_price: 0.87, target_price: 0.95}
- {entry_price: 0.88, target_price: 0.96}
- name: no_table_targets
when:
all:
- {field: no_position_size, op: ">", value: 0}
action: sell_no
entry_profit_targets: *favorite_targets
- name: buy_yes_favorite
trading_schedule: &weekdays_only
timezone: UTC
blackouts:
- days: [sat, sun]
start: "00:00"
end: "24:00"
when:
all:
- {field: order_count, op: "==", value: 0}
- {field: yes_best_ask, op: ">=", value: 0.85}
- {field: yes_best_ask, op: "<=", value: 0.88}
- {field: no_position_size, op: "==", value: 0}
- {field: yes_position_size, op: "<", value: 2}
- {field: time_to_expiry, op: ">", value: "30m"}
action: buy_yes
size: 1
- name: buy_no_favorite
trading_schedule: *weekdays_only
when:
all:
- {field: order_count, op: "==", value: 0}
- {field: no_best_ask, op: ">=", value: 0.85}
- {field: no_best_ask, op: "<=", value: 0.88}
- {field: yes_position_size, op: "==", value: 0}
- {field: no_position_size, op: "<", value: 2}
- {field: time_to_expiry, op: ">", value: "30m"}
action: buy_no
size: 1
- name: cancel_unfilled_entry
when:
all:
- {field: order_count, op: ">", value: 0}
action: cancel_allWhat to notice:
- For NO fills, the stop and table read the NO bid and match the NO fill price.
&favorite_targetsand&weekdays_onlyare YAML anchors;*favorite_targetsand*weekdays_onlyreuse them.- The opposite-side position checks stop a buy on one side from netting against contracts you hold on the other side. Kalshi nets YES and NO in one market, which would close your oldest lots at that price.
Rejected: quotes plus per-fill exits
Resting composite quotes can't share a strategy with per-fill exits, in any rule order and with a table or an offset (see What can be combined). Use per-fill exits with buy_yes/buy_no entries, or composite quotes with sell_all/cancel_all exits.
# Rejected: resting composite quotes cannot share a strategy with per-fill exits
# error: rules[1].orders: entry-relative exits do not support composite maker orders
version: 1
platform: kalshi
strategy: custom
strategy_name: CPI YES bid improver with per-fill take-profit
strategy_name_origin: generated
market:
series_ticker: KXCPI
risk:
max_position: 2
price_floor: 0.03
price_ceiling: 0.97
loop:
interval: 15
rules:
- name: take_profit_each_yes_fill
when:
all:
- {field: yes_position_size, op: ">", value: 0}
action: sell_yes
entry_profit_offset: 0.04
- name: quote_yes_bid
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: spread, op: ">=", value: 0.03}
orders:
- side: yes
action: buy
size: 1
price: {reference: best_bid, offset: 0.01}
post_only: true