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_execution | string | omitted (GTC) | immediate_or_cancel | Optional fresh signal check and IOC submission for custom buys; no minimum fill guarantee. |
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 (up to four decimals) | 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. For time-sensitive custombuy_yes/buy_norules, addentry_execution: immediate_or_cancel. The Bot refreshes state and rechecks the originating rule immediately before submission, after account safety checks. Another owned pending entry blocks submission, and the exchange cancels any unfilled remainder. An ambiguous submission pauses further entries and is not automatically retried or reconciled. Verify its venue outcome before repairing the reservation or redeploying; managed Bots retain this reservation across process restarts. Omit the field for the existing GTC behavior, including intentional resting entries. This field is unavailable on composite orders, sell rules, built-in strategies, and other platforms. A buy limit caps the maximum paid; it cannot require a minimum actual fill price or guarantee predicates at exchange match time. Backtests retain fills at the next executable event even when the quote moves outside the original signal range.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¢), with up to four decimals. Kalshi sets the valid prices for each market in itsprice_ranges. Most markets trade in whole cents. Some, such asdeci_centandtapered_deci_centmarkets, also allow 0.1¢ steps, so0.999(99.9¢) is a valid price there. For buys and composite quotes, the Bot clamps the computed price into yourprice_floortoprice_ceilingband, then moves it to a valid market price. Choose bounds that contain valid prices: a band with no valid price can round above its ceiling (see price bounds). On a whole-cent market,price_ceiling: 0.999buys at 0.99 at most. - Entry sizes are whole contracts. An omitted or
0buysizemeans 1. Fills can be fractional. Exits normalize available inventory down to 0.01-contract increments, correcting tiny floating-point residue near whole numbers, and leave any remainder below 0.01 unsent. For example, 49.99 contracts can submit exits of 40 and 9.99, subject to Max contracts per order. These are submitted quantities, not guaranteed fills; see Selling. 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. By default, a custom buy is a good-till-canceled limit order at the ask;
entry_execution: immediate_or_cancelcancels any unfilled remainder instead. A GTC order that doesn't fill at once rests on the book. It goes away when it fills, when acancel_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. - Paper market closure. On a paper broker tick, resting simulated orders are canceled when the market is closed, determined, settled or finalized, or its official
close_timehas passed. A stale crossing book cannot fill those orders after closure is detected. These are simulated cancellations, not live exchange order cancellations. - 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. Each Bot shares a request budget across its own threads, replenished at 8 requests per second with a burst of 8. A read Kalshi rate-limits is tried up to twice more after a short wait. When a read is still refused, or an order is, new orders are skipped for the wait Kalshi asks for or, if it doesn't say, for 2 seconds, doubling while refusals continue up to 30 seconds. A minute without one starts over at 2 seconds. Rate-limit errors never count toward the API error limit. Keep this in mind with
selection: allon a series with many brackets.
Authentication status and reconnecting
Studio accepts a Kalshi API Key ID with a complete, unencrypted RSA or Ed25519 private key in PEM format, including its matching BEGIN and END lines. RSA keys may use PKCS#1 or PKCS#8; Ed25519 keys use PKCS#8. Older runners need a software upgrade to receive Ed25519 signing support; that explicit upgrade stops all Bots on the runner, so plan to redeploy them.
Before reusing a runner for a live deployment or update, Studio verifies its Kalshi credentials, even when credentials were previously saved. Missing credentials prompt you to reconnect before the deployment can proceed. If Kalshi refuses the saved runner key with HTTP 401 or 403, Studio asks you to update the API key; this also applies when starting a Paper Run. Other verification failures remain retryable and refuse authorization, with one exception when checking saved Kalshi keys: when the gateway masks the credential probe's failure as service unavailable, a Kalshi rate-limit log from the last 10 minutes allows the code push to proceed unless the saved key is known to be unreadable. This does not remove the Bot's runtime rate-limit backoff. A healthy runner alone is not enough. Queued deployments make the same check when claimed. This check does not prove that the Bot started or that an order filled.
A live Bot's process can keep running while Kalshi rejects its account checks. Studio shows Kalshi authentication blocked and offers Reconnect Kalshi when the selected Bot's current run reports an authentication blocker or has repeated positions-account authentication failures. Choose it to enter valid credentials for that Bot. Reconnect restarts only the selected Bot with its already deployed code, preserving its run identity and risk settings; it does not deploy an edited draft or start a new deployment. Studio requires verified current run, code and risk settings, and the runner requires fresh guardrail evidence with no active API-error cooldown. If these checks are unavailable, reconnect is refused; it does not automatically upgrade or rebuild an older runner.
Newly compiled live Kalshi custom Bots pause normal market evaluation after three /portfolio/positions responses with HTTP 401 and authentication_error, without an intervening successful positions read. While paused, the Bot blocks ordinary venue reads, order submissions and cancellations, including exits; the pause does not automatically cancel resting orders or liquidate positions. It allows at most one positions-read probe every five minutes. A timeout, rate limit or server error on that probe keeps the pause in place. A successful probe reopens evaluation at the strategy's cadence without clearing risk limits or an existing risk halt. This runtime pause belongs to that Bot's process; it does not pause sibling Bots and does not apply to built-in strategies, Paper Runs or Kalshi Perps. Existing Bots need a strategy update or redeploy to receive this compiled behavior; upgrading the runner alone does not add it.
Before a reconnect attempt, a newer successful authenticated account check or accepted order can clear the blocker indication. After reconnect, Studio keeps the Bot blocked until credential activation and durable persistence are verified and a fresh authenticated positions check is recorded for that reconnect on the selected run. An accepted order, an older successful check or a healthy process does not clear that reconnect blocker. None of these checks proves a new fill or guarantees trading will resume; earlier fills are historical. Recent checks unavailable means Studio could not read the selected run's evidence. The status check covers live Kalshi event and Kalshi Perps Bots; Paper Runs are excluded. The automatic runtime pause described above remains specific to Kalshi event custom Bots.
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. Exits, custom and built-in, per-fill exits and
max_lossliquidations are split into orders of at most min(40, Max contracts per order) contracts. Built-in entries are not split, which is why their pre-fill covers their largest entry. - 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. Exits still require working authentication; the authentication pause blocks their venue requests too. - 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, except recognized sports series |
market.event_ticker | Supported, except recognized sports series; 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 |
entry_execution: immediate_or_cancel | Supported by the custom execution-engine replay; with L2 depth, may fill partially or not at all and cancels the remainder. A delayed fill can occur outside the original signal range. |
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 |
Studio refuses backtests when the market, event or series ticker is recognized as a sports series, including price-only strategies with no sports or espn feed. Sports market prices are not recorded for replay. Such a strategy can still be saved for a Paper Run or live trading; a direct backtest request returns 422 with Sports strategies can't be backtested. Query selectors remain unsupported separately.
Backtests don't read your real account, and fills are simulated. Treat a backtest as a way to find problems, not as a forecast.
For newly calculated results, Risk capital (starting_capital) is the larger of risk.max_position dollars and the largest value of (recorded exposure − cumulative P&L) at any equity-curve point, rounded to cents. For a cost-based exposure curve, this accounts for open position cost plus net losses to date; profits offset that need. Repeating series or simultaneous markets can therefore need a larger base than the per-market contract cap. Return on Risk is simulated P&L divided by that base; percentage drawdown and the short-run return-based Sharpe fallback also use it. This changes reported metrics, not order sizing or the max_position limit, and is not a measured account balance. Stored older results may use the earlier base. See Reading Backtest Results.
A completed derived-data backtest may cover less than the requested period. When a bounded subminute history repair clips the window, the result preserves the original requested UTC bounds, reports coverage_status: partial and includes repair_window in window_clip_reason. Execution uses the admitted window, not the full request. Other clipping reasons can remain alongside it; older sealed jobs without request provenance keep their existing metadata. Compare requested and actual bounds before treating a result as full-period coverage.
Derived Kalshi series backtests can admit hours without replay entries when archived listing snapshots verify a quiet period or an interval with nothing listed. These hours contain no simulated quotes or fills. At the window edges, only listed quiet hours next to replay data qualify; listing evidence alone does not make an entirely empty backtest usable. Unverified hours or a detected volume rise within a stable set of interior listings keep the existing gap and clipping rules, and serving limits still apply. A 30-day request does not guarantee 30 days of coverage.
Derived maker backtests also apply an estimated-memory serving limit (server default: 6 GiB; deployment settings can change it). When adding older hours would exceed it, the run keeps the newest admitted suffix and reports window_clip_reason: serving_limit with meta.derived_maker_admission.limiting_dimension: memory. Admission metadata includes estimated_memory_bytes and max_memory_bytes; this is an estimate, not a measured memory guarantee. If even the newest hour cannot fit, the backtest fails with a serving-limit error rather than automatically retrying the same sealed inputs. Compare requested and actual UTC bounds before interpreting the result.
Derived taker series backtests retain valid intervals and unaffected markets when a contract has an order-book replay gap. The affected contract resumes from a fresh source opening or an independently sealed hourly book. Pending signals are cancelled across that gap; filled positions, costs, and risk state remain intact. No quotes or fills are fabricated inside excluded intervals. The result preserves the requested UTC bounds, reports coverage_status: partial, and records each excluded interval with its market ticker and reason in skipped_derived_intervals. Actual start and end describe the outer bounds of retained data, not uninterrupted coverage.
If no usable intervals remain, Studio shows Historical replay is unavailable, naming the market and UTC gap bounds. Request a history repair, then choose Retry after history repair to retry the same strategy. This failure is a history problem; Studio does not offer Fix with AI to change your rules. An otherwise eligible Run action remains available after a failed backtest.
Kalshi backtests and Paper Runs include simulated 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.