Kalshi Perps Strategy Reference
This section covers platform: kalshi_perps: strategies that trade Kalshi's perpetual futures on crypto assets. It lists every key a Kalshi Perps strategy accepts, what each one does when your Bot runs, and the caveats that matter once real money is involved. Event-contract strategies on Kalshi use a different format; see Kalshi.
The reference is split across this page and the pages listed under In this section. This page has the overview: how perps differ from event contracts, a minimal strategy, the full parameter table with a link from every key to its details, the top-level keys and format rules, what the Bot does on every tick, paper trading, and deploying a Perps Bot.
Studio's AI writes the strategy for you as a short YAML document. Studio checks it every time it is saved and again when you deploy, so a strategy with an invalid setting is refused before it can trade. You don't have to write YAML yourself, but reading it is the best way to confirm the strategy does what you asked. Some mistakes still get through. Misspelled keys inside a block are ignored rather than rejected (see Unknown and misspelled keys), and a few settings save fine but only fail once the Bot runs, such as a leverage above the market's cap or a ticker Kalshi doesn't list.
All values in the examples in this section are illustrative. They show how the settings work, not what to trade, how much to commit or which thresholds to use. Nothing here is a recommendation or a claim about results. Leverage magnifies losses as well as gains.
In this section
- Kalshi Perps Markets and Execution: The
marketslist and its perp tickers, theloop.intervalcadence, andexecution: taker vs limit entries, slippage, limit offset and time limit, and what happens when a close doesn't go through. - Kalshi Perps Sizing and Risk: Margin and leverage sizing, the Bot's own take-profit and stop-loss and their limits, and funding awareness.
- Kalshi Perps Archetypes: The
trend_rider,breakout_hunteranddip_reverterarchetypes: the rules forparams, the ATR stop, and what each archetype does with every parameter. - Kalshi Perps Custom Strategies: Custom rules: indicators, the fields rules can read (including Coinbase MACD), operators and crossovers, actions and
size_pct, missing data, setups, evaluation order, and patterns for rules that behave. - Kalshi Perps Example Strategies: Seven complete strategies that save as written: one
trend_riderarchetype and six custom strategies using crossovers, setups, add and trim, resting entries and Coinbase MACD.
How perps strategies differ from event contracts
A perpetual ("perp") follows the price of a crypto asset and never expires. That changes almost everything about how a strategy is written.
| Event contracts (Kalshi, Polymarket) | Kalshi Perps | |
|---|---|---|
| What you hold | YES or NO contracts | A signed position: long (positive) or short (negative) |
| Price | $0 to $1, read as a probability | Dollars per contract, where a contract is a fixed fraction of a coin |
| Size | Contracts per order | margin_usd × leverage of notional per entry |
| End of a position | Settles at $0 or $1 when the event resolves | No expiry or settlement. Open until something closes it or the exchange liquidates it |
| Holding cost | None | Funding payments between longs and shorts |
| Exits | Your rules and risk keys | Your rules, plus take-profit and stop-loss the Bot enforces itself |
| Backtesting | Available for supported strategies on Kalshi and Polymarket (not Polymarket US) | Not available. Paper trading only |
What this means in practice:
- Signed positions. Your Bot is long, short or flat, counted in contracts.
positionis this Bot's own position, built from its own fills. It is not your whole account's position, which your other Perps Bots and your manual trades share. - Dollar prices per contract. One
KXBTCPERPcontract is 0.0001 BTC, so it trades near $6.45 when BTC is near $64,500. Every price field, and every price you write in a rule, is in dollars per contract. - Leverage. Each entry commits
margin_usdand multiplies it byleverageto get its notional size (see Sizing and Risk). All your Perps Bots trade one cross-margined account, and Kalshi liquidates across that whole account. - No settlement. A position stays open until a rule closes it, the Bot's own take-profit or stop-loss closes it, you close it, or it is liquidated. While it is open it pays or receives funding.
- The Bot enforces its own exits. Take-profit and stop-loss are not orders resting at the exchange. The Bot checks them on every tick (the check it runs every
loop.intervalseconds), so they stop protecting you while the Bot is stopped. See Limits of Bot-enforced exits. - A document of its own. There is no
market,edge,trading_scheduleororders. Actions areopen_long,open_short,add,reduceandclose. Custom perps rules also have crossover operators and setups, which other venues don't.
A minimal Kalshi Perps strategy
version: 1
platform: kalshi_perps
strategy: trend_rider
strategy_name: Minimal perps example
markets:
- ticker: KXBTCPERP
sizing:
margin_usd: 10
leverage: 1
risk:
stop_loss_pct: 3Every 60 seconds (the default), the Bot checks KXBTCPERP. With its default settings, trend_rider compares a 9-candle and a 21-candle exponential moving average of hourly candles. It holds a long while the fast average is above the slow one and a short while it is below. Each entry commits $10 × 1 = $10 of notional, which is 1 contract at about $6.45. The Bot closes the position if the price moves 3% against its average entry.
Everything left out takes its default: loop checks every 60 seconds, execution uses immediate (taker) entries with a 0.5% slippage bound, params uses the archetype's defaults, there is no take-profit, and funding_awareness is off.
All Kalshi Perps parameters
This is the complete list. At the top level, any other key is rejected. Inside a block, any other key is ignored without an error, except in archetype params (see Unknown and misspelled keys). Percentages are plain numbers: 2 means 2%. Prices are dollars per contract.
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
version | integer | required | 1 | Format version of the document. |
platform | string | required | kalshi_perps | Selects the Kalshi Perps format. |
strategy | string | required | trend_rider, breakout_hunter, dip_reverter, custom | A ready-made archetype, or your own rules. |
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 (others saved as generated) | Where the name came from. No effect on trading. |
markets | list | required | 1 to 5 entries | The perp markets this strategy trades, each checked separately. |
markets[].ticker | string | required | capitals like KXBTCPERP; no duplicates | One perpetual market. |
loop | map | {interval: 60} | only interval | Holds the check cadence. |
loop.interval | integer (seconds) | 60 | whole number ≥ 10 | Pause between checks. |
execution | map | taker entries | the four keys below | How entries are placed. |
execution.entry_mode | string | taker | taker, limit | Immediate entries, or resting limit entries with a time limit. |
execution.max_slippage_pct | number (%) | 0.5 | above 0, up to 5 (both modes) | Furthest a taker order may be priced past the top of the book. Every exit uses it. |
execution.limit_offset_pct | number (%) | 0.05 | limit mode: 0 to 5 | How far behind the best price a resting entry sits. |
execution.limit_ttl_seconds | integer (seconds) | 300 | limit mode: 30 to 86400 | How long a resting entry waits before the Bot cancels it. |
sizing | map | required | margin_usd and leverage | Notional per entry. |
sizing.margin_usd | number ($) | required | ≥ 1 | Margin committed per entry and per add. |
sizing.leverage | number | required | above 0, up to 50 (dip_reverter: up to 2) | Multiplies margin into notional. The market's own cap applies when the Bot runs. |
risk | map | none (no exits) | the two keys below | The Bot's own percent exits. |
risk.take_profit_pct | number (% of price) | none (off) | above 0 | Close the whole position after this favorable move from the average entry. |
risk.stop_loss_pct | number (% of price) | none (off); required for dip_reverter | above 0, below 100 | Close the whole position after this adverse move from the average entry. |
funding_awareness | boolean | false | true, false | Skip entries on the side that currently pays funding, and let rules read the funding estimate. |
indicators | list | none | custom only; no fixed maximum | Indicators your rules read by name. |
indicators[].name | string | required | lowercase letters, digits and _, starting with a letter, up to 31 characters | The field name rules use. |
indicators[].kind | string | required | sma, ema, rsi, atr, bollinger_upper, bollinger_lower, donchian_high, donchian_low | What to compute. |
indicators[].window | integer (candles) | required | 2 to 400 | Look-back length. |
indicators[].timeframe | string | required | 1m, 1h, 4h, 1d | Candle size. |
params | map | {} (all defaults) | the chosen archetype's keys | Tunes an archetype. Ignored on custom. |
params.timeframe | string | 1h | 1m, 1h, 4h, 1d | Candle size for any archetype. |
params.fast_window | integer | 9 | ≥ 1, below slow_window | trend_rider: fast moving average length. |
params.slow_window | integer | 21 | ≥ 1 | trend_rider: slow moving average length. |
params.ma_kind | string | ema | ema, sma | trend_rider: exponential or simple averages. |
params.mode | string | long_short | long_short, long_only, short_only | trend_rider: which sides it trades. |
params.confirm_closes | integer | 1 | ≥ 1 | trend_rider: agreeing closed candles needed before an entry. |
params.atr_stop_multiple | number | trend_rider: 0 (off); breakout_hunter: 2.0 | trend_rider: ≥ 0; breakout_hunter: above 0 | Places a fixed stop this many ATRs from the average entry price. |
params.atr_window | integer | 14 | ≥ 1 | ATR look-back for the ATR stop or dip_reverter's volatility pause. |
params.entry_window | integer | 20 | ≥ 1 | breakout_hunter: channel length for entries. |
params.exit_window | integer | 10 | ≥ 1 | breakout_hunter: channel length for exits. |
params.midline_filter | boolean | true | any | breakout_hunter: has no effect today. |
params.rsi_window | integer | 14 | ≥ 1 | dip_reverter: RSI look-back. |
params.rsi_oversold | number | 30 | above 0, below 100, below rsi_overbought | dip_reverter: RSI level for long entries. |
params.rsi_overbought | number | 70 | above 0, below 100 | dip_reverter: RSI level for short entries. |
params.bb_window | integer | 20 | ≥ 1 | dip_reverter: Bollinger band look-back. |
params.bb_mult | number | 2.0 | above 0, up to 5 | dip_reverter: band width in standard deviations. |
params.vol_pause_mult | number | 1.5 | ≥ 1 | dip_reverter: pause entries while volatility has jumped this much. |
rules | list | none | custom: at least 1; no fixed maximum | Condition and action pairs, checked top to bottom. The first match acts. |
rules[].name | string | required | 1 to 64 letters, digits, spaces, _ or -, starting with a letter or digit | Label shown in logs and decisions. |
rules[].when.all / rules[].when.any | list of conditions | one required; write only one (if both, any is ignored) | 1 or more conditions | Every condition (all) or at least one (any). |
rules[].when.*[].field | string | required | a base field, a MACD field, an indicator name or a setup name | The live value on the left. |
rules[].when.*[].op | string | required | > < >= <= == crosses_above crosses_below | The comparison, or a crossover event. |
rules[].when.*[].value | number | one of value / value_field | a finite number | Fixed number on the right. |
rules[].when.*[].value_field | string | one of value / value_field | any readable field | Another live field on the right. |
rules[].action | string | required | open_long, open_short, add, reduce, close | What the Bot does when the rule fires. |
rules[].size_pct | number (%) | 100 for reduce | reduce: above 0, up to 100; other actions: only 0 or 100 | Share of the position a reduce closes. |
rules[].arm / rules[].clear | list of setup names | none | declared setups, never the same one in both | Setups this rule turns on or off when it fires. |
setups | list | none | custom only, at most 8 | Named per-market flags rules can read as 1 or 0. |
setups[].name | string | required | same pattern as indicator names; can't reuse a field, MACD or indicator name | The field name rules use. |
setups[].expires_after_seconds | integer (seconds) | none (never expires) | loop.interval to 604800 | Clear the setup this long after it was last armed. |
setup_rules | list | none | at most 16; needs declared setups | Rules that only arm or clear setups. Every match applies. |
setup_rules[].name | string | required | same pattern as rule names | Label shown in logs. |
setup_rules[].when | conditions | required | same as rules[].when | When the setup rule applies. |
setup_rules[].arm / setup_rules[].clear | list of setup names | at least one of the two | declared setups, never the same one in both | Setups to turn on or off. |
Document root
version
Always 1, written as a plain number. version: "1" in quotes fails to parse, and any other number fails with version must be 1.
platform
Always kalshi_perps, in lowercase. Kalshi_Perps and kalshi-perps fail with platform must be kalshi_perps.
strategy
| Value | What it is |
|---|---|
trend_rider | Moving-average trend following, long, short or both. |
breakout_hunter | Channel breakouts with a built-in ATR stop. |
dip_reverter | RSI and Bollinger band mean reversion, with fixed guardrails. |
custom | Your own indicators, rules and setups. |
The three archetypes are tuned only through params. rules, indicators, setups and setup_rules are rejected on them. A custom strategy needs at least one rule, and it ignores params. Any other value fails with unknown perps strategy.
strategy_name and strategy_name_origin
strategy_name is the name Studio shows. Studio trims it and keeps the first 80 characters. strategy_name_origin records where the name came from: user (you typed it), generated (Studio named it) or curated (a Turbine strategy). Any other value, including the older legacy, is saved as generated. Neither key changes how the Bot trades.
Use strategy_name, not name. Event-contract documents accept name, but here it is an unknown top-level key.
Unknown and misspelled keys
At the top level, an unknown key is an error. This catches keys copied from event-contract strategies, such as name, market, edge and trading_schedule:
# Rejected: `name` is an event-contract key; Kalshi Perps uses `strategy_name`
# error: name: unknown top-level field
version: 1
platform: kalshi_perps
strategy: trend_rider
name: BTC trend
markets:
- ticker: KXBTCPERP
sizing:
margin_usd: 10
leverage: 1Inside a block, unknown keys are ignored without an error, and the setting you meant falls back to its default:
risk: {max_loss: 50}gives no loss limit at all, andrisk: {stoploss_pct: 3}gives no stop-loss.execution: {entry_mod: limit}runs in taker mode.size: 50on areducerule closes 100% of the position. The key issize_pct.expires_after: 360on a setup means it never expires. The key isexpires_after_seconds.actionon a setup rule does nothing. Setup rules never trade.
The one checked block is archetype params: an unknown key there fails with params.<key>: unknown param for <strategy>, including a key that belongs to a different archetype.
Warning: Check key names against the parameter table. A misspelled
riskorexecutionkey saves without an error. A missing stop-loss gets a deploy warning only whensizing.leverageis above 1, and even then the deploy goes ahead.
Whole numbers and other format rules
- Whole-number keys drop fractions.
version,loop.interval,execution.limit_ttl_seconds,indicators[].windowandsetups[].expires_after_secondsare cut down to a whole number before they are checked.interval: 60.5runs as 60, andinterval: 9.9becomes 9 and is rejected. - Archetype windows must be whole. In
params,9.0works but9.5fails withmust be a positive integer. - Zero means default.
0is replaced by the default forloop.interval,max_slippage_pct,limit_offset_pct,limit_ttl_seconds(limit mode) and areducerule'ssize_pct. - Blocks are mappings.
loop: 60andexecution: takerfail to parse. Writeloop: {interval: 60}andexecution: {entry_mode: taker}. Eachmarketsentry is a mapping too (- ticker: KXBTCPERP), not a bare string. - One document. Anything after a
---line is ignored, and saving through Studio removes it. - Syntax. Duplicate keys and tab indentation fail to parse. JSON works. YAML anchors and aliases work, but a top-level
<<:merge is an unknown key.
What happens on every tick
Every tick follows the same order, whatever the strategy type. Knowing it explains most surprises: why a rule didn't run, why an exit came first, why a later market was skipped.
One tick of a Kalshi Perps Bot
- Once per tick
- The Bot's own exits
- Your strategy
limit_ttl_seconds.Step 3 · For each market, in list order
- computes indicators and MACD,
- updates crossover memory,
- clears expired setups,
- applies every matching setup rule,
- runs the first matching trading rule, which ends this market's tick.
ErrorAn error on one market (an unlisted ticker, a failed account request) skips only that market for this tick: the markets after it still get their exit checks and strategy.
loop.interval secondsThen start the next tick from step 1.loop.interval seconds (60 by default) and starts again. Crossover memory and setups live only in the running Bot, so a stop, restart, redeploy or update clears them; the position and its ATR stop carry over.The Bot cancels its resting entries whose
limit_ttl_secondshas passed.It updates its own position from its fills.
For each market, in list order:
- It reads the market and account state: prices, its position, P&L, your available balance, the exchange's liquidation estimate and, when
funding_awarenessis on, the funding estimate. - It runs its own exits: take-profit, then stop-loss, then the archetype ATR stop, which an archetype first places if its open position has none. If one of them sends a close, the strategy is skipped for that market on this tick.
- Otherwise it runs the strategy: the archetype's logic, or your custom rules in evaluation order.
If anything fails for one market, such as a failed request to Kalshi, the Bot skips only that market for this tick and goes on to the next.
- It reads the market and account state: prices, its position, P&L, your available balance, the exchange's liquidation estimate and, when
It waits
loop.intervalseconds and starts again.
Note: Crossover history and setups live only while the Bot runs: a stop, restart, redeploy or update clears them. The Bot's position and its ATR stops carry over. The position is rebuilt from the Bot's own fills, and each ATR stop is saved with it.
Backtesting and paper trading
Historical backtesting is not available for Kalshi Perps. Studio marks every perps strategy as unsupported for backtests. To see a strategy run before committing real money, deploy it as a paper Bot, and see Backtesting for the venues that support it.
A paper Bot reads live Kalshi market data and simulates your account. Its fills and costs differ from live trading:
- Taker orders fill completely at the top of the book when it is within the order's limit. Resting orders fill only when the price trades through them.
- Fees are assumed at 5 basis points of notional for taker fills and 1 for maker fills.
- Funding is charged every hour at the live estimate. Kalshi itself settles funding a few times a day.
- Nothing is liquidated, and
liquidation_priceis always missing. - Leverage below 1 is treated as 1 for margin.
- The starting balance is the one you choose. If you don't choose one, it is your live perps balance when Studio could read it during the deploy, or $1,000 otherwise.
Deploying and running a Perps Bot
Before you deploy
- Fund the perps balance. Perps Bots use your Kalshi API key but trade a separate perpetuals balance, which you fund in the Kalshi app. When you deploy with new credentials and that balance is $0, Studio warns you that the Bot can't trade until it is funded.
- Leverage without a stop. Studio warns when
sizing.leverageis above 1 andrisk.stop_loss_pctisn't set. Both are warnings: the deploy goes ahead.
Deployment risk limits
Perps Bots read only some of your Studio deployment risk limits:
| Limit | What it does on a Perps Bot |
|---|---|
| Max dollars per order | Skips an entry whose size is above it. Size here is contracts × the order's limit price, which for a taker entry includes the max_slippage_pct bound. Studio pre-fills it with margin_usd × leverage plus that bound, rounded up to whole dollars: $51 for $25 × 2 at 0.5%. When the running Bot's limits can be verified, the dialog starts from those limits instead. If readback is missing or unverified, review the suggested limits before confirming an update; see deployment risk settings. If you raise margin_usd or leverage, check this limit too or entries can be skipped. Closes are never blocked. |
| API error limit and Error window minutes | Once that many API errors happen within the window, pauses new entries and adds for one more window, with halted: API error breaker tripped ..., then resumes on its own. The deploy dialog starts at 3 errors in 5 minutes. Closes are still attempted. |
The deploy dialog shows only these two for Perps. Max contracts per order, Max open contracts, Max daily notional traded and Max daily loss are not read by Perps Bots. Your strategy's sizing and risk are its size controls.
Perps Bots also space their order attempts at least 1 second apart. An entry skipped by this pacing tries again on the next tick if its signal or comparison still holds; a crossover entry doesn't. Closes aren't paced, but each close restarts the 1-second wait. See markets for how this affects several markets on one tick.
Note: Failed requests to Kalshi count toward the API error limit, including rejected orders, but failed funding estimates don't. A tripped limit clears after one error window without a restart.
Stops, restarts and updates
- Stopping a Perps Bot cancels its own resting orders and leaves its positions open. From then on no take-profit, stop-loss or rule protects them, so close them in the Kalshi app if you don't want to hold them.
- Restarting, redeploying or updating keeps the Bot's position, rebuilt from its own fills, and its ATR stops, which are saved with it. The clean stop beforehand cancels its resting orders. It clears crossover memory and setups, and the first reading after the restart never counts as a crossover. Turbine occasionally updates the software your Bots run on, which restarts each Bot once; that restart clears crossover memory and setups the same way. An update that rebuilds the runner also clears the Bot's saved position, so the Bot first rebuilds its position from its own orders and fills on Kalshi, holding entries and exits until it has (the log shows
holding: rebuilding this Bot's Position), and then places its ATR stop again from the rebuilt average entry. See Upgrading a runner. - Stop all on your Kalshi Bots also stops your Perps Bots, but its optional sale covers Kalshi event-contract positions only. Perps positions stay open; close them in the Kalshi app. See Run.
- Removing a market with Update Bot while the Bot holds a position there leaves that position unmanaged. Close it first.
Several Bots, one account
All your Perps Bots trade one cross-margined account. Each Bot tracks its own position from its own fills, and its take-profit, stop-loss and rules close only that position, limited to what the account still holds. available_balance and liquidation_price belong to the account, not to any one Bot, and liquidation applies across the whole account: one Bot's losses can liquidate what the others hold. Watch margin across all your Perps Bots together; see Monitoring and Risk.