Kalshi Perps Example Strategies
This page collects complete Kalshi Perps strategies (platform: kalshi_perps): one trend_rider archetype and six custom strategies. For what each key does, see All Kalshi Perps parameters.
Each example below saves and compiles as written. They show different ways to combine the keys documented in this section. The markets, sizes and thresholds are illustrative only, chosen to be small and easy to follow. They are not recommendations, and nothing here says any of them would be profitable.
Trend entry on a crossover, with a stop
A long-only trend entry that fires once, on the tick the 20-candle EMA crosses above the 50-candle EMA. Two exits retry until flat: the trend turning back, and the price falling below the low of the previous 10 hourly candles. The percent take-profit and stop-loss run before any rule.
version: 1
platform: kalshi_perps
strategy: custom
strategy_name: ETH hourly EMA cross, long only, with stops
strategy_name_origin: user
markets:
- ticker: KXETHPERP
loop:
interval: 60
sizing:
margin_usd: 10
leverage: 2
risk:
take_profit_pct: 5
stop_loss_pct: 2
indicators:
- {name: ema_20, kind: ema, window: 20, timeframe: 1h}
- {name: ema_50, kind: ema, window: 50, timeframe: 1h}
- {name: low_10, kind: donchian_low, window: 10, timeframe: 1h}
rules:
- name: exit when the trend turns
when:
all:
- {field: position, op: ">", value: 0}
- {field: ema_20, op: "<", value_field: ema_50}
action: close
- name: exit below the 10 candle low
when:
all:
- {field: position, op: ">", value: 0}
- {field: mark_price, op: "<", value_field: low_10}
action: close
- name: enter on the bullish cross
when:
all:
- {field: position, op: "==", value: 0}
- {field: ema_20, op: crosses_above, value_field: ema_50}
action: open_longIf the entry is blocked on the crossing tick, for example by the market's leverage cap, it waits for the next crossover. After a stop-loss, the Bot stays flat until the EMAs cross again.
Pullback entry armed by a breakout
A two-stage entry. A setup rule arms breakout_seen when the price crosses above the 20-candle high while the trend is up. The entry waits for the price to pull back to the 20-candle EMA while staying above the 50-candle EMA. The setup expires after 4 hours, and it is cleared if the trend turns or when the entry fires.
version: 1
platform: kalshi_perps
strategy: custom
strategy_name: SOL pullback after a breakout
strategy_name_origin: user
markets:
- ticker: KXSOLPERP
loop:
interval: 60
sizing:
margin_usd: 10
leverage: 2
risk:
take_profit_pct: 4
stop_loss_pct: 2
indicators:
- {name: high_20, kind: donchian_high, window: 20, timeframe: 1h}
- {name: ema_20, kind: ema, window: 20, timeframe: 1h}
- {name: ema_50, kind: ema, window: 50, timeframe: 1h}
setups:
- {name: breakout_seen, expires_after_seconds: 14400} # watch for a pullback for 4 hours
setup_rules:
- name: breakout in an uptrend
when:
all:
- {field: mark_price, op: crosses_above, value_field: high_20}
- {field: ema_20, op: ">", value_field: ema_50}
arm: [breakout_seen]
- name: uptrend lost
when:
all:
- {field: ema_20, op: "<", value_field: ema_50}
clear: [breakout_seen]
rules:
- name: exit below the slow EMA
when:
all:
- {field: position, op: ">", value: 0}
- {field: mark_price, op: "<", value_field: ema_50}
action: close
- name: buy the pullback to the fast EMA
when:
all:
- {field: position, op: "==", value: 0}
- {field: breakout_seen, op: "==", value: 1}
- {field: mark_price, op: "<=", value_field: ema_20}
- {field: mark_price, op: ">", value_field: ema_50}
action: open_long
clear: [breakout_seen]The entry's clear applies whenever the rule fires, even if the order is blocked, so a blocked pullback entry also ends the watch. The setup, like crossover memory, is cleared by a restart.
Trend Rider on three markets, both directions
The trend_rider archetype on three markets at once, using simple averages on 4-hour candles, trading both sides, with an ATR stop and a percent stop-loss as a backstop.
version: 1
platform: kalshi_perps
strategy: trend_rider
strategy_name: Three-market 4h trend, both directions
strategy_name_origin: user
markets:
- ticker: KXBTCPERP
- ticker: KXETHPERP
- ticker: KXSOLPERP
loop:
interval: 60 # 4h signals change every 4 hours; exits are still checked every minute
sizing:
margin_usd: 10 # per entry, per market
leverage: 1
risk:
stop_loss_pct: 4 # percent backstop alongside the ATR stop
params:
timeframe: 4h
ma_kind: sma
fast_window: 10
slow_window: 30
mode: long_short
atr_stop_multiple: 2.5
atr_window: 14Each market is sized separately at $10 of notional. On a market where one contract costs more than $10, entries are skipped with margin x leverage below one contract. When the signal flips, the Bot closes first and opens the other side on a later tick.
Add once, trim once
Uses add, reduce and two setups as "do this once per trade" flags: enter when the price breaks above the previous 20-candle high while above a 50-candle EMA, add once on a fresh breakout, trim half once at $3 of unrealized profit, and exit fully below the EMA.
version: 1
platform: kalshi_perps
strategy: custom
strategy_name: SOL breakout, add once, trim half
strategy_name_origin: user
markets:
- ticker: KXSOLPERP
loop:
interval: 60
sizing:
margin_usd: 15 # every entry AND every add is 15 x 2 = about $30 notional
leverage: 2
risk:
stop_loss_pct: 5 # measured from the weighted-average entry, re-anchors after the add
indicators:
- {name: channel_high, kind: donchian_high, window: 20, timeframe: 1h}
- {name: trend_ema, kind: ema, window: 50, timeframe: 1h}
- {name: atr_1h, kind: atr, window: 14, timeframe: 1h}
setups:
- {name: added}
- {name: trimmed}
rules:
- name: exit below trend
when:
all:
- {field: position, op: ">", value: 0}
- {field: mark_price, op: "<", value_field: trend_ema}
action: close
clear: [added, trimmed]
- name: trim half at 3 dollars profit
when:
all:
- {field: position, op: ">", value: 0}
- {field: trimmed, op: "==", value: 0}
- {field: unrealized_pnl, op: ">", value: 3}
action: reduce
size_pct: 50
arm: [trimmed]
- name: add once on a fresh breakout
when:
all:
- {field: position, op: ">", value: 0}
- {field: added, op: "==", value: 0}
- {field: mark_price, op: crosses_above, value_field: channel_high}
action: add
arm: [added]
- name: enter on breakout above trend
when:
all:
- {field: position, op: "==", value: 0}
- {field: mark_price, op: crosses_above, value_field: channel_high}
- {field: mark_price, op: ">", value_field: trend_ema}
- {field: atr_1h, op: ">", value: 0}
action: open_long
clear: [added, trimmed]The most this can hold is two entries, about $60 of notional. trimmed is armed whenever the trim rule fires, even if 50% rounds down to zero contracts. atr_1h > 0 shows a readiness guard: a condition on an indicator the rule doesn't otherwise need, which holds the rule back until that indicator has a value. Here it changes nothing, because trend_ema needs 50 closed candles and warms up later than the 14-candle ATR.
Bollinger fade with resting entries and a dollar bracket
Mean reversion written as a custom strategy, with passive limit entries. It fades prices outside the Bollinger bands with an RSI filter, exits at the middle band or on a dollar take-profit or stop written as one any rule, only enters while the account has a minimum available balance, and keeps a percent stop-loss as a backstop.
version: 1
platform: kalshi_perps
strategy: custom
strategy_name: DOGE Bollinger fade with resting entries and a dollar bracket
strategy_name_origin: generated
markets:
- ticker: KXDOGEPERP
loop:
interval: 60
execution:
entry_mode: limit # entries rest on the book instead of crossing the spread
limit_offset_pct: 0.05 # longs rest 0.05% below the bid, shorts 0.05% above the ask
limit_ttl_seconds: 60 # = loop.interval: an unfilled entry is re-priced every tick
max_slippage_pct: 1 # closes are still immediate-or-cancel orders, bounded by this
sizing:
margin_usd: 10 # 10 x 2 = about $20 notional per entry
leverage: 2
risk:
stop_loss_pct: 6 # percentage backstop, checked every tick before the rules
indicators:
- {name: bb_upper, kind: bollinger_upper, window: 20, timeframe: 1h} # SMA(20) + 2 standard deviations
- {name: bb_lower, kind: bollinger_lower, window: 20, timeframe: 1h} # SMA(20) - 2 standard deviations
- {name: bb_mid, kind: sma, window: 20, timeframe: 1h} # the middle band
- {name: rsi_1h, kind: rsi, window: 14, timeframe: 1h}
rules:
# any: fires when either condition holds. This Bot's unrealized_pnl is 0 while flat, so it cannot fire then.
- name: dollar bracket
when:
any:
- {field: unrealized_pnl, op: ">=", value: 1.2} # take $1.20 (about 6% on $20)
- {field: unrealized_pnl, op: "<=", value: -0.8} # or cut at -$0.80 (about 4%)
action: close
- name: long back at the middle band
when:
all:
- {field: position, op: ">", value: 0}
- {field: mark_price, op: ">=", value_field: bb_mid}
action: close
- name: short back at the middle band
when:
all:
- {field: position, op: "<", value: 0}
- {field: mark_price, op: "<=", value_field: bb_mid}
action: close
- name: fade below the lower band
when:
all:
- {field: position, op: "==", value: 0}
- {field: available_balance, op: ">=", value: 50} # only while the perps account has $50+ available
- {field: ask, op: "<", value_field: bb_lower}
- {field: rsi_1h, op: "<", value: 30}
action: open_long
- name: fade above the upper band
when:
all:
- {field: position, op: "==", value: 0}
- {field: available_balance, op: ">=", value: 50}
- {field: bid, op: ">", value_field: bb_upper}
- {field: rsi_1h, op: ">", value: 70}
action: open_shortThe entries are comparisons, so while flat and outside the band, the Bot keeps one resting entry on the book. Because limit_ttl_seconds equals loop.interval, an unfilled entry is cancelled on the next tick and replaced at the current price. available_balance is your whole perps account's balance, shared with other Bots, not a per-Bot figure.
MACD momentum on three markets with a 4h trend filter
One rule set run separately on three markets: enter when the 5-minute Coinbase MACD histogram crosses zero in the direction of a 4-hour EMA trend, only while that side isn't paying funding, and exit when the histogram changes sign.
version: 1
platform: kalshi_perps
strategy: custom
strategy_name: 5m MACD momentum on BTC, ETH and SOL with a 4h trend filter
strategy_name_origin: user
markets: # the same rules run separately on each market
- ticker: KXBTCPERP # MACD from Coinbase BTC-USD
- ticker: KXETHPERP # ETH-USD
- ticker: KXSOLPERP # SOL-USD
loop:
interval: 30 # 5m MACD changes once per completed 5-minute candle
execution:
entry_mode: taker
max_slippage_pct: 0.3
sizing:
margin_usd: 15 # per entry, per market: 15 x 2 = about $30 notional
leverage: 2
risk:
take_profit_pct: 3
stop_loss_pct: 1.5
funding_awareness: true # required to read funding_rate_estimate in rules
indicators:
- {name: trend_4h, kind: ema, window: 50, timeframe: 4h} # perp candles, so it compares with mark_price
rules:
# Exits: histogram sign as state comparisons, so they retry until flat.
- name: exit long when momentum fades
when:
all:
- {field: position, op: ">", value: 0}
- {field: macd_histogram_5m, op: "<", value: 0}
action: close
- name: exit short when momentum fades
when:
all:
- {field: position, op: "<", value: 0}
- {field: macd_histogram_5m, op: ">", value: 0}
action: close
# Entries: the histogram crossing zero is the same moment MACD crosses its signal line.
- name: long on bullish 5m MACD above 4h trend
when:
all:
- {field: position, op: "==", value: 0}
- {field: macd_histogram_5m, op: crosses_above, value: 0}
- {field: mark_price, op: ">", value_field: trend_4h}
- {field: funding_rate_estimate, op: "<=", value: 0} # longs are not paying funding right now
action: open_long
- name: short on bearish 5m MACD below 4h trend
when:
all:
- {field: position, op: "==", value: 0}
- {field: macd_histogram_5m, op: crosses_below, value: 0}
- {field: mark_price, op: "<", value_field: trend_4h}
- {field: funding_rate_estimate, op: ">=", value: 0} # shorts are not paying funding right now
action: open_shortEach market keeps its own crossover memory. If two markets cross on the same tick, the second entry can be skipped by order pacing, and because it is a crossover it waits for the next one. The explicit funding conditions repeat the automatic funding veto, with one difference: they also block entries while the estimate is missing. Up to three positions can be open at once, about $90 of notional, all on one margin account. trend_4h needs 50 completed 4-hour candles before entries can fire.
MACD setup, then an EMA trigger, with a re-entry window
A two-stage entry on 1-minute data. A strong bullish MACD cross arms long_setup, and a later 1-minute EMA cross enters. A bearish MACD cross exits (a comparison, so it retries) and opens a 6-minute window in which a fresh MACD cross can re-enter.
version: 1
platform: kalshi_perps
strategy: custom
strategy_name: BTC MACD setup, EMA trigger, re-entry watch
strategy_name_origin: user
markets:
- ticker: KXBTCPERP
loop:
interval: 15 # 1m signals: check several times per candle
sizing:
margin_usd: 20
leverage: 1
risk:
stop_loss_pct: 1
indicators:
- {name: ema_12, kind: ema, window: 12, timeframe: 1m}
- {name: ema_26, kind: ema, window: 26, timeframe: 1m}
setups:
- {name: long_setup} # waits until used or cleared
- {name: long_reentry, expires_after_seconds: 360} # 6-minute re-entry window
setup_rules:
- name: strong bullish MACD cross
when:
all:
- {field: macd_1m, op: crosses_above, value_field: macd_signal_1m}
- {field: macd_histogram_1m, op: ">=", value: 5}
arm: [long_setup]
- name: weak bullish MACD cross
when:
all:
- {field: macd_1m, op: crosses_above, value_field: macd_signal_1m}
- {field: macd_histogram_1m, op: "<", value: 5}
clear: [long_setup]
- name: bearish MACD cross
when:
all:
- {field: macd_1m, op: crosses_below, value_field: macd_signal_1m}
clear: [long_setup]
- name: open re-entry watch at the exit cross
when:
all:
- {field: position, op: ">", value: 0}
- {field: macd_1m, op: crosses_below, value_field: macd_signal_1m}
arm: [long_reentry]
- name: trend turns bearish
when:
all:
- {field: ema_12, op: crosses_below, value_field: ema_26}
clear: [long_reentry]
rules:
- name: exit long on bearish MACD
when:
all:
- {field: position, op: ">", value: 0}
- {field: macd_1m, op: "<", value_field: macd_signal_1m}
action: close
- name: re-enter long inside the watch
when:
all:
- {field: position, op: "==", value: 0}
- {field: long_reentry, op: "==", value: 1}
- {field: ema_12, op: ">", value_field: ema_26}
- {field: macd_1m, op: crosses_above, value_field: macd_signal_1m}
action: open_long
clear: [long_reentry]
- name: enter long on EMA cross after setup
when:
all:
- {field: position, op: "==", value: 0}
- {field: long_setup, op: "==", value: 1}
- {field: ema_12, op: crosses_above, value_field: ema_26}
action: open_long
clear: [long_setup]The re-entry window is armed by a setup rule on the crossing tick, not by the exit rule, so it isn't re-armed on every tick while the exit retries. The histogram threshold of 5 is in USD on BTC-USD; it would need rescaling for any other coin. MACD comes from Coinbase spot, so rules that read it stop firing during a Coinbase data gap, while the stop-loss keeps working. To trade shorts as well, add mirrored setups and rules.