Derived Metrics
A derived metric is a number the Bot calculates from one edge value and the current Kalshi market's strike. Today there is one kind, signed_distance_from_strike: the observed value minus the strike. Rules read it as derived.<name>.
Derived metrics work only in strategy: custom on Kalshi, and each one reads an edge alias declared in the same strategy (see Declaring and Reading Edge Data). This page covers the derived: keys, which markets have a strike, when a metric blocks entries, backtesting, and a recipe that compares both spot and an EMA with the strike. For edge data in general, start at Edge Data and Derived Metrics.
Declaring a derived metric
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
derived | map of metrics | none | 0 to 8 metrics; Kalshi strategy: custom only | Declares numbers calculated from an edge value and the market's strike. |
| metric name | map key | none | same rule as alias names; only kind and observed inside | The name rules read as derived.<name>. |
kind | string | required | signed_distance_from_strike | Observed value minus the market's strike. More |
observed | field reference | required | edge.<alias>.<field>: a declared alias and a listed numeric field | The value the strike is subtracted from. |
- Any other key, such as
strike:,offset:oradjustment:, fails withunsupported derived metric key. The strike always comes from the market, and there is no arithmetic beyond the subtraction. observedmust be an edge field: built-in fields such aspricefail withmust be edge.<alias>.<field>. Text fields fail withmust reference a numeric edge field.- Any numeric edge field is accepted, including raw EMAs. Fields unrelated to a strike, such as
seconds_to_next, also pass validation, but the result is meaningless. - On Polymarket, Polymarket US or a Kalshi built-in strategy,
derived:fails withderived metrics are supported only for custom Kalshi strategies. Kalshi Perps and arbitrage reject it as an unknown top-level key.
Which markets have a strike
The strike comes from the Kalshi market's own strike type, and only one-sided markets have one:
signed_distance_from_strike on a number line
greater, greater_than, greater_or_equal. Positive means the observed value is above the strike, where YES wins.less, less_than, less_or_equal. The sign does not flip: positive still means above the cap, where YES loses.strike_unavailable and the strategy's resting entry orders on that market are cancelled.derived.spot_vs_strike = Coinbase price − the market's strike, with the example rules "exit when below 0" and "enter at 250 or more". Illustrative values on an "above 102,000" BTC market.greater,greater_than,greater_or_equal("above X" markets) use the market's floor strike.less,less_than,less_or_equal("X or below" markets) use its cap strike. The sign does not flip: a positive distance still means the observed value is above the cap, which is the losing side for YES.- Range buckets and every other type have no strike. Their entries are blocked with
strike_unavailable.
The literal strike is used, with no bucket or settlement adjustment. On a whole-degree "greater than 85°F" market the strike is 85, so the temperature has to reach 86, a distance of +1, to be above it.
Rules cannot read the strike itself, only the distance through derived.<name>: a strike or market.strike field fails with unknown field. To test the observed value alone, use the edge field directly.
If your series mixes "above" and "or below" markets and your rules assume positive means YES is winning, filter the "or below" markets out, for example with a yes_best_ask floor as in the recipe at the end of this page and the forecast-high example.
When a rule that reads a derived field fires, its decision log entry records the observed value, the strike, the distance and whether the value was above, below or at the strike.
When a derived metric blocks entries
Every declared metric is computed on every tick, after the market is read. If any one fails, every derived field reads as unavailable and entries on that market are blocked for the tick: the strategy's resting entry orders there are cancelled, and exit rules that don't read a derived field still run:
strike_unavailable: the market has no usable one-sided strike, such as a range bucket.derived_input_unavailable: the observed value is unavailable, for example a late or missing Coinbase candle behind an EMA, or an NWS forecast outage.
With market.series_ticker and selection: all, the range markets in a series are simply never traded. With the default selection only the most liquid market is evaluated, and if that is a range market every entry is blocked, so series that mix range and threshold markets usually need selection: all. If you pin a single market with market.ticker, a deploy can be rejected up front with selected Kalshi market does not expose a supported numeric strike.
Backtesting derived metrics
Derived metrics work in Kalshi backtests, with two caveats:
- Also name the observed field in a rule condition. Today, backtests are guaranteed to load only the edge fields that rule conditions read. A field used only as
observedmay be missing, and then the metric is unavailable on every historical tick. A harmless condition such as{field: edge.btc.price, op: ">", value: 0}avoids this, as the recipe below and the forecast-high example show. - Mixed series can fail. On a series that mixes one-sided and range markets, some backtest modes block the range markets tick by tick, while others stop the whole run with
strike unavailable.
Recipe: spot and EMA vs. strike
To ask "is the trend, not just the last trade, above this market's strike?", point a second metric at a raw EMA field. Use the EMA itself (ema_26_5m), not one of its _distance fields, which measure distance from spot. Sizes and thresholds in this recipe are illustrative, not recommendations.
version: 1
platform: kalshi
strategy: custom
strategy_name: BTC spot and EMA above strike
market:
series_ticker: KXBTCD
selection: all # evaluate every strike in the current event
edge:
btc:
provider: coinbase
symbol: BTC-USD
fields: [price, ema_26_5m]
refresh: 15s # ema_26_5m is candle-based: 5s minimum
derived:
spot_vs_strike:
kind: signed_distance_from_strike
observed: edge.btc.price # Coinbase spot minus this market's strike, in USD
ema_vs_strike:
kind: signed_distance_from_strike
observed: edge.btc.ema_26_5m # 5-minute EMA-26 minus this market's strike, in USD
risk:
max_position: 2
max_portfolio_positions: 2
price_floor: 0.05
price_ceiling: 0.85
loop:
interval: 30
rules:
- name: exit_when_spot_drops_below_strike
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: derived.spot_vs_strike, op: "<", value: 0}
action: sell_all
- name: enter_yes_when_spot_and_ema_clear_strike
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: derived.spot_vs_strike, op: ">=", value: 250}
- {field: derived.ema_vs_strike, op: ">=", value: 100}
- {field: edge.btc.price, op: ">", value: 0} # these two conditions make backtests
- {field: edge.btc.ema_26_5m, op: ">", value: 0} # load both observed fields
- {field: yes_best_ask, op: ">=", value: 0.30} # keeps "or below" markets out
- {field: yes_best_ask, op: "<=", value: 0.80}
- {field: time_to_expiry, op: "<=", value: "2h"}
action: buy_yes
size: 1If the 5-minute candle behind ema_26_5m is late, derived_input_unavailable blocks entries until it arrives, and the exit, which reads the same distance, can't fire meanwhile.