Edge Data Examples and Validation Errors
This page collects complete custom strategies for Kalshi and Polymarket US that read edge data and derived metrics, followed by the validation errors Studio shows for the most common mistakes. For what each key and field means, start at Edge Data and Derived Metrics.
Example strategies
Each example compiles as written. Sizes, thresholds and market choices are illustrative only. Before you deploy anything, backtest it where the venue allows and review its limits on Risk & Limits.
BTC 15-minute markets with a Coinbase change filter
Buys YES on Kalshi's 15-minute BTC markets after a short up-move on Coinbase, and exits if the last five minutes turn down.
version: 1
platform: kalshi
strategy: custom
strategy_name: BTC 15-minute markets with a Coinbase change filter
market:
series_ticker: KXBTC15M
edge:
btc:
provider: coinbase
symbol: BTC-USD
fields: [change_15m, change_5m]
refresh: 10s # change fields are candle-based: 5s minimum
risk:
max_position: 2
price_floor: 0.10
price_ceiling: 0.90
loop:
interval: 15
rules:
- name: exit_when_move_reverses
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.btc.change_5m, op: "<", value: -0.001} # down more than 0.1% over 5 minutes
action: sell_all
- name: buy_yes_after_up_move
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: time_to_expiry, op: ">=", value: "5m"}
- {field: edge.btc.change_15m, op: ">=", value: 0.002} # up at least 0.2% over 15 minutes
- {field: edge.btc.change_5m, op: ">", value: 0} # and still rising
- {field: yes_best_ask, op: "<=", value: 0.60}
action: buy_yes
size: 1Both change fields include the one-minute candle still forming. If a candle is late, both are unavailable and neither the entry nor the exit fires on that tick (see Candle history and freshness).
EMA distance band
Buys YES on Kalshi's daily ETH markets when spot is above its one-hour VWAP and within 0 to 0.4% above the 15-minute EMA-26. Exits if spot falls 0.2% below that EMA or the contract price reaches 0.75.
version: 1
platform: kalshi
strategy: custom
strategy_name: ETH trend filter with an EMA distance band
market:
series_ticker: KXETHD
edge:
eth:
provider: coinbase
symbol: ETH-USD
fields: [price, vwap_1h, ema_26_15m_distance_pct, change_1h]
refresh: 15s
risk:
max_position: 3
price_floor: 0.10
price_ceiling: 0.80
loop:
interval: 30
rules:
- name: exit_on_trend_break
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.eth.ema_26_15m_distance_pct, op: "<", value: -0.002} # 0.2% below the EMA
action: sell_all
- name: take_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: ">=", value: 0.75}
action: sell_all
- name: enter_on_orderly_uptrend
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: edge.eth.price, op: ">", value_field: edge.eth.vwap_1h}
- {field: edge.eth.ema_26_15m_distance_pct, op: ">=", value: 0}
- {field: edge.eth.ema_26_15m_distance_pct, op: "<=", value: 0.004} # at most 0.4% above
- {field: edge.eth.change_1h, op: ">", value: 0.001} # up more than 0.1% in an hour
- {field: yes_best_ask, op: ">=", value: 0.35}
- {field: yes_best_ask, op: "<=", value: 0.65}
action: buy_yes
size: 1Without selection: all, only the most liquid market of the current event is traded. In backtests, vwap_1h and change_1h come from five-minute candles the samples can see up to 5 minutes early.
MACD line vs. signal line
Trades both sides of Kalshi's 15-minute BTC markets on one-minute MACD momentum. It buys YES when the MACD line is above its signal, velocity is positive and spot is above the 5-minute SMA-50, and buys NO on the mirror-image setup. Each side exits when the histogram turns against it.
version: 1
platform: kalshi
strategy: custom
strategy_name: BTC 15-minute MACD momentum on both sides
market:
series_ticker: KXBTC15M
edge:
btc:
provider: coinbase
symbol: BTC-USD
fields: [price, sma_50_5m, macd_1m, macd_signal_1m, macd_histogram_1m, velocity_1m]
refresh: 10s
risk:
max_position: 2
price_floor: 0.10
price_ceiling: 0.90
loop:
interval: 15
rules:
- name: exit_yes_when_momentum_turns_down
when:
all:
- {field: yes_position_size, op: ">", value: 0}
- {field: edge.btc.macd_histogram_1m, op: "<", value: 0}
action: sell_yes
- name: exit_no_when_momentum_turns_up
when:
all:
- {field: no_position_size, op: ">", value: 0}
- {field: edge.btc.macd_histogram_1m, op: ">", value: 0}
action: sell_no
- name: buy_yes_on_bullish_momentum
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: time_to_expiry, op: ">=", value: "5m"}
- {field: edge.btc.macd_1m, op: ">", value_field: edge.btc.macd_signal_1m}
- {field: edge.btc.velocity_1m, op: ">", value: 0}
- {field: edge.btc.price, op: ">", value_field: edge.btc.sma_50_5m}
- {field: yes_best_ask, op: "<=", value: 0.60}
action: buy_yes
size: 1
- name: buy_no_on_bearish_momentum
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: time_to_expiry, op: ">=", value: "5m"}
- {field: edge.btc.macd_1m, op: "<", value_field: edge.btc.macd_signal_1m}
- {field: edge.btc.velocity_1m, op: "<", value: 0}
- {field: edge.btc.price, op: "<", value_field: edge.btc.sma_50_5m}
- {field: no_best_ask, op: "<=", value: 0.60}
action: buy_no
size: 1The entries compare the MACD line with its signal through value_field, and the exits test the histogram's sign, which is the same relationship. If the latest one-minute candle is late, or any one-minute candle in roughly the last four hours is missing, all three MACD fields are unavailable, so neither the entries nor the MACD exits fire on those ticks.
NWS forecast on Polymarket US
Buys NO on an illustrative Polymarket US "NYC high 80°F or above" market while the LaGuardia forecast high and the forecast for the sixth hourly period (about five hours ahead, temp_hour_6) stay well below 80. Polymarket US has no derived metrics, so the thresholds are written as plain temperatures.
version: 1
platform: polymarket_us
strategy: custom
strategy_name: NYC high-temp NO on a cool forecast
market:
slug: tc-temp-nychigh-2026-10-02-gte80f # illustrative "NYC high 80°F or above" market; use a live slug
edge:
nyc:
provider: nws
station: KLGA # illustrative; use the station your market's rules name for settlement
fields: [forecast_high_f, temp_hour_6]
refresh: 5m
risk:
max_position: 5
price_floor: 0.05
price_ceiling: 0.90
loop:
interval: 60
rules:
- name: take_profit
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: price, op: "<=", value: 0.05} # YES has collapsed, so NO is worth about 0.95
action: sell_all
- name: stop_if_forecast_heats_up
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.nyc.forecast_high_f, op: ">=", value: 78}
action: sell_all
- name: buy_no_on_cool_forecast
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: edge.nyc.forecast_high_f, op: "<=", value: 74}
- {field: edge.nyc.temp_hour_6, op: "<", value: 75}
- {field: price, op: ">=", value: 0.10} # NO costs at most about 0.90
action: buy_no
size: 1Check your market's resolution rules for its settlement station; KLGA is used here only to show the syntax. price is the market's YES price. In the evening, forecast_high_f already describes tomorrow, so check that your market's date matches (see Daily forecast fields). If the NWS alias goes missing, Polymarket US skips ticks but keeps resting orders. This strategy cannot be backtested.
Forecast high vs. strike with an alert exit
On Kalshi's Chicago daily-high markets, buys YES when the NWS forecast high at Chicago Midway is at least 3°F above that market's strike and rain odds are under 40%. Exits on any active NWS alert or when the forecast margin shrinks below 1°F.
version: 1
platform: kalshi
strategy: custom
strategy_name: Chicago forecast high vs strike
market:
series_ticker: KXHIGHCHI
selection: all # range markets have no strike and are skipped
edge:
chi:
provider: nws
station: KMDW # Chicago Midway
fields: [forecast_high_f, precip_prob, alert_active]
refresh: 5m
derived:
forecast_vs_strike:
kind: signed_distance_from_strike
observed: edge.chi.forecast_high_f # forecast high minus this market's strike, in °F
risk:
max_position: 5
max_portfolio_positions: 1
price_floor: 0.05
price_ceiling: 0.80
loop:
interval: 60
rules:
- name: exit_on_weather_alert
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.chi.alert_active, op: "==", value: 1} # any alert for the county, advisories included
action: sell_all
- name: exit_when_forecast_margin_shrinks
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: derived.forecast_vs_strike, op: "<", value: 1}
action: sell_all
- name: enter_yes_on_forecast_margin
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: derived.forecast_vs_strike, op: ">=", value: 3}
- {field: edge.chi.forecast_high_f, op: ">", value: -100} # makes backtests load the observed field
- {field: edge.chi.precip_prob, op: "<", value: 0.40} # 0-1 fraction: 0.40 = 40%
- {field: yes_best_ask, op: ">=", value: 0.30} # keeps "or below" markets out
- {field: yes_best_ask, op: "<=", value: 0.70}
action: buy_yes
size: 1The yes_best_ask floor matters here: on the series' "or below" market a positive distance means YES is losing. If the NWS forecast lookup fails, forecast_high_f is unavailable, so the derived metric blocks entries and the exits that read it can't fire until it returns (see When a derived metric blocks entries). If the alert lookup fails, alert_active reads 0. KMDW can be backtested, but forecast history covers only about the last 92 days.
Pausing quotes around FOMC and CPI
Rests a paired YES and NO quote one cent above each best bid while the combined bids leave room under $0.98, and pulls the strategy's resting orders on that market from about 35 minutes before until 30 minutes after any FOMC or CPI announcement.
version: 1
platform: kalshi
strategy: custom
strategy_name: Paired quotes with an FOMC/CPI blackout
market:
series_ticker: KXBTCD
edge:
macro:
provider: economic_calendar
event: FOMC,CPI # no spaces; quote it inside a one-line {...} map
fields: [seconds_to_next, seconds_since_previous]
refresh: 5m # the minimum; the countdown is recalculated only on refresh
risk:
max_position: 2
price_floor: 0.05
price_ceiling: 0.95
loop:
interval: 20
rules:
- name: announcement_blackout
when:
any:
- {field: edge.macro.seconds_to_next, op: "<=", value: 2100} # 30 minutes + one refresh
- {field: edge.macro.seconds_since_previous, op: "<=", value: 1800}
action: cancel_all
- name: pull_when_outbid
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: order_count, op: ">", value: 0}
- {field: paired_best_bid_sum, op: ">", value: 0.985}
action: cancel_all
- name: quote_pair
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: order_count, op: "==", value: 0} # let a resting pair keep its place
- {field: paired_best_bid_sum, op: "<", value: 0.965}
orders:
- side: "yes"
action: buy
size: 1
price: {reference: best_bid, offset: 0.01}
post_only: true
- side: "no"
action: buy
size: 1
price: {reference: best_bid, offset: 0.01}
post_only: true
max_combined_price: 0.98order_count == 0 lets the pair rest instead of being cancelled and re-sent every loop. Your own quotes are part of the book, so the pull rule watches paired_best_bid_sum rather than spread. If only one leg fills, position_size > 0 stops re-quoting and the other leg keeps resting until it fills or the blackout pulls it. The strategy has no sell rule, so a filled contract is held until the market settles.
The blackout sits first so it wins over the quoting rules. With a 5-minute refresh it starts roughly 35 to 30 minutes before the release, depending on when the last fetch happened (see Blackout recipe). During a calendar outage it can start as late as about 20 minutes before, and once the alias goes missing entries are blocked and the resting quotes cancelled. For multi-order rules like quote_pair and the max_combined_price cap, see Composite Orders and max_combined_price in Advanced Kalshi Rules.
Common validation errors
Each row pairs text from an error message with its usual cause. Declaring and Reading Edge Data and Derived Metrics explain each rule in context.
| Message contains | Cause |
|---|---|
at most 8 edge aliases allowed | More than 8 aliases. |
alias name ... must match | Uppercase letters, hyphens or a leading digit in an alias name. |
unknown provider | Provider misspelled or not lowercase. |
requires selector | Coinbase alias without symbol, or NWS alias without station. |
requires event directly on the edge alias | Calendar alias without event, or event nested under selector:. |
expects selector | The wrong selector key for the provider. |
exactly one selector allowed | More than one of symbol, station and event. |
does not match required format | Lowercase or malformed selector, a space in FOMC, CPI, or an unsupported event. |
fields must be non-empty | Missing or empty fields list. |
has no field | A field name outside the provider's catalog. |
invalid refresh duration | A refresh with no unit or with days. |
below provider minimum | A refresh under the provider's minimum. |
edge field must be edge.<alias>.<field> | A reference without exactly three parts. |
not declared | A rule reads an alias or derived metric that isn't declared. |
not listed in alias | A rule or observed reads a field missing from the alias's fields. |
exactly one of value or value_field must be set | A condition with both value and value_field, or with neither. |
requires a numeric value (got string) | A quoted number, or a duration like "30m", on a numeric field. |
is a text field; use == or != | <, >, <= or >= on a text field. |
is a text field; value must be one of | A number compared with a text field. |
is not one of | A text value the field never takes, often a capitalization difference such as Rain. |
must be finite | .inf or .nan as a value. |
expects a fraction in [0,1] | A percent written as a whole number on an NWS fraction field. |
expects a flag in [0,1] | An alert_active value outside 0 to 1. |
value_field cannot be used with text edge fields | value_field with a text field on either side. |
regular Polymarket strategies do not support edge aliases | An edge: block on platform: polymarket. |
unknown top-level field | An edge: or derived: block on Kalshi Perps or arbitrage. |
derived metrics are supported only for custom Kalshi strategies | derived: on Polymarket, Polymarket US or a built-in strategy. |
at most 8 derived metrics allowed | More than 8 derived metrics. |
derived metric name ... must match | Uppercase letters, hyphens or a leading digit in a derived metric name. |
.observed: required | A derived metric without observed. |
unsupported derived metric kind | A kind other than signed_distance_from_strike. |
unsupported derived metric key | A key other than kind or observed in a metric. |
must reference a numeric edge field | observed points at a text field. |
unknown field | A rule field that does not exist, such as market.strike (rules cannot read the strike). |
selected Kalshi market does not expose a supported numeric strike | At deploy: a pinned market.ticker with no one-sided strike. |