Polymarket US Strategies and Edge Data
This page covers the two ways to write a platform: polymarket_us strategy (a built-in strategy tuned through params, or strategy: custom with your own rules) and the edge data (Coinbase, NWS weather and the economic calendar) that custom rules can read. Limits such as Max contracts per order and Max open contracts are the deployment risk limits you set when you deploy, and how orders are priced is covered in Order pricing and execution. For every parameter at a glance, see All Polymarket US parameters.
Built-in strategies on Polymarket US
The five built-in strategies work on Polymarket US, with fixed logic you tune through params. Each one has its own page under Built-in Strategies that explains it in general. This section covers what's specific to Polymarket US.
A built-in strategy can't also have rules: Studio rejects it with "Polymarket US built-in strategies do not support top-level rules; use strategy: custom to define rules". An edge block on a built-in strategy is ignored.
| Strategy | Required params | Optional params (default) | Order size on Polymarket US |
|---|---|---|---|
spread_capture | spread_floor, order_count | post_only (false) | 1 contract per quote |
mean_reversion | entry_low, entry_high | exit_target (0.5), cooldown (60) | min(max_position, 10) |
panic_fade | panic_threshold, fade_size | recovery_exit (0.04), max_fades (3) | fade_size |
observation_momentum | lookback_periods, momentum_threshold | position_scale (linear) | grows 1, 2, 3… capped at max_position |
pre_announcement_drift | entry_hours_before, blackout_minutes_before | drift_direction (auto), exit_on_release (false) | min(max_position, 5) |
spread_capture
Each loop, if ask − bid is less than spread_floor, the Bot cancels its own open orders in the market and stops there. Otherwise it cancels them and places order_count levels of 1-contract quotes: a YES bid at mid − (spread_floor ÷ 2) × level (never below the floor) and a YES offer at mid + (spread_floor ÷ 2) × level (never above the ceiling).
For example, with a bid of 0.45, an offer of 0.55 and spread_floor: 0.04, level 1 quotes 0.48 / 0.52, level 2 quotes 0.46 / 0.54, and level 3 quotes 0.44 / 0.56.
- Every resting quote counts toward Max open contracts, bids and offers alike, and Studio pre-fills that limit from
max_position. The bid at level 2 already counts level 1's two quotes, so it needs a limit of at least 3 plus what you hold; the bid at level 3 needs at least 5 plus what you hold. A refused bid ends that loop, so the quotes after it aren't placed. Useorder_count: 1, or raise Max open contracts to about twiceorder_countwhen you deploy. - Once you hold
max_positionor more, the Bot stops quoting, but it doesn't cancel quotes that are already resting. - The offer is a YES sell placed even when you hold no YES. Check your positions closely after the first fills. In paper mode, an offer that fills while you hold no YES opens a NO position.
- It cancels only its own orders each loop: the ones the exchange accepted from this Bot (see Exits). Orders you placed by hand and other Bots' orders stay resting, but they still count toward Max open contracts.
post_only: truemakes quotes maker-only, so a quote that would trade immediately isn't placed as a taker order.
mean_reversion
When you hold nothing, the cooldown has passed and no order is resting, the Bot buys YES if price_floor < price ≤ entry_low, or buys NO if entry_high ≤ price < price_ceiling. Each entry is min(max_position, 10) contracts at the last trade price. An entry that hasn't filled after two loops (at least 60 seconds) is cancelled.
While holding, it market-sells your whole position when the YES price is strictly within 2¢ of exit_target. If the price jumps across that band between loops, no exit happens. cooldown starts when an entry is placed. An attempt skipped because an order was resting or buying power was short doesn't restart it.
panic_fade
The Bot keeps each loop's price for 5 minutes (fixed). When YES trades at least panic_threshold below the highest of those prices, fewer than max_fades fades have filled in this run, no fade is open and no order is resting, it buys fade_size YES at the last trade price. The fade opens once that buy fills, even partly. A buy that hasn't filled after two loops is cancelled and doesn't count.
It exits with a market sell of the YES the fade bought when YES rises recovery_exit above the fade price, or drops below price_floor. The next fade can start once the exit has sold.
- The rise is compared in floating point, so an exactly-equal move can miss. With a fade at 0.39 and
recovery_exit: 0.05, 0.44 may not exit while 0.45 does. - The exit cancels the unfilled rest of a partly filled buy, then sells what filled. YES you held before the fade isn't sold.
- A restart or redeploy resets the fade count and forgets any open fade, so the Bot won't exit a fade it bought before the restart. Close it yourself.
observation_momentum
Each loop adds one price sample. When the newest sample differs from the oldest (up to lookback_periods samples back) by at least momentum_threshold, the Bot buys in that direction: YES on a rise, NO on a fall. Order sizes grow on consecutive qualifying loops: 1, 2, 3… (or 1, 2, 4… with position_scale: exponential), each capped at max_position.
- Position cap. Each order is cut so your total position never passes
max_position. Atmax_positionthe Bot stops buying. - Exit. When a qualifying move goes against the side you hold, the Bot market-sells that whole position, split to fit Max contracts per order, instead of buying the other side, and the streak starts over.
- Resting orders. No new order goes out while one rests. An unfilled buy is cancelled after two loops.
Studio pre-fills Max contracts per order at max_position, the largest order a streak can reach. Lower it and the larger orders of a streak are refused.
pre_announcement_drift
The Bot enters once when the market's close is within entry_hours_before hours but more than blackout_minutes_before minutes away, buying min(max_position, 5) contracts at the last trade price. With exit_on_release: true, it market-sells what the entry bought, on the side it bought, when the blackout begins. Despite the name, that is before the release, not after it.
- A
bearishentry buys NO, and the default (auto) entry buys NO whenever YES is at 0.50 or above. The release exit then sells that NO. - The release exit is split to fit Max contracts per order, which Studio pre-fills at the entry size.
- The market must report a close time, or the Bot never enters.
- An entry that hasn't filled after two loops is cancelled and placed again while the window is open. An entry still resting when the blackout begins is cancelled.
- A restart or redeploy forgets the entry. The Bot won't send the release exit for a position it opened before, and it enters again if the entry window is still open.
- Studio doesn't check that
blackout_minutes_beforeis shorter thanentry_hours_before. If the blackout is as long as the window or longer, the Bot never enters.
Custom rules on Polymarket US
A custom strategy is an ordered list of rules. Every loop the Bot reads the market and your account once, then checks the rules top to bottom. The first rule whose when is true performs its action, and nothing else runs that loop. Rules have no memory between loops, so "have I already entered?" has to come from state such as position_size == 0.
The Custom Rules pages cover rule syntax in depth. This section covers what's specific to Polymarket US.
One loop of a custom Polymarket US strategy
- Normal path
- Where the loop stops early or blocks entries
- Once, at startPick the marketmarket.slug is used as is; otherwise event_slug, then query, is searched once.If: event_slug or query finds no open marketThe Bot stopsIt logs a configuration error and never starts looping.
- Every loopRefresh edge dataOnly when the strategy declares edge data. Each alias is reused until its refresh period passes.If: Any alias missing, or older than 3 × refreshBlock entries this loopThe alias's fields read as unavailable. A matching buy rule is skipped; the rules below it, exits included, still run. Resting orders stay in place.
- Read stateMarket and account snapshotprice, spread, volume, time_to_expiry, position_size, unrealized_pnl, balance, order_count, edge fields.If: A market, book, position or open-order read failsLoop errorNothing trades this loop. (A failed balance read gives balance 0 instead.)
- EvaluateRules, top to bottomThe first rule whose when block is true wins. Later rules are not checked, unless the match is a buy while entries are blocked.If: No rule matchesNothing happensLogged as “no rule condition met”.
- ActRun that rule's actionOne action per loop: buy_yes, buy_no, sell_all, cancel_all or skip.If: A pre-order check failsOrder refusedThe rule can fire again next loop if it still matches.
- WaitSleep loop.interval secondsThen the next loop starts at “Refresh edge data”.
Condition fields
| Field | Unit | What it reads on Polymarket US |
|---|---|---|
price | $ (YES) | Last trade price, then current price, then the book midpoint. 0 if none is available. |
spread | $ | Best offer − best bid. A missing bid counts as 0 and a missing offer as 1. |
volume | number | The market's volume as reported by Polymarket US, as a whole number. |
time_to_expiry | duration | Time until the market's end date. Compare with a string such as "30m", "6h" or "1d". It reads 0 when the market has no end date, so time_to_expiry < ... is then always true. |
position_size | contracts | Contracts your account holds in this market, on either side. |
unrealized_pnl | $ | Read from the position data Polymarket US reports for this market, and it may be the position's value rather than its profit or loss. Always 0 in paper mode. Not recommended on Polymarket US: build take-profits and stop-losses on price instead. |
balance | $ | Your buying power. Reads 0 if the balance can't be read. |
order_count | count | Your account's open orders in this market. |
edge.<alias>.<field> | varies | A numeric field you declared under edge. |
position_size and order_count work per account and market, so orders you place by hand, or another Bot on the same market, are included. cancel_all cancels only this Bot's orders.
time_to_expiry values take one whole number and one lowercase unit: s, m, h or d. "1h30m", "6H" and a bare number are rejected.
Actions
| Action | What it does on Polymarket US |
|---|---|
buy_yes | Good-till-cancel limit buy of YES at the clamped last trade price. Skipped while any order rests in the market or buying power is short. |
buy_no | The same for NO, at 1 − the YES price. |
sell_all | Market sell of your whole position on the side you hold. Refused if the position is larger than Max contracts per order. |
cancel_all | Cancels this Bot's open orders in the market; your manual orders and other Bots' orders stay. It satisfies the "needs an exit" check, but it never closes a position. |
skip | Does nothing and stops checking rules for this loop. Useful as a guard above your entries. |
size sets contracts per entry. Omitting it (or size: 0) means 1, and decimals are dropped (1.9 becomes 1). A negative size or one larger than risk.max_position is rejected, and above 1 it also needs a higher Max contracts per order when you deploy.
What the validator checks
Studio rejects a custom strategy that:
- has no
sell_allorcancel_allrule ("custom strategy requires at least one rule with action sell_all or cancel_all"), - has exit rules that together fire in every state where you hold a position ("exit rules collectively match every held-position state"),
- has an exit whose only condition is holding a position, like
position_size > 0("exit predicate fires in profitable, neutral, and losing states"), - has an entry that always matches, a rule that can never match, or a rule that earlier rules make unreachable,
- has entry and exit rules that would trade back and forth on unchanged prices,
- has an entry
sizebelow 0 or aboverisk.max_position.
The fix for the exit errors is to give every exit a real condition: a price level, a time before close, or an edge value. The weather example on the Examples page shows a rejected first draft and its fix.
What the decision log shows
- When a rule matches, the Bot records that it fired before it acts. A fired
buy_yesorbuy_nocan still place nothing, because entries are skipped while an order is resting or buying power is short. - "Order placed" means Polymarket US accepted the order, not that it filled.
- If no rule matches, the log shows "no rule condition met".
- A refused order (a failed pre-order check or a deployment risk limit) shows as a loop error with the reason.
- Built-in strategies record only orders, errors and limit checks. Why a built-in didn't trade (spread below
spread_floor, no price yet, an order already resting) appears only in the Bot's log text.
See Monitoring.
Edge data on Polymarket US
Custom Polymarket US strategies can read outside data. Built-in strategies ignore it. The Edge Data pages list every field; the providers are:
| Provider | Selector | Default refresh | Minimum refresh | Units to know |
|---|---|---|---|---|
coinbase | symbol, e.g. BTC-USD | 5s | 1s, or 5s if you list any candle or indicator field | change_* fields are fractions: 0.003 = +0.3%. |
nws | station, e.g. KLGA (4 uppercase letters) | 5m | 60s | Humidity and precipitation probabilities are fractions: 0.70 = 70%. |
economic_calendar | event: FOMC, CPI or FOMC,CPI (no space) | 15m | 5m | seconds_to_next and seconds_since_previous are seconds. |
Rules may read only fields listed in the alias's fields. refresh is a duration like 30s, 5m or 1h30m (there is no d unit here). A field can be temporarily unavailable, for example when a Coinbase candle is late. Every comparison with an unavailable value is false, including !=, and that holds on either side of a value_field comparison.
Missing or stale data blocks entries
If any declared alias has no data, or data older than 3 × its refresh, the Bot blocks new entries for that loop and records an "edge_data blocked" decision. Every field of the missing alias reads as unavailable, so conditions on it are false. The rules still run: a buy_yes or buy_no rule that matches is skipped, and a sell_all or cancel_all rule whose conditions read only market data works as usual. Resting orders are left in place, and the Bot tries the feed again on the next loop.
An exit whose own conditions read the missing alias can't fire while it is down, so keep a price-based sell_all exit that doesn't depend on edge data.