Kalshi Strategy Reference
This section lists every setting a Kalshi strategy accepts, what each one does when your Bot runs, and where live trading and backtests behave differently. It covers platform: kalshi (event contracts). Kalshi Perps has its own format; see Kalshi Perps.
Studio's AI writes the strategy for you as a short YAML document. You don't have to write YAML by hand, but reading it lets you check that the strategy does what you asked. Studio checks the document every time it is saved and again when you deploy, so a strategy with an invalid setting is refused before it can trade. The check can't catch everything: misspelled keys inside params and edge aliases are ignored, and it doesn't confirm that a ticker exists. See Unknown and misspelled keys.
All values in the examples in this section are illustrative. They show how the settings work, not what to trade or how much. Nothing here is a recommendation or a claim about results.
In this section
The Kalshi reference is split into these pages:
- Kalshi Market Selection: How a Kalshi strategy picks markets with ticker, event_ticker, series_ticker or query, how selection works on a rolling series, and which selector wins when more than one is set.
- Kalshi Risk and Loop Settings: Every Kalshi risk key (max_position, price_floor and price_ceiling, max_loss, max_entries_per_market, max_notional, max_portfolio_positions), what each one counts and when it resets, and the loop interval.
- Kalshi Active Window and Trading Schedule: Limit when a Kalshi strategy may trade with a one-time active_window or a recurring weekly trading_schedule: timezones, windows, daylight saving, what a schedule blocks, and rule-level schedules.
- Kalshi Example Strategies: Seven complete strategies that pass Studio's checks: a one-shot event trade, a rolling crypto series, an every-bracket sweep, a spend-capped scale-in, weekday hours, an event window and a scheduled built-in, each with the deploy limits it needs.
- Kalshi Strategies the Validator Rejects: Common Kalshi strategy mistakes Studio refuses at save time, with the exact error and how to fix each one.
This page keeps what every Kalshi strategy shares: a minimal strategy, the full parameter table, the document root keys, how built-in strategies and custom rules run on Kalshi, and units and execution, including the deployment risk limits and backtest support.
A minimal Kalshi strategy
Every Kalshi strategy has the same skeleton: a version, the platform, a strategy type, the market, risk limits, a loop interval, and either rules (custom strategies) or params (built-in strategies).
version: 1
platform: kalshi
strategy: custom
strategy_name: Minimal Kalshi example
market:
ticker: KXMLBGAME-26OCT15NYYBOS-NYY
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.60
loop:
interval: 30
rules:
- name: take_profit
when:
all:
- {field: yes_position_size, op: ">", value: 0}
- {field: yes_best_bid, op: ">=", value: 0.70}
action: sell_yes
- name: enter_yes
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: yes_best_ask, op: "<=", value: 0.40}
action: buy_yes
size: 1About every 30 seconds (30 seconds after each cycle finishes), the Bot reads this one market. If it holds YES and the best YES bid is at least $0.70, it sells. If it holds nothing and YES can be bought for $0.40 or less, it buys 1 contract. The ticker is illustrative; Studio does not check that a ticker exists when it saves.
All Kalshi parameters
This is the complete list. Anything not in this table is either rejected or ignored (see Unknown and misspelled keys). Prices are dollars per contract from 0 to 1, so 0.05 means 5¢.
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
version | integer | required | 1 | Format version of the document. |
platform | string | required | kalshi | Picks the venue. Must match your Studio session. |
strategy | string | required | custom, spread_capture, mean_reversion, panic_fade, observation_momentum, pre_announcement_drift | Custom rules or one of five built-in strategies. |
strategy_name | string | none | any text (Studio keeps 80 characters) | Display name. No effect on trading. |
strategy_name_origin | string | generated when a name is set | user, generated, curated | Where the name came from. No effect on trading. |
name | any | none | anything | Old key. Accepted and ignored. |
market.ticker | string | one selector required | a Kalshi market ticker | Trade exactly one market. |
market.event_ticker | string | one selector required | a Kalshi event ticker | Trade the first open market Kalshi lists for the event. |
market.series_ticker | string | one selector required | a Kalshi series ticker | Follow a recurring series and trade its current event. |
market.selection | string | most_liquid | most_liquid, all | With a series: one bracket or every bracket. |
market.query | string | one selector required | free text | Search open event titles at run time. Not backtestable. |
risk.max_position | integer (contracts) | required | whole number > 0 | Largest position per market, held plus resting. Pre-fills the deploy dialog. |
risk.price_floor | number ($) | 0 | 0 to under 1, below the ceiling | Lower edge of the price band. |
risk.price_ceiling | number ($) | required | above 0 up to 1, above the floor | Upper edge of the price band. |
risk.max_loss | number ($) | 0 (off) | ≥ 0; above 0 only with strategy: custom | Halts the deployment and sells its inventory after this loss. |
risk.max_entries_per_market | integer | 0 (off) | whole number ≥ 0 | Most buy orders per market per deployment. |
risk.max_notional | number ($) | 0 (off) | ≥ 0; above 0 only with strategy: custom | Most dollars spent on entries per market per deployment. |
risk.max_portfolio_positions | integer | 0 (off) | whole number ≥ 0 | Blocks entries into a new market once your account is active in this many. |
loop.interval | integer (seconds) | required | whole number ≥ 10 | Pause between evaluation cycles. |
active_window.start | timestamp | none (active from deploy) | RFC3339 with offset; custom only | The Bot idles before this instant. |
active_window.end | timestamp | none (never ends) | RFC3339 with offset, after start; custom only | Entries stop at this instant; exits and max_loss keep running. |
trading_schedule.timezone | string | required in a schedule | UTC or an IANA name such as America/New_York | Timezone for the schedule's days and times. |
trading_schedule.trading_hours | list of windows | omitted (all week open) | 1 or more windows | The only times new entries are allowed. |
trading_schedule.blackouts | list of windows | omitted (none) | 1 or more windows | Times new entries are never allowed. Wins over hours. |
trading_schedule.*[].days | list of strings | required | unique mon tue wed thu fri sat sun | Days the window starts on. |
trading_schedule.*[].start | string HH:MM | required | 00:00 to 23:59 | Local start time (included). |
trading_schedule.*[].end | string HH:MM | required | 00:00 to 23:59, or 24:00 | Local end time (excluded). Earlier than start means overnight. |
params (spread_capture) | map | see built-ins | spread_floor, order_count, post_only, refresh_on_fill (no effect) | Settings for spread_capture. |
params (mean_reversion) | map | see built-ins | entry_low, entry_high, exit_target, cooldown | Settings for mean_reversion. |
params (panic_fade) | map | see built-ins | panic_threshold, fade_size, recovery_exit, max_fades | Settings for panic_fade. |
params (observation_momentum) | map | see built-ins | lookback_periods, momentum_threshold, position_scale | Settings for observation_momentum. |
params (pre_announcement_drift) | map | see built-ins | entry_hours_before, blackout_minutes_before, drift_direction, exit_on_release | Settings for pre_announcement_drift. |
rules | list | required for custom | 1 or more rules | Checked top to bottom each cycle; first match acts. |
rules[].name | string | required | non-empty text without double quotes | Label shown in logs and decisions. |
rules[].when.all / rules[].when.any | list of conditions | exactly one required | 1 or more conditions | AND (all) or OR (any) of the conditions. |
rules[].when.*[].field | string | required | 17 built-in fields (listed under Custom rules on Kalshi), edge.<alias>.<field>, derived.<name> | The value the condition reads. See Custom rules. |
rules[].when.*[].op | string | required | < > <= >= == != (quoted) | The comparison. |
rules[].when.*[].value | number or duration | one of value / value_field | a number; time_to_expiry takes "30s", "15m", "6h", "1d" | Fixed threshold. |
rules[].when.*[].value_field | string | one of value / value_field | another numeric field | Compare against another live field. |
rules[].action | string | one of action / orders | buy_yes, buy_no, sell_yes, sell_no, sell_all, cancel_all, skip | What the rule does. |
rules[].size | integer (contracts) | 1 for buys | whole number, at most max_position (omitted or 0 means 1) | Contracts per buy. Kalshi sells ignore it. |
rules[].orders | list | one of action / orders | one YES buy and/or one NO buy | Resting limit buys priced from the book. Advanced rules. |
rules[].orders[].side | string | required | yes, no | Outcome the leg buys. |
rules[].orders[].action | string | required | buy | Only buying is supported. |
rules[].orders[].size | integer (contracts) | required | whole number > 0 | Contracts for this leg. |
rules[].orders[].price.reference | string | required | best_bid, best_ask | Book price the leg is anchored to. |
rules[].orders[].price.offset | number ($) | 0 | between -1 and 1 | Added to the reference price. |
rules[].orders[].post_only | boolean | false | true, false | Rest only as a maker. |
rules[].max_combined_price | number ($) | none | above 0 up to 1 | Cap on a paired YES + NO quote. |
rules[].entry_profit_offset | number ($) | none | above 0, below 1 | Per-fill take-profit on a sell rule. |
rules[].entry_stop_loss_offset | number ($) | none | above 0, below 1 | Per-fill stop on a sell rule. |
rules[].entry_profit_targets | list | none | rows of entry_price, target_price (whole cents) | Per-fill profit table on a sell rule. |
rules[].entry_profit_min_time_to_expiry | duration | none | whole s, m or h, e.g. "30m" | Limits the profit table to earlier fills. |
rules[].trading_schedule | schedule | none | same shape as trading_schedule; buy rules only | Hours when one entry rule may buy. |
edge.<alias> | map | none | up to 8 aliases; lowercase names such as btc; custom only (ignored on built-ins) | External data rules can read. See Edge data. |
edge.<alias>.provider | string | required | coinbase, nws, economic_calendar | Data source. |
edge.<alias>.symbol / station / event | string | the provider's one selector | e.g. BTC-USD, KLGA, FOMC,CPI | What the source reads. |
edge.<alias>.fields | list | required | names from the provider's catalog | Values rules may read. |
edge.<alias>.refresh | duration | 5s / 5m / 15m by provider | e.g. "10s", "5m" | How long fetched values are reused. |
derived.<name> | map | none | up to 8; custom only | A computed number such as spot minus strike. |
derived.<name>.kind | string | required | signed_distance_from_strike | The calculation. |
derived.<name>.observed | string | required | edge.<alias>.<field> | The value the strike is subtracted from. |
Document root
version
Always 1. Leaving it out fails with unsupported DSL version 0 (want 1). Write it as the plain number 1, not "1".
platform
Write kalshi. Case and surrounding spaces don't matter. platform has no default: if you leave it out the strategy is not treated as Kalshi and is refused. A Studio session belongs to one venue, so you can't move a strategy to another venue by editing this line; Studio rejects the save as a platform mismatch.
strategy
custom runs your own rules. The other five values run a built-in strategy tuned with params. Some settings only work with custom: active_window, max_loss, max_notional, derived, and the advanced rule keys. trading_schedule, max_entries_per_market and max_portfolio_positions are accepted by every Kalshi strategy type.
A custom strategy needs at least one rule and at least one exit rule (sell_yes, sell_no, sell_all or cancel_all).
strategy_name and strategy_name_origin
strategy_name is the label Studio shows for the strategy. strategy_name_origin records whether you typed the name (user), the AI generated it (generated) or it came from a curated template (curated). Other values, including the older legacy, are saved as generated. Neither changes how the Bot trades. Renaming a strategy doesn't change its behavior.
Unknown and misspelled keys
Studio is strict everywhere except inside params and edge aliases:
- An unknown top-level key is rejected, for example
description: unknown top-level field. - Unknown keys inside
market,risk,loop,active_window,rules, conditions,orders,trading_scheduleandderivedare rejected. A typo such asmax_notinalfails withrisk.max_notinal: unknown risk field. - Unknown keys inside
paramsand eachedgealias are silently ignored. A typo such asrefesh: 30ssaves without an error, and the setting falls back to its default.
# Rejected: top-level keys are checked, so an extra description field is refused
# error: description: unknown top-level field
version: 1
platform: kalshi
strategy: custom
description: my first strategy
market:
ticker: KXMLBGAME-26OCT15NYYBOS-NYY
risk:
max_position: 2
price_ceiling: 0.60
loop:
interval: 30
rules:
- name: take_profit
when:
all:
- {field: yes_position_size, op: ">", value: 0}
- {field: yes_best_bid, op: ">=", value: 0.70}
action: sell_yes
- name: enter_yes
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: yes_best_ask, op: "<=", value: 0.40}
action: buy_yesWhole-number settings (version, max_position, max_entries_per_market, max_portfolio_positions, interval, rule and order size) must be whole: interval: 10.9 fails with loop.interval: must be a whole number, got 10.9, while 10.0 is fine. Numbers in quotes ("30") and numbers with units (30s) are refused too.
Formatting rules
- Keep the whole strategy in one YAML document. Anything after a
---line is ignored. - JSON also works, because JSON is valid YAML.
- Anchors and aliases (
&name,*name) work anywhere, for example to reuse a condition. - Merge keys (
<<:) only work insidemarket,risk,loop,active_windowandparams. - Indent with spaces. A tab used for indentation is a parse error.
Built-in strategies on Kalshi
Set strategy to one of five built-in strategies and tune it with params. There are no rules: the strategy decides entries, exits and sizes.
| Strategy | Required params | Optional params (live default) |
|---|---|---|
spread_capture | spread_floor, order_count | post_only (false); refresh_on_fill is accepted but has no effect |
mean_reversion | entry_low, entry_high | exit_target (0.50), cooldown (60 s) |
panic_fade | panic_threshold, fade_size | recovery_exit (0.04), max_fades (3) |
observation_momentum | lookback_periods, momentum_threshold | position_scale (linear; or exponential) |
pre_announcement_drift | entry_hours_before, blackout_minutes_before | drift_direction (auto; or bullish, bearish), exit_on_release (false) |
On Kalshi, built-in strategies can use trading_schedule, max_portfolio_positions and max_entries_per_market (except with spread_capture; see risk.max_entries_per_market). They can't use active_window, max_notional, max_loss or derived. A plain rules block or an edge block on a built-in strategy saves but never runs (rules that use per-fill exits or a rule-level trading_schedule are rejected instead), so don't add rules to a built-in expecting them to trade. Unknown params keys are ignored.
Every price threshold in params is a YES price in dollars, including the ones that trigger NO buys. Built-in exits sell only what the Bot holds, on the side it holds: the Bot works out its position from its own fills in this deployment, so a restart doesn't lose track of it. A redeploy or update doesn't manage positions the previous deployment bought, and a built-in doesn't enter a market while the account holds contracts there that the Bot didn't buy. Exits are reduce-only, immediate-or-cancel sells at the best bid, split to fit Max contracts per order. Entries aren't split: several built-in strategies size them above 1 contract (mean_reversion buys min(max_position, 10)). Studio pre-fills Max contracts per order from that largest entry (see deployment limits), but if you lower it, a larger entry is refused, not shrunk. An entry that hasn't filled after two loops is cancelled. The full reference, including each strategy's caveats, is on Built-in strategies.
Custom rules on Kalshi
strategy: custom runs your rules top to bottom every cycle; the first rule whose conditions are true acts, and nothing below it runs that cycle. The exception is a buy rule that matches while entries are blocked (a closed trading_schedule, active_window.end, an edge data outage, or Max daily loss): it is skipped, and the rules below it still run. Kalshi has the widest rule support of any venue:
- All 17 fields:
price,spread,volume,time_to_expiry,position_size,unrealized_pnl,balance,order_count, the order book fieldsyes_best_bid,yes_best_ask,no_best_bidandno_best_ask, and the Kalshi-onlyyes_position_size,no_position_size,portfolio_position_count,paired_best_bid_sumandpaired_best_ask_sum. On Kalshi,priceis the best YES bid (the last trade when there's no bid),position_size, the side positions andunrealized_pnldescribe this Bot's attributed holdings, andorder_countreads your whole account in that market. - All 7 actions. Buys are limit orders at the ask (clamped into your price band).
sell_yesandsell_nosell the whole side immediately-or-cancel at the bid.sell_allsells both sides the same way.cancel_allcancels only this Bot's orders in that market. - Advanced keys that only Kalshi custom strategies accept: composite
orderswithmax_combined_price, per-fill exits (entry_profit_offset,entry_stop_loss_offset,entry_profit_targets,entry_profit_min_time_to_expiry) and rule-leveltrading_schedule. See Advanced Kalshi rules. - External data:
edgealiases (Coinbase, NWS weather, FOMC/CPI calendar) andderivedmetrics such as spot price minus strike. See Edge data.
Position fields read your account's whole position in that market. Without a schedule, sell rules act on that whole position, including contracts you bought by hand. Rule syntax, every field, and the checks Studio runs on your rule logic are on Custom rules.
Units and execution
- Prices are dollars per contract from 0 to 1 (
0.05= 5¢). The Bot sends Kalshi limit prices in whole cents, rounding a computed price to the nearest cent. - Sizes are whole contracts. An omitted or
0buysizemeans 1. max_notionalandmax_lossare dollars.max_lossincludes fees.loop.intervalis seconds. Schedule times are localHH:MM;active_windowtimes are absolute instants.- Orders are placed each cycle, not continuously. A custom buy is a good-till-canceled limit order at the ask. If it doesn't fill at once, it rests on the book. It goes away when it fills, when a
cancel_allrule fires, when a compositeordersrule re-quotes in that market, when a schedule closes, when an active window ends, whenmax_losshalts the deployment, or when required edge data or aderivedvalue is unavailable. When any rule has its owntrading_schedule, entry orders also expire at the next schedule change. - Every order passes fixed Kalshi checks first. The market must be open with a close time in the future, and its most recent trade, by anyone, must be at most 120 seconds old. A market that has never traded passes. This applies to sells too, so on a quiet market a stop-loss or take-profit can be refused until a fresh trade prints. A buy also needs a two-sided, uncrossed book, and a custom sell needs a bid on the side it sells. You can't change these checks.
- Missing book values. Order book fields, and the paired sums built from them, read as unavailable when a side is empty. A condition that reads an unavailable value, as its
fieldor itsvalue_field, is false whatever the operator, including!=, live and in backtests. See Unavailable values. - Order budget and backoff. One Bot sends at most 20 orders per rolling 60 seconds, shared across all its markets. Each composite leg counts; cancels don't. Orders beyond that are skipped, not refused. After Kalshi answers with a rate-limit error, new orders are skipped for the wait Kalshi asks for, or 5 minutes if it doesn't say. Keep this in mind with
selection: allon a series with many brackets.
Deployment risk limits
When you deploy, you confirm a set of risk limits in the deploy dialog. They aren't part of the YAML, but every order must pass them as well as the strategy's own checks. An order that breaks a limit is refused, not shrunk.
| Deploy dialog limit | Pre-filled from | If a deployment sets no value |
|---|---|---|
| Max contracts per order | The largest single order the strategy sends (see below) | 1 |
| Max open contracts (per market) | max_position | max(1, min(max_position, 3)) |
| Max dollars per order | Same number as Max contracts per order, in dollars | $10 |
| Max daily loss | max_position, in dollars | Off |
| Max daily notional traded | max(order size, max_position, 10 × order size) | $100 |
| API error limit, Error window minutes | Not taken from the strategy | 3 errors in 5 minutes |
The largest single order is the biggest buy size across your rules (an omitted size counts as 1) or composite orders leg. For built-in strategies it is min(max_position, 10) for mean_reversion, max_position for observation_momentum, min(max_position, 5) for pre_announcement_drift, fade_size for panic_fade and 1 for spread_capture. Exits the Bot splits (below) don't count.
- Max contracts per order is checked on every order, sells included. Custom exits, per-fill exits and
max_lossliquidations are split into orders of at most min(40, Max contracts per order) contracts. Built-in strategy orders are not split, which is why their pre-fill covers their largest order. - Max open contracts counts held plus resting contracts in that market, from any source. The Bot applies the lower of this and
max_position. - Max daily loss watches this deployment's P&L since 00:00 UTC: the realized P&L of today's fills, after fees, plus the unrealized P&L of everything the deployment still holds, valued at the bid against its entry price. A position opened before today counts its whole open loss; a market that settled counts at $1 or $0 on the day it closed; a held side with no bid counts at $0. Only this deployment's own fills and the positions you adopt at deploy count, not manual trades. Once the loss reaches the limit, buys are refused until 00:00 UTC (reason
max_daily_loss). A buy rule that matches meanwhile is skipped and the rules below it, exits included, still run. Sells are never blocked by this limit. The stop persists even if P&L recovers, the process restarts, or the limit is raised or disabled within the same deployment run. At 00:00 UTC it resets only after the new day's state is saved; ongoing unrealized losses can immediately trip the new day's limit. Updating or redeploying the Bot starts a new deployment, whose count starts at $0: the positions you adopt count from their value at deploy, and the earlier deployment's losses today don't carry over. The limit never sells anything by itself. - Max daily notional counts traded notional in that market since 00:00 UTC, including fills this Bot didn't make, and checks buys only. A restart doesn't reset it.
- Exits, custom and built-in, and
max_lossliquidations are split into orders of at most min(40, Max contracts per order) contracts. Built-in entries are not split, so a built-in that sizes entries frommax_positionneeds a Max contracts per order at least that large. - API error limit and Error window minutes: once the Bot hits that many Kalshi API errors inside the window, it refuses new orders until one more window has passed, then resumes by itself. Reduce-only exits (custom-rule and built-in sells, per-fill exits and
max_lossliquidations) keep going. - For an already deployed Bot, the dialog starts from its verified current limits. If those settings are unknown, review the suggested limits before confirming an update.
The daily-loss stop is saved on the runner's filesystem. Missing or corrupt state, or an interrupted P&L check, pauses new entries for the rest of the UTC day while exits remain allowed. If the state can't be saved, entries pause without a promised reset time. Keeping the state requires retaining that filesystem; it isn't a remote backup. Existing Bots need a strategy update or redeploy to receive this compiled check; upgrading the runner alone doesn't add it to previously compiled code.
Note: Max daily loss pauses entries for the rest of the day.
risk.max_lossis the loss halt for a whole deployment: it sells the deployment's inventory and never trades that deployment again.
See Risk & Limits for the wider picture and Monitoring for watching a live Bot.
Backtesting and Deep Research
Kalshi strategies can be backtested (see Backtest and Data sources), with these exceptions and differences:
| Setting | Backtest |
|---|---|
market.ticker, series_ticker | Supported |
market.event_ticker | Supported; replays past events of the same series, trading each one's most liquid bracket |
market.query | Not supported |
active_window | Not supported |
trading_schedule (top-level and rule-level) | Supported |
max_position | Supported; blocks buys past it, for custom strategies too |
price_floor, price_ceiling | Supported; out-of-band buys are clamped or skipped depending on the replay |
max_entries_per_market, max_loss | Supported |
max_notional | Supported, except for strategies that rest quotes with orders |
max_portfolio_positions | Supported; counts only the backtest's own positions |
Composite orders | Partial: replayed as simulated resting orders; see Advanced Kalshi rules |
Per-fill exits (entry_profit_offset, entry_stop_loss_offset, entry_profit_targets) | Ordinary backtests only; not with composite orders |
edge, derived | Partial: only fields your rule conditions read are loaded; see Edge data |
Backtests don't read your real account, and fills are simulated. Treat a backtest as a way to find problems, not as a forecast.
Kalshi backtest fees include trade fees and cent-alignment rounding. For partial fills of one order, rounding overpayment accumulates across fills. Each fill's rounding rebate is capped at that fill's trade and rounding fees, so its modeled net fee cannot be negative. Any unapplied overpayment stays on the same order for a later fill that can cover the rebate; it is not credited immediately. These are simulated fees, not a promise of a live rebate or fill.
Deep Research is Kalshi-only and covers crypto and weather markets. It runs many backtest variants of your strategy, re-checking every variant as a normal save would. Anything that blocks a backtest (such as query or active_window) also blocks research.