Custom Rules
A strategy: custom Bot is an ordered list of if-then rules. On every loop tick the Bot reads a fresh snapshot of the market and your account, walks your rules from the top, and runs the action of the first rule whose conditions are all true.
This section documents every key a rule can use, every field a condition can read, what each action does on each venue, and exactly what Studio rejects when you save. It is split into the pages listed under In this section. This page covers what every rule shares: the rule anatomy, the full rule keys table, and how rules are evaluated on each tick.
Custom rules work on Kalshi, Polymarket and Polymarket US. The grammar is the same everywhere, but the available fields and the way orders are placed differ by venue, and each page in this section calls out the differences. Kalshi Perps and Arbitrage use the same when / field / op shape with their own fields and actions, and are documented on their own pages.
Note: Every example in this section is illustrative. Market tickers, slugs, thresholds and sizes are small placeholder values chosen to show the syntax. They are not recommendations, and no rule set guarantees a result. Start small, backtest where you can, and read Risk & Limits before you deploy.
In this section
- Custom Rule Conditions and Missing Data: The condition keys,
allvsany, operators,valuevsvalue_field, durations fortime_to_expiry, and what happens when data is unavailable, a tick is skipped or a rule errors. - Custom Rule Field Catalog: Every field a custom rule can read, with units, venue availability and backtest support, how each venue computes it, and the edge data and derived metric fields.
- Custom Rule Actions: Every custom rule action and exactly what it does on each venue and in backtests, plus how
sizeand price bounds work. - Custom Rule Validation: What Studio checks when you save a custom strategy: the three validation passes, the required exit rule, the behavior checks, and what the checker cannot see.
- Custom Rule Exit Checks: Exit rules the validator rejects (selling whenever you hold, no hold zone, exits that fire right after entry), each with a rejected strategy and a version that saves.
- Custom Rule Entry Checks: Entry and rule-set problems the validator rejects (contradictions, always-matching entries, unreachable rules and oversized entries), each with a rejected strategy and a version that saves.
- Custom Rule Example Strategies: Seven complete custom strategies that save as written: three for Kalshi, two for Polymarket and two for Polymarket US.
Rule anatomy
A rule has a name, a when block of conditions, and an action. This is a complete rule:
rules:
- name: stop_loss # label shown in logs and errors
when:
all: # all = AND, any = OR
- {field: position_size, op: ">", value: 0}
- {field: unrealized_pnl, op: "<=", value: -0.15}
action: sell_all # runs when this is the first matchEach condition compares a live field with an operator (op) against either a fixed value or another field (value_field); Conditions covers every condition key. A buy rule usually adds a size.
Rule keys
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
rules | list of rules | none | 1 or more rules; required for strategy: custom | The ordered rule list, checked top to bottom every tick. See How rules are evaluated. |
rules[].name | text | none (required) | Any non-empty text; use letters, digits, spaces, _ and - | A label for decision logs and validation errors. See name. |
rules[].when | mapping | none (required) | Exactly one of all or any | The rule's trigger. See all and any. |
rules[].when.all | list of conditions | none | 1 or more conditions | Matches when every condition is true. |
rules[].when.any | list of conditions | none | 1 or more conditions | Matches when at least one condition is true. |
rules[].action | text | none | buy_yes, buy_no, sell_yes, sell_no, sell_all, cancel_all, skip | What the rule does when it is the first match. Exactly one of action or orders. See Actions. |
rules[].size | whole number | 1 for buys | Positive, at most risk.max_position | Contracts (Kalshi, Polymarket US) or shares (Polymarket) to buy. See Size rules. |
rules[].orders | list | none | Kalshi only | Composite resting buy orders instead of action. See Advanced keys. |
rules[].max_combined_price | number | none | Above 0, at most 1; Kalshi only | Caps the combined price of a paired YES/NO composite quote. |
rules[].entry_profit_offset | number (dollars) | none | Above 0, below 1; Kalshi only | Per-fill take-profit on a sell_yes / sell_no rule. |
rules[].entry_stop_loss_offset | number (dollars) | none | Above 0, below 1; Kalshi only | Per-fill stop-loss on a sell_yes / sell_no rule. |
rules[].entry_profit_targets | list of rows | none | Kalshi only | Per-fill take-profit table keyed by entry price. |
rules[].entry_profit_min_time_to_expiry | duration | none | Whole s, m or h, such as "2h"; Kalshi only | Limits a profit table to fills made before a time-to-close cutoff. |
rules[].trading_schedule | mapping | none | Kalshi buy_yes / buy_no rules only | Weekly hours when this entry rule may buy. |
Any other key is rejected when you save, including conditions, id, description, price, order_type and side (for example unsupported custom rule key "conditions"). Conditions always live under when.all or when.any.
rules only runs with strategy: custom. On Kalshi and Polymarket, a built-in strategy saves a rules block but never runs it, and does not check its fields, operators or actions. Polymarket US rejects it (Polymarket US built-in strategies do not support top-level rules). See Built-in Strategies.
name
name is required and is only a label. It does not affect priority; the position of the rule in the list does. It appears in the decision log when the rule fires, and validation errors list rule names so you can find the problem.
Keep names to letters, digits, spaces, underscores and hyphens, and make each one unique. Double quotes and backslashes in a name are not supported: a name such as take "profit" saves, but the Bot then fails to start. Duplicate or blank names save too, but they make the decision log ambiguous.
Advanced keys (Kalshi only)
Seven rule keys add Kalshi-only behavior on top of the basic grammar: composite maker orders (orders, max_combined_price), per-fill exits that manage each purchase at its own entry price (entry_profit_offset, entry_stop_loss_offset, entry_profit_targets, entry_profit_min_time_to_expiry), and a rule-level trading_schedule for entries. Polymarket and Polymarket US custom strategies reject all seven. They are documented in Advanced Kalshi Rules.
How rules are evaluated
One tick, step by step
Every tick, for each market the strategy trades:
- The Bot reads a fresh snapshot: prices and the order book, your position, your resting orders, your balance, and any edge data or derived metrics you declared.
- If a declared data source is unavailable, its fields read as unavailable and new entries are blocked for that market this tick; exits still run. See Missing or unavailable data.
- Rules are checked in file order. Every condition of a rule is evaluated, then combined with
allorany. - The first rule that matches runs its action, and evaluation stops for that market until the next tick. One matched rule, one action. The exception is a buy rule that matches while entries are blocked (see When entries are blocked): it is skipped and the rules below it still run.
- If no rule matches, nothing is traded and the decision log records "no rule condition met".
After the whole cycle finishes, the Bot waits loop.interval seconds (at least 10) and starts again. The interval is a pause between cycles, not a fixed clock. With a series or a multi-market selection on Kalshi, each market gets its own snapshot and its own first match, and an error in one market does not stop the others.
Rules are not evaluated at all while a Kalshi strategy is halted by risk.max_loss (the Bot instead cancels its orders and sells the position this run opened), or before an active_window starts (Kalshi and Polymarket). When the window ends, the Bot cancels its own resting buys once and blocks new entries, but keeps evaluating rules so exits still run; it never closes a position on its own. See When entries are blocked.
One tick of a custom strategy
- Exit rule
- Guard (skip)
- Entry rule
- 1stop_
lossholding and P&L ≤ −$0.15 → sell_allNo match: you are flat - no match, try the next rule2take_
profitholding and P&L ≥ +$0.20 → sell_allNo match: you are flat - no match, try the next rule3pause_
when_ illiquid_ or_ low_ cashspread > 0.05 or balance < $20 → skipNo match: spread 0.02, balance $120 - no match, try the next rule4buy_
no_ when_ yes_ richflat and 0.80 ≤ price ≤ 0.90 → buy_noMatchedBuys 1 NO at the NO ask. No later rule runs this tick.
active_window.end, during an edge data outage, or on Kalshi by a trading schedule, an older series market or Max daily loss) is logged as blocked and evaluation continues to the next rule. Not shown: rules do not run at all while a Kalshi strategy is halted by risk.max_loss, or before an active_window starts (Kalshi and Polymarket).skip is a real match: it places no order, but it still ends evaluation for the tick. A buy rule that matches and is then held back by a risk limit (for example risk.max_entries_per_market or risk.max_notional) also counts as the match for that tick.
On Kalshi, the Studio live terminal records each tick as a decision tree: every rule and condition is shown with the recorded value, the threshold, and a status of Met, Not met, Not evaluated, Not recorded (the value was unavailable), Blocked or Error. A record covers at most 16 markets, 32 rules and 16 conditions per rule; anything beyond that is left out and the record is marked incomplete. On Polymarket and Polymarket US, the decision log shows which rule fired, or that no rule matched, but not the value of each condition.
Rules have no memory
Rules see only the current snapshot. There is no "I already entered" flag and no "price crossed above" event. Express that kind of state through the account:
position_size == 0to allow an entry only while flat,position_size > 0for exits.order_count == 0to avoid stacking a second order on top of a resting one.risk.max_entries_per_marketto cap how many entries a market can get (Kalshi and Polymarket; Polymarket US rejects it). On a regular Polymarket rolling series this cap is not currently recommended: once one market reaches it, entries stay blocked in every later market of the series until you redeploy, so preferposition_sizeandorder_countgates there.
Comparisons test the relationship on each tick. {field: edge.btc.price, op: ">", value_field: edge.btc.ema_12_1m} is true on every tick where spot is above its EMA, not only on the tick it crosses.
Kalshi: blocked entries fall through
On Kalshi, if a matching entry (buy_yes, buy_no, or a composite orders rule) is held back because it is outside a trading schedule, or because the market is an older market of your series kept only so the Bot can manage its position, the Bot records the rule as blocked and keeps going to the next rule on the same tick. On Polymarket and Polymarket US evaluation is strictly first match.
In backtests
Backtests use the same top-to-bottom, first-match logic. Rules are evaluated at each candle close, and the matched action executes at the next candle's open, so there is at most one action per candle. Kalshi and Polymarket custom strategies can be backtested; Polymarket US custom strategies cannot. Some fields are simulated or unavailable in backtests; the field catalog lists which.
Recommended rule order
Because the first match wins, order is behavior. A safe default:
- Safety exits such as a stop-loss or a flatten-before-close rule, gated on
position_size > 0. - Take-profit exits, with the same gate.
- Guards:
skipto stand aside when conditions are poor,cancel_allto pull resting orders. - Entries, gated on
position_size == 0(or a size cap) plus a price band.
Warning: Two ordering mistakes pass validation. An exit without a position gate that is true at a P&L of 0, such as
unrealized_pnl >= 0on its own, matches every tick while you are flat (P&L is 0 when flat) and starves every entry below it. Askipplaced above your exits blocks the exits too. Gate exits on holding a position, and keep guards below exits.