Custom Rule Exit Checks
When you save a strategy: custom strategy, Studio's behavior checks reject exit rules that would never let the Bot hold a position, or that sell right after each entry. Each section below shows one of the three exit problems they catch as a rejected strategy (Kalshi or Polymarket US) with the error text it produces, followed by a corrected version that saves. For contradictions, unreachable rules and entry problems, see Entry Checks.
Selling whenever you hold
This is the most common rejection. An exit whose only condition is position_size > 0 sells in every state you could hold in: profitable, neutral and losing. It also means the Bot sells right after each entry. One rule trips three checks:
# Rejected: the only exit sells whenever anything is held
# error: exit rules collectively match every held-position state
# error: exit predicate fires in profitable, neutral, and losing states
# error: entry and exit rules deterministically alternate on unchanged market state
version: 1
platform: polymarket_us
strategy: custom
strategy_name: "Sell whenever held (rejected)"
market:
slug: "example-market-slug"
risk:
max_position: 1
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 30
rules:
- name: exit_whenever_held
when:
all:
- {field: position_size, op: ">", value: 0}
action: sell_all
- name: buy_yes_dip
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: price, op: "<=", value: 0.35}
action: buy_yes
size: 1Fixed: give each exit a reason that is false right after entry. On Polymarket US, exits are written on price (see unrealized_pnl).
version: 1
platform: polymarket_us
strategy: custom
strategy_name: "Dip buyer with price exits"
market:
slug: "example-market-slug"
risk:
max_position: 1
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 30
rules:
- name: take_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: ">=", value: 0.55}
action: sell_all
- name: stop_loss
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: "<=", value: 0.20}
action: sell_all
- name: buy_yes_dip
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: price, op: ">=", value: 0.25}
- {field: price, op: "<=", value: 0.35}
action: buy_yes
size: 1A plain sell rule gated only on a larger holding, such as position_size >= 2, is allowed, because it does not fire while you hold a single contract.
Exits that leave no hold zone
Taken together, your exits must leave some held state in which no exit fires. Otherwise the Bot can never keep a position. The usual cause is a stop-loss and a take-profit whose thresholds overlap, here because they were swapped:
# Rejected: swapped stop and take-profit thresholds cover every price
# error: exit rules collectively match every held-position state
version: 1
platform: kalshi
strategy: custom
strategy_name: "Swapped exits (rejected)"
market:
series_ticker: KXHIGHNY
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 30
rules:
- name: stop_loss
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: "<=", value: 0.70}
action: sell_all
- name: take_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: ">=", value: 0.30}
action: sell_all
- name: buy_yes_in_band
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: price, op: ">=", value: 0.35}
- {field: price, op: "<=", value: 0.45}
action: buy_yes
size: 1Fixed: the Bot holds while the YES price is between 0.30 and 0.60, and the entry band sits inside that zone.
version: 1
platform: kalshi
strategy: custom
strategy_name: "YES band with a hold zone"
market:
series_ticker: KXHIGHNY
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 30
rules:
- name: stop_loss
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: "<=", value: 0.30}
action: sell_all
- name: take_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: ">=", value: 0.60}
action: sell_all
- name: buy_yes_in_band
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: price, op: ">=", value: 0.35}
- {field: price, op: "<=", value: 0.45}
action: buy_yes
size: 1Exits that fire right after entry
Right after a buy fills, the position exists and prices have not moved. The validator models that state as P&L $0.00 (live, it starts slightly below 0 because of the spread and fees; see unrealized_pnl). If the first rule that matches that state is an exit, the Bot sells, is flat again, and the same entry buys again on the next tick: a buy-sell loop that only pays spreads and fees. The validator simulates this on sample states and rejects the loop.
The entry/exit loop the validator blocks
immediate_entry_exit_loop. The replay tries a limited set of prices, so a narrower entry band can hide the same loop from the check. Live, P&L right after a fill starts slightly below 0.00 because of the spread and fees.# Rejected: the take-profit accepts P&L 0, the state right after a fill
# error: entry and exit rules deterministically alternate on unchanged market state
version: 1
platform: kalshi
strategy: custom
strategy_name: "Breakeven exit (rejected)"
market:
series_ticker: KXHIGHNY
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 30
rules:
- name: stop_loss
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: unrealized_pnl, op: "<=", value: -0.15}
action: sell_all
- name: take_any_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: unrealized_pnl, op: ">=", value: 0}
action: sell_all
- name: buy_no_when_yes_rich
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: price, op: ">=", value: 0.65}
- {field: price, op: "<=", value: 0.75}
action: buy_no
size: 1Fixed: the take-profit needs the position to gain $0.20 first.
version: 1
platform: kalshi
strategy: custom
strategy_name: "Fade rich YES with a P&L bracket"
market:
series_ticker: KXHIGHNY
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 30
rules:
- name: stop_loss
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: unrealized_pnl, op: "<=", value: -0.15}
action: sell_all
- name: take_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: unrealized_pnl, op: ">=", value: 0.20}
action: sell_all
- name: buy_no_when_yes_rich
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: price, op: ">=", value: 0.65}
- {field: price, op: "<=", value: 0.75}
action: buy_no
size: 1The same applies to price exits: if your entry buys at price <= 0.40 and your exit sells at price < 0.50, every entry price is also an exit price. Put the exit threshold on the far side of the entry band.