Declaring and Reading Edge Data
This page is the key-by-key reference for the edge: block in custom strategies on Kalshi and Polymarket US: how to declare an alias, and how rule conditions read its values. For how edge data fits into a strategy, where it works and what happens when data is late, see Edge Data and Derived Metrics.
The YAML examples on this page are rejected on purpose, to show the error each mistake produces.
Declaring an edge alias
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
edge | map of aliases | none | 0 to 8 aliases | Declares the outside data sources your rules can read. |
| alias name | map key | none | lowercase letter, then up to 31 lowercase letters, digits or _ | Your label for the source; rules read edge.<alias>.<field>. More |
provider | string | required | coinbase, nws, economic_calendar | Which data source the alias reads. More |
symbol | string | required for coinbase | Coinbase product id such as BTC-USD | The crypto product to read. |
station | string | required for nws | 4 uppercase letters such as KMDW | The NWS observation station to read. More |
event | string | required for economic_calendar | FOMC, CPI, FOMC,CPI | The announcements to track. More |
fields | list of strings | required | names from the provider's catalog | The values rules may read. More |
refresh | duration string | 5s Coinbase, 5m NWS, 15m calendar | a duration at or above the provider minimum | How long a fetched value is reused; also sets how long stale data is tolerated. More |
Alias names
The alias is your own label. It does not pick the data: btc does not imply BTC-USD, so you always declare the symbol too.
- Names must match
^[a-z][a-z0-9_]{0,31}$: start with a lowercase letter, then lowercase letters, digits or underscores, 32 characters at most.btcandnyc_tempwork;BTCandbad-aliasare rejected. - You can declare up to 8 aliases. An empty
edge: {}is accepted. - Declare only aliases your rules use. Live, the Bot reads every declared alias on every tick (fetching it when its
refreshis due), and a failing alias blocks entries even if no rule reads it.
Providers and selectors
Each provider has exactly one selector key. Set that key directly on the alias, and no other selector.
| Provider | Selector key | Selector format | Example | Fields | Minimum refresh | Default refresh |
|---|---|---|---|---|---|---|
coinbase | symbol | Coinbase product id: uppercase letters or digits, one hyphen | BTC-USD, ETH-USD | 39 | 1s, or 5s with any candle-based field | 5s |
nws | station | 4 uppercase letters (an ICAO station id) | KMDW, KLGA | 57 | 60s | 5m |
economic_calendar | event | FOMC, CPI or both, comma-separated, no spaces | FOMC,CPI | 4 | 5m | 15m |
- Provider names are lowercase and exact.
NWSorbinancefails withunknown provider. - A missing selector fails (
provider "coinbase" requires selector "symbol"), and so does the wrong one (provider "nws" expects selector "station", not "symbol") or more than one (exactly one selector allowed). - Only the format is checked. A product or station that does not exist, such as
ZZZZ, passes validation, then every live fetch fails and every entry is blocked. Double-check the id before you deploy. - Selectors are case-sensitive:
btc-usd,kmdwandfomcare rejected withdoes not match required format.
Fields
fields lists the provider values this alias exposes. It must be a non-empty list of names from the provider's catalog: Coinbase, NWS or economic calendar.
- A name outside the catalog fails, for example
provider "nws" has no field "temp_hour_13". - Rules and derived metrics can read only fields listed here. Reading an unlisted one fails with
edge field "..." not listed in alias "...". - The list does not change what is downloaded. Each refresh fetches the provider's full data set.
- For Coinbase, listing any candle-based field raises the minimum refresh from
1sto5s. - Listing a field does not make it backtestable. See Backtest support.
Refresh
refresh is how long the Bot reuses a fetched value before fetching the alias again. It also sets the stale limit: if a fetch fails, the last good values are reused until they are 3 × refresh old. After that the alias is missing and entries are blocked. See The refresh cache.
Write it as a duration with a unit: 10s, 5m, 1m30s or 2.5s. Valid units are ns, us, ms, s, m and h. A bare number (5) fails with missing unit, and days (1d) fail with unknown unit "d". There is no maximum.
| Provider | When | Minimum | Default |
|---|---|---|---|
coinbase | every listed field is one of price, bid, ask, spread, spread_bps, change_24h, volume_24h | 1s | 5s |
coinbase | any other field is listed (all of them are built from candles) | 5s | 5s |
nws | always | 60s | 5m |
economic_calendar | always | 5m | 15m |
The cache is only checked when your loop runs, so a refresh shorter than loop.interval means "fetch on every loop" and gains nothing more. Each Coinbase refresh makes six requests (stats, book and four candle sizes) whatever fields you list.
# Rejected: a candle-based field (change_5m) needs a refresh of at least 5s
# error: refresh 2s below provider minimum 5s
version: 1
platform: kalshi
strategy: custom
strategy_name: Refresh too fast for candle fields
market:
series_ticker: KXBTC15M
edge:
btc:
provider: coinbase
symbol: BTC-USD
fields: [price, change_5m]
refresh: 2s
risk:
max_position: 1
price_floor: 0.10
price_ceiling: 0.90
loop:
interval: 15
rules:
- name: exit_on_down_move
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.btc.change_5m, op: "<", value: 0}
action: sell_all
- name: enter_on_up_move
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: edge.btc.change_5m, op: ">", value: 0.001}
action: buy_yes
size: 1Keys Studio does not check
Inside an alias, keys other than provider, symbol, station, event, fields and refresh are ignored without an error. A typo such as refesh: 30s silently falls back to the default refresh, and a selector: wrapper is ignored (the alias then fails for a missing selector). Check spelling carefully.
If you write an alias on one line with braces, quote a two-event list. In {provider: economic_calendar, event: FOMC,CPI, fields: [seconds_to_next]} the unquoted comma splits the value, so only FOMC is kept and CPI is dropped with no error. Write event: "FOMC,CPI" or use block style, as in the blackout recipe.
Reading edge values in rules
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
field (edge) | field reference | none | edge.<alias>.<field> with a declared alias and a listed field | Reads an edge value in a condition. |
field (derived) | field reference | none | derived.<name> with a declared metric (Kalshi only) | Reads a derived metric in a condition. More |
value | number, or text for a text field | none | a finite, unquoted number; 0 to 1 for NWS fraction fields; one of the field's values for a text field | The literal to compare with. More |
value_field | field reference | none | any rule field, including edge and derived fields | Compares with another field instead of a literal. More |
Each condition needs exactly one of value or value_field. Edge references must have exactly three parts: edge.btc or edge.btc.price.x fails with edge field must be edge.<alias>.<field>, and an undeclared alias fails with edge alias "..." not declared.
Numbers and units
Every edge value except the four text fields is a plain number, and value must be one too.
- Fractions, not percents. Coinbase
change_*and*_distance_pctfields are fractions: write0.004for 0.4%. NWShumidityand everyprecip_prob*field run from 0 to 1: write0.70for 70%. - Checked ranges. A literal for NWS
humidity,precip_prob,precip_prob_day_2,precip_prob_day_3,precip_prob_hour_1toprecip_prob_hour_12oralert_activemust be between 0 and 1, or the save fails withexpects a fraction in [0,1](orexpects a flag in [0,1]foralert_active). - No quotes.
value: "100000"is rejected withrequires a numeric value (got string) — remove the surrounding quotes. Calendar fields are plain seconds, sovalue: "30m"is rejected too; write1800. - Finite only.
.infand.nanfail withmust be finite. - Currency. Coinbase prices are in the product's quote currency: USD for
-USDpairs, USDT for-USDT, BTC forETH-BTC. Field names ending in_usdalso mean the quote currency.
# Rejected: precipitation probability is a 0-1 fraction, so 40 is out of range (write 0.40)
# error: expects a fraction in [0,1] (use 0.80 for 80%)
version: 1
platform: kalshi
strategy: custom
strategy_name: Rain odds written as a percent
market:
series_ticker: KXHIGHCHI
edge:
chi:
provider: nws
station: KMDW
fields: [forecast_high_f, precip_prob]
risk:
max_position: 1
price_floor: 0.05
price_ceiling: 0.80
loop:
interval: 60
rules:
- name: exit_on_rain_risk
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.chi.precip_prob, op: ">", value: 0.60}
action: sell_all
- name: enter_on_dry_forecast
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: edge.chi.forecast_high_f, op: ">=", value: 80}
- {field: edge.chi.precip_prob, op: "<", value: 40}
action: buy_yes
size: 1Comparing two fields
value_field compares a field with another field on the same tick, for example edge.btc.macd_1m against edge.btc.macd_signal_1m, or edge.eth.change_24h against edge.btc.change_24h from a second alias.
- Studio does not check that both sides use the same units or timeframe. Comparing
edge.btc.price(dollars) with the contractprice(0 to 1), ormacd_1mwithmacd_signal_5m, saves fine but means little. - The 0-to-1 range check does not apply to
value_fieldcomparisons. - There is no crossover operator.
macd_1m > macd_signal_1mtests the current relationship on every tick, not a one-time cross.
Text fields
Four fields hold text rather than numbers: NWS precip_type and alert_severity, and calendar next_event_type and previous_event_type. A rule can test one for equality with a value it can take:
| Field | Values |
|---|---|
precip_type | none, rain, snow, mixed |
alert_severity | none, advisory, watch, warning |
next_event_type, previous_event_type | FOMC, CPI |
- Use
==or!=. Text can't be ordered, so<,>,<=and>=fail withis a text field; use == or !=. - Write one of the listed values. Quotes are optional:
value: warningandvalue: "warning"are the same. Values are case-sensitive, soWarningorfomcfails withis not one of, and a number fails withis a text field; value must be one of. - No
value_field. A text field can't be compared with another field, on either side.
Text conditions work live and in backtests.
version: 1
platform: kalshi
strategy: custom
strategy_name: Exit on a weather warning
market:
series_ticker: KXHIGHCHI
edge:
chi:
provider: nws
station: KMDW
fields: [forecast_high_f, alert_severity]
risk:
max_position: 1
price_floor: 0.05
price_ceiling: 0.80
loop:
interval: 60
rules:
- name: exit_on_warning
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.chi.alert_severity, op: "==", value: warning}
action: sell_all
- name: enter_on_warm_forecast
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: edge.chi.forecast_high_f, op: ">=", value: 80}
- {field: edge.chi.alert_severity, op: "!=", value: warning}
action: buy_yes
size: 1Text has no order, so "at least a watch" has to list the values:
# Rejected: text fields can't be compared with >=; list the values with == in an any rule instead
# error: edge.chi.alert_severity is a text field; use == or !=
version: 1
platform: kalshi
strategy: custom
strategy_name: Ordering a text field
market:
series_ticker: KXHIGHCHI
edge:
chi:
provider: nws
station: KMDW
fields: [forecast_high_f, alert_severity]
risk:
max_position: 1
price_floor: 0.05
price_ceiling: 0.80
loop:
interval: 60
rules:
- name: exit_on_watch_or_worse
when:
all:
- {field: position_size, op: ">", value: 0}
- {field: edge.chi.alert_severity, op: ">=", value: watch}
action: sell_all
- name: enter_on_warm_forecast
when:
all:
- {field: position_size, op: "==", value: 0}
- {field: edge.chi.forecast_high_f, op: ">=", value: 80}
action: buy_yes
size: 1Unavailable values
A healthy alias can still hold individual values that are unavailable for a moment: a Coinbase candle is late, an NWS forecast lookup fails, a station does not report humidity. A condition that reads an unavailable value is false for every operator, including !=, live and in backtests. That holds on either side of a value_field comparison.
The condition fails, so an all rule does not match, while an any rule can still match through its other conditions. Later rules still run.