Arbitrage Strategy Reference
This section covers every parameter for platform: arbitrage. An arbitrage strategy trades two legs on two venues at once. Leg A is always Kalshi. Leg B is either regular (international) Polymarket or Polymarket US. The two legs name markets that should settle on the same real-world outcome.
You don't have to write the YAML yourself. You describe the pair in an Arbitrage chat in Studio, and Studio's AI writes the document. This section explains what each key does, so you can check the document before you deploy.
Note: Turbine is not a financial advisor. Markets, tickers, prices and thresholds in this section are illustrative placeholders that show how parameters behave. They are not recommendations, and nothing here suggests a strategy will be profitable.
In this section
The arbitrage reference is split into these pages:
- Arbitrage Venues and Selectors: How each leg picks its market: the selectors Kalshi, Polymarket and Polymarket US accept, inline or nested selectors, how rolling legs are paired, and search placeholders that save as a draft but can't deploy.
- Arbitrage Outcome Mapping and Strategies: How
mapping.a_yes_equalslines up the two legs' outcomes, and how thecomplementary,directionalandcustomstrategies trade, including custom rule fields and actions. - Arbitrage Risk Settings and Fees: Every
riskkey (per-lock budget, net edge, slippage, unhedged time, resolution-source mismatch), how a lock is placed, safety halts, the deploy dialog's limits,loop.interval, and the built-in fee estimates andfeesoverrides. - Arbitrage Buckets: How a Kalshi series and a rolling daily Polymarket US leg are matched bucket by bucket, such as daily temperature bands, and how
bucket.selectandbucket.boundschoose the pair to trade. - Arbitrage Examples and Validation Errors: Seven complete strategies that pass Studio's checks, from fixed and rolling locks to directional buys and custom rules, plus common validation errors and what to change for each.
This page keeps what every arbitrage strategy shares: how arbitrage works, the supported venue pairs, the full parameter table, backtesting and paper trading support, and the caveats to read before you deploy.
How arbitrage works
The built-in strategy looks for a lock. It buys one outcome on Kalshi and the opposite outcome on leg B. If the two markets settle the same way, exactly one of the two contracts pays $1 and the other pays nothing. When the two asks plus fees come to less than $1, the difference is the lock's edge.
For example, Kalshi YES at $0.42 plus Polymarket US NO at $0.53 costs $0.95 per pair. One of the two pays $1, so the pair has $0.05 of edge before fees.
Every loop.interval seconds, the Bot:
- reads both order books,
- prices both ways to lock (Kalshi YES with one leg-B side, and Kalshi NO with the other), including estimated fees,
- keeps the better one, and trades it only if its net edge is at least
risk.min_net_edge_bps, - places one leg, then the other straight away (see How a lock is placed),
- holds both legs until the markets settle.
There's no take-profit, stop-loss or early exit. The Bot can lock again on a later loop while the edge, your balances and your deployment risk limits last.
Warning: A lock is not risk-free. The two venues can settle on different sources or rules, the second leg can fail after the first one fills, and prices can move between the two orders. Read Caveats before you deploy.
Supported venue pairs
There are two venue pairs. You pick one when you start an Arbitrage chat, and the chat stays on that pair. To trade the other pair, start a new chat. Kalshi + Polymarket appears only where regular Polymarket is available to you.
| Kalshi + Polymarket | Kalshi + Polymarket US | |
|---|---|---|
venues.b.platform | polymarket | polymarket_us |
| Fixed leg-B market | condition_id + yes_token_id + no_token_id | slug |
| Rolling leg-B markets | series_slug, or recurring crypto up/down series | recurring daily events, paired by bucket |
| Leg placed first | Polymarket, always | The leg with more visible depth; Kalshi on a tie or when depth is unknown |
| Leg-B order | Limit order; whatever doesn't fill at once is cancelled | Fill-or-kill |
| Leg-B fee estimate | The market's live fee rate | Polymarket US's published taker formula |
| Leg-B minimum order | The market's minimum, usually 5 shares | 1 contract |
| Runs on | Your Polymarket runner | Your Kalshi runner |
Leg A is always Kalshi, and the legs can't be swapped. There is no Polymarket + Polymarket US pair, no Kalshi + Kalshi pair and no Kalshi Perps arbitrage. Arbitrage needs an active Studio subscription.
The platform value
Every arbitrage document uses platform: arbitrage (letter case doesn't matter). The venue pair comes from venues.b.platform, not from platform. Deploy-time names such as arbitrage_kalshi_polymarket and arbitrage_kalshi_pmus are not valid here:
# Rejected: platform must be arbitrage; the venue pair comes from venues.b.platform
# error: arbitrage DSL must set platform: arbitrage
version: 1
platform: arbitrage_kalshi_pmus
strategy: complementary
venues:
a:
platform: kalshi
market:
ticker: KXFEDDECISION-26OCT-C25
b:
platform: polymarket_us
market:
slug: fed-cuts-rates-25bps-october-2026
mapping:
a_yes_equals: b_yes
risk:
max_position_usdc: 25
loop:
interval: 30All arbitrage parameters
Every key an arbitrage document accepts is listed below. Rows that link to a section have behavior worth reading about.
Studio rejects an unknown top-level key. Most unknown keys inside a block, such as risk.max_notional or fees.c, are ignored without an error, so check spelling. Keys inside rules are the exception: unknown rule and condition keys are rejected. Whole-number keys must be whole: min_net_edge_bps: 12.5 fails with risk.min_net_edge_bps: must be a whole number, got 12.5.
Document keys
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
version | integer | required | 1 | Document version. |
platform | string | required | arbitrage | Marks a two-leg arbitrage document. |
strategy | string | required | complementary, directional, custom | Hedged lock, single-leg buy, or your own rules. Case doesn't matter. |
strategy_name | string | none | any text; Studio trims it to 80 characters | Display name. Studio sets it when it saves, so a hand edit can be replaced. No effect on trading. |
strategy_name_origin | string | generated | user, generated, curated. Older legacy and unknown values are saved as generated | Where the name came from. Metadata only. |
Venues and markets
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
venues.a.platform | string | required | kalshi | Leg A is always Kalshi. |
venues.b.platform | string | required | polymarket, polymarket_us | Picks the venue pair. |
venues.a.market, venues.b.market | map | required | the selector keys below, under market or directly on the leg | The market each leg trades. Each leg needs at least one selector. |
venues.a.market.ticker | string | none | a Kalshi market ticker | One fixed Kalshi market. |
venues.a.market.series_ticker | string | none | a Kalshi series ticker | Follows the series' current market. Required with a rolling daily Polymarket US leg. |
venues.a.market.event_ticker | string | none | a Kalshi event ticker | Search placeholder. Saves as a draft that can't deploy. |
venues.a.market.query, venues.b.market.query | string | none | free text | Search placeholder on either leg. Saves as a draft that can't deploy. |
venues.b.market.slug | string | none | a market slug | Polymarket US: the fixed market. Polymarket: a label only. |
venues.b.market.event_slug | string | none | an event slug | Search placeholder. Saves as a draft that can't deploy. |
venues.b.market.series_slug | string | none | a Polymarket series slug such as btc-up-or-down-15m | Polymarket only: follows the series' current market. |
venues.b.market.condition_id | string | none | a Polymarket condition ID | Polymarket only: the fixed market. Needs both token IDs. |
venues.b.market.yes_token_id | string | none | a token ID | Polymarket only: the token the Bot buys as leg-B YES. |
venues.b.market.no_token_id | string | none | a token ID | Polymarket only: the token the Bot buys as leg-B NO. |
venues.b.market.neg_risk | boolean | false | false | Polymarket only. true is rejected: multi-outcome markets aren't supported. |
venues.b.market.tick_size | string | "0.01" | a price step such as "0.01"; not checked when you save | Polymarket only: fallback price step if the live one can't be read. |
venues.b.market.min_order_size | number (quoted or unquoted) | 5 | a positive number such as 5 or "5" | Polymarket only: smallest order on a fixed market. |
venues.b.market.recurring | map | none | Polymarket: kind, asset, interval. Polymarket US: kind, event_slug_prefix | Rolling leg-B markets. |
venues.b.market.recurring.kind | string | updown on Polymarket; required on Polymarket US | Polymarket: updown. Polymarket US: daily | The kind of rolling market. Case doesn't matter. |
venues.b.market.recurring.asset | string | none | btc, eth, sol, xrp, doge, hype, bnb | Polymarket only: the crypto asset. |
venues.b.market.recurring.interval | string | none | 5m, 15m, 1h, 4h, 1d | Polymarket only: the window length. |
venues.b.market.recurring.event_slug_prefix | string | none | an event slug without its date, such as temp-nychigh | Polymarket US only: the daily event family. Ignored on Polymarket. |
venues.a.market.selection, venues.b.market.selection | string | none | not allowed | Rejected. Use bucket.select. |
Mapping, risk and loop
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
mapping.a_yes_equals | string | required | b_yes, b_no | Which leg-B outcome is the same event as Kalshi YES. Decides which pairs the Bot buys. |
mapping.leg_a_yes_to.leg_b_side | string | none | yes, no, b_yes, b_no | Older spelling of a_yes_equals. Used only when a_yes_equals is missing. |
mapping.leg_a_no_to | map | none | anything | Older key. Accepted and ignored. |
risk.max_position_usdc | number ($) | required | above 0 | Most dollars one lock may spend across both legs; not a lifetime cap. For directional, the cap on the whole position. |
risk.min_net_edge_bps | integer (bps of $1) | 0 | 0 or more | Minimum edge after estimated fees before the Bot trades. |
risk.max_leg_slippage_bps | integer (bps of price) | 25 | 0 or more | How far above the quoted ask each order's limit may sit. |
risk.max_unhedged_seconds | integer (seconds) | 10 | 0 or more; don't use 0 | Longest the second leg may take before the Bot halts itself. |
risk.accept_resolution_source_mismatch | boolean | false | true, false | Records that you accept the two markets may settle differently. Doesn't change trading. |
loop.interval | integer (seconds) | required | 10 or more | Seconds to wait after each loop. |
Fees and buckets
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
fees | map | none | a, b | Optional overrides for the fee estimates used in edge checks. |
fees.a.flat_bps | integer (bps of notional) | none | 0 or more | Kalshi estimate: a flat share of notional. |
fees.a.fee_rate_bps | integer | none | not allowed | Rejected: it's a Polymarket fee input. Use fees.a.flat_bps. |
fees.b.flat_bps | integer (bps of notional) | none | 0 or more; not with fee_rate_bps | Leg-B estimate: a flat share of notional. |
fees.b.fee_rate_bps | integer (bps) | none | 0 or more; not with flat_bps | Leg-B estimate: a Polymarket-style rate × min(p, 1 − p). |
bucket | map | none | select, bounds | Only with a rolling daily Polymarket US leg: which matched bucket to trade. |
bucket.select | string | all | all, nearest_even_odds, max_volume, pinned_bounds | How the bucket is chosen. |
bucket.bounds.gte | number | none | a whole number | Lowest value in the pinned bucket, inclusive. Only with pinned_bounds. |
bucket.bounds.lte | number | none | a whole number | Highest value in the pinned bucket, inclusive. Only with pinned_bounds. |
Directional params
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
params | map | none | leg, side, max_price, size | Read only by strategy: directional. The other strategies ignore it. |
params.leg | string | b | a (Kalshi), b (leg B) | Which venue to buy on. |
params.side | string | yes | yes, no | Which outcome to buy. |
params.max_price | number ($) | 0 (never buys) | a price above 0 (0 or missing never buys; not checked when you save) | Highest ask the Bot will pay. |
params.size | integer (contracts) | 1 | 1 or more | Contracts per buy. Raised to the leg's minimum order size, then cut to what fits under the position cap and the deploy limits. |
Custom rules
| Parameter | Type | Default | Allowed values | What it does |
|---|---|---|---|---|
rules | list | required for custom | 1 or more rules | Ordered rules. The first match each loop acts. |
rules[].name | string | required | non-empty text | Label in logs. |
rules[].when.all or rules[].when.any | list of conditions | required (exactly one) | all: every condition true. any: at least one | The rule's trigger. |
rules[].when.*[].field | string | required | the 11 arbitrage fields | The value to test. |
rules[].when.*[].op | string | required | <, >, <=, >=, ==, != | Comparison. Quote it: op: ">=". |
rules[].when.*[].value | number | one of value or value_field | an unquoted, finite number (true/false for halted) | What to compare against. |
rules[].when.*[].value_field | string | one of value or value_field | any arbitrage field | Compare against another live value. |
rules[].action | string | required | execute_lock, cancel_all, skip | What happens when the rule matches. |
rules[].size and other single-venue rule keys | varies | none | accepted when well-typed | Ignored by arbitrage. size doesn't size a lock. |
The deploy dialog's Max contracts per order, Max dollars per order, Max open contracts and Max daily notional traded also limit every buy, including each lock. They aren't YAML keys; see Deployment risk limits.
Backtesting and paper trading
Arbitrage strategies can't be backtested, and there is no paper mode. An arbitrage Bot's first run is live, with real orders on both venues. Start with small deploy limits and watch the first locks closely.
Caveats
- Settlement differences (basis risk). A lock only pays out as planned if both markets settle the same way. Venues can use different data sources, cut-off times and rules for edge cases, and then both legs can lose.
accept_resolution_source_mismatchrecords that you accepted this; it doesn't reduce it. - Legging risk. Between the first fill and the hedge you hold one side alone. If the hedge fails or is late, the Bot halts and leaves that position open.
- Prices move. The edge is measured at quoted asks. Slippage and a moving market during the hedge can turn a planned lock into a loss.
- Fees are estimates. They follow each venue's published or live rate, but venues can change fees, and overrides replace the estimate only.
- Locks add up to your deploy limits.
max_position_usdccaps each lock, not your total. Repeated locks stop only at Max open contracts (per leg) and Max daily notional traded, so set those to the most you want held and traded. - Polymarket collateral. On Kalshi + Polymarket, a Polymarket order the Bot refuses just before sending, for example because your collateral doesn't cover it at its slippage limit, halts the Bot even though no order was placed. Keep your Polymarket collateral above what one lock can spend at its slippage limit.
- Leg-B cancels take only this Bot's orders. Polymarket and Polymarket US orders carry no client id, so the Bot remembers the id of every leg-B order the exchange accepts from it; the memory survives restarts and redeploys, but not stopping the Bot. The
cancel_allaction, the cleanup before a partial-fill or hedge halt, and the cancel that follows each regular Polymarket order touch only those ids, never orders you placed by hand or another Bot's. Studio still won't deploy a second arbitrage Bot on a leg-B market that a running arbitrage Bot already trades, because both would size against the one position your account holds there. - Markets end. A fixed Kalshi ticker stops the Bot once it closes. A closed fixed leg-B market makes every loop skip.
- Fail-closed data. Stale quotes, a one-sided book, a missing Polymarket fee rate or an unreadable position make the Bot skip the loop rather than guess.
Read Risk & Limits before you deploy.