Custom Rule Conditions and Missing Data
A condition is one comparison inside a custom rule's when block, and a rule fires only when its conditions match. This page covers how to write conditions for custom strategies on Kalshi, Polymarket and Polymarket US, then what happens when the data a condition reads is missing. For where conditions sit in a rule and how rules are checked each tick, see Rule anatomy and How rules are evaluated.
Conditions
Condition keys
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
field | field name | none (required) | A built-in field, edge.<alias>.<field> or derived.<name>, allowed on your venue; case-sensitive | The live value on the left side of the comparison. |
op | text | none (required) | <, >, <=, >=, ==, != (always quoted) | The comparison. See Operators. |
value | number, or duration | none | A plain finite number; a duration string for time_to_expiry | A fixed threshold. Exactly one of value or value_field. See value and value_field. |
value_field | field name | none | Any numeric field valid for field on your venue | Compares against another live field instead of a number. |
No other keys are allowed in a condition (unsupported condition key "operator" (allowed: field, op, value, value_field)). A condition must be a mapping, so a string such as "price < 0.4" is a YAML error.
all and any
when holds exactly one group:
allmatches when every condition is true (AND). Two conditions on the same field make a band, such asprice >= 0.30andprice <= 0.40.anymatches when at least one condition is true (OR).
Setting both gives set exactly one of all/any, not both; setting neither, or only empty lists, gives set at least one of all/any. Other group names (none, not) are rejected. Groups do not nest: an any inside an all list is rejected. To express "(A and B) or C", write two rules with the same action.
YAML anchors work if you want to reuse a condition. Define it once with - &flat {field: position_size, op: "==", value: 0} and reuse it in another rule as - *flat. Merge keys (<<:) are rejected inside rules and conditions.
Operators
op | Meaning | Quoting |
|---|---|---|
"<" | Less than | Recommended |
"<=" | Less than or equal | Recommended |
">" | Greater than | Required |
">=" | Greater than or equal | Required |
"==" | Equal | Recommended |
"!=" | Not equal | Required |
Always quote operators. In the one-line
{field: ..., op: ..., value: ...}style used throughout the Custom Rules docs, an unquoted>,>=or!=is a YAML error. Written on its own line (op: >), an unquoted>or!=is read as empty (unknown operator "") and>=is a YAML error. Anything else, such as=,=>orcontains, givesunknown operator.
There are no crossover operators in custom rules. == and != compare exactly, so prefer ranges for computed values like price and unrealized_pnl: price >= 0.50 is safer than price == 0.50. A comparison with an unavailable value is always false, whatever the operator.
value and value_field
value is a plain number. Write value: 0.4, not value: "0.4"; a quoted number is rejected with price requires a numeric value (got string) — remove the surrounding quotes. Booleans, null, .inf and .nan are rejected too. There are two exceptions: time_to_expiry takes a duration string, and a text edge field takes one of its text values.
Units matter, and most mistakes here are unit mistakes:
- Prices and spreads are dollars per contract, from 0 to 1. Write
0.40for 40 cents.price > 40can never be true and is rejected as contradictory;price < 40is always true. unrealized_pnlandbalanceare dollars. P&L covers the whole position, not one contract.position_size,order_countandsizeare counts of contracts, shares or orders.- Edge fields use their provider's units. NWS humidity and precipitation probabilities are fractions (
0.70, not70), and values outside 0 to 1 are rejected for them.
value_field compares two live fields read from the same snapshot, for example yes_best_ask against no_best_ask, or a Coinbase price against its EMA. Both sides must be numeric fields allowed on your venue. Setting both value and value_field is rejected (exactly one of value or value_field must be set, not both), and so is setting neither.
Studio does not check units across a value_field comparison. Comparing a dollar field with a seconds field saves, so make sure both sides mean the same thing. Behavior validation also cannot analyze value_field conditions; see What the checker cannot see.
Durations for time_to_expiry
time_to_expiry is the only built-in field that takes a duration string instead of a number: a whole number followed by one lowercase unit.
| Unit | Example | Seconds |
|---|---|---|
s | "90s" | 90 |
m | "15m" | 900 |
h | "6h" | 21,600 |
d | "1d" | 86,400 |
Invalid forms and their errors: a bare number such as 3600 (time_to_expiry requires a duration string (e.g. "6h")), "300" with no unit (unknown duration unit "0" in "300" (want s|m|h|d)), decimals such as "1.5h" (invalid duration magnitude "1.5"), and compound, weekly or uppercase units such as "1h30m", "1w" or "2H". Write "90m" instead of "1h30m". Durations are converted to seconds when the strategy is compiled.
Edge fields measured in seconds, such as the economic calendar's seconds_to_next, take plain numbers, not duration strings.
Missing or unavailable data
Most missing data fails closed: rules on that data stay off rather than trading on a guess. A few values fall back to a number instead, and those are listed under Unavailable values. How that plays out depends on what is missing.
Unavailable values
A single value can be unavailable for a tick, for example a side of the order book that is empty, or an edge value the provider could not compute (a late Coinbase candle, an NWS forecast outage). Every comparison with an unavailable value is false, whatever the operator, including !=, and that applies to either side of a value_field comparison. The condition fails, so an all rule does not match; an any rule can still match through its other conditions. Later rules still run. Backtests behave the same way.
Some fields fall back to a number instead of going unavailable:
| Field | When data is missing |
|---|---|
price | Kalshi: last trade, else 0.00. Polymarket: ask, bid, then last trade. Polymarket US: current price, then midpoint. |
spread | A missing ask counts as 1.00 and a missing bid as 0.00, so the spread reads wide. |
time_to_expiry | 0 when the close time is unknown, so near-close exits fire and late-entry gates block. |
unrealized_pnl | Kalshi: a held side with no bid is marked at $0. Polymarket: unavailable. |
balance | Polymarket and Polymarket US: 0.00 if it cannot be read. |
position_size | Polymarket: a token whose balance cannot be read counts as 0, so an entry gated on position_size == 0 can match while you hold. |
order_count | Polymarket: if the open-orders read fails, only the orders that loaded are counted. |
NWS alert and forecast-text fields also fall back rather than going unavailable: during an alert outage alert_active reads 0, and during a forecast outage precip_amount_in reads 0. See Edge Data.
When entries are blocked
Some situations block only new entries. Rules are still evaluated: a matching buy_yes, buy_no or composite orders rule is skipped with a blocked decision, and the rules below it, exits included, still run.
| Situation | Kalshi | Polymarket | Polymarket US |
|---|---|---|---|
| A declared edge alias is stale beyond its limit | Entries blocked; this Bot's resting entry orders on the market are canceled | Edge data not supported | Entries blocked; resting orders stay |
| A derived metric's strike or input is unavailable | Entries blocked; this Bot's resting entry orders on the market are canceled | Not supported | Not supported |
After active_window.end | Entries blocked in the markets it still holds or has orders in | Entries blocked in its market | Not supported |
| Max daily loss reached | Entries blocked until 00:00 UTC | Not read | Not read |
Outside a trading_schedule | Entries blocked | Not supported | Not supported |
An exit whose own conditions read the missing edge or derived value can't fire until it is back, because those conditions are false; exits on price, the book, position or P&L run as usual. Watch data freshness in Monitoring.
When the tick is skipped
Two situations stop rule evaluation for a market altogether:
| Situation | Kalshi | Polymarket | Polymarket US |
|---|---|---|---|
risk.max_loss has halted the strategy | Rules not evaluated; the Bot cancels its orders and sells the position this run opened | Not supported | Not supported |
Before active_window.start | Rules not evaluated | Rules not evaluated | Not supported |
The decision log records why the tick was skipped.
Errors end the tick
If reading the market or your account fails (a venue API error, for example), or a rule raises an error while being evaluated or executed, the rest of that market's tick is abandoned: no later rule runs, including exits. The next tick starts fresh. On a Kalshi series, other markets in the same cycle still run. On Polymarket, a failed read of the trade history behind unrealized_pnl does not end the tick: P&L reads as unavailable and the other rules still run.
Rule-level causes of errors to avoid: a Kalshi sell with no bid on that side, a Kalshi buy while either side of the book is empty, any Kalshi order while the market's most recent trade is more than 120 seconds old, a Kalshi or Polymarket US buy larger than your Max contracts per order, and a Polymarket US sell_all with no bid to sell into.