Polymarket Built-in Strategies and Custom Rules
This page covers how strategies run on regular Polymarket (platform: polymarket): how each built-in strategy enters, prices, sizes and exits here, and what is specific to custom rules: the fields, the actions, how a custom buy is priced and sized, and what the validator checks when you save. For every key and its allowed values, see All Polymarket parameters.
Built-in strategies on Polymarket
All six strategy types work on Polymarket, but the built-ins run on Polymarket's own logic, so sizes, prices and exits differ from Kalshi. Parameter meanings and bounds are on Built-in Strategies. This is how each one trades here:
| Strategy | Enters when | Order price | Order size | Exits | Backtest |
|---|---|---|---|---|---|
spread_capture | The YES spread is at least spread_floor. No price gate. | Bids at the midpoint minus k × spread_floor/2, offers at plus the same, for k = 1 to order_count; bids clamped to floor and ceiling, offers capped at the ceiling | The market minimum (usually 5) per level, not configurable. Each offer is capped at the YES you hold, so several levels together can offer more than you hold, and several bids together can rest more than max_position. No offers when you hold no YES. | Sells through its offers. Each loop it cancels its own orders in the market and re-quotes, except at max_position, where it stops quoting and leaves the last quotes resting. | No |
mean_reversion | Flat, cooldown passed, nothing resting. YES if price is at or below entry_low; NO if at or above entry_high. | The side's midpoint | The market minimum; skipped if max_position is below it | Sells everything when YES is within ±0.02 of exit_target. No stop-loss. | Yes |
panic_fade | YES is at least panic_threshold below its 5-minute high, fewer than max_fades so far, no fade open, nothing resting | YES midpoint | fade_size, raised to the market minimum | Sells the YES the fade bought, at the YES bid, once price rises recovery_exit above the entry or drops below the floor | Yes |
observation_momentum | YES moved at least momentum_threshold across the last lookback_periods samples. Buys YES on a rise, NO on a fall, while holdings are under max_position and nothing rests. | The side's midpoint | 1, 2, 3… (linear) or 1, 2, 4… (exponential), raised to the market minimum and cut to what fits under max_position | Sells the side you hold, at its bid, when a qualifying move goes the other way | Runs, but records no trades |
pre_announcement_drift | Once per market, when the end date is within entry_hours_before hours and outside the blackout | The side's midpoint | max_position or 5, whichever is smaller, raised to the market minimum | Optional: sells what the entry bought when the blackout starts (exit_on_release) | Yes |
Things to know before you use a built-in here:
- Entries rest at the midpoint. They are limit orders and may not fill. An entry that hasn't filled after two loops (at least 60 seconds) is cancelled. Regular Polymarket cancels by market, so any other order resting in the market is cancelled with it.
- Exits clear the market first. Before a built-in sells, it cancels whatever is resting in the market: the unfilled rest of its own entry, or an earlier exit the bid has moved away from.
- Memory starts over in each market. On
series_slugorrecurring, each new market starts with a freshmean_reversioncooldown,panic_fadeprice history and fade count,observation_momentumhistory andpre_announcement_driftentry. Everything also resets when the Bot restarts. panic_fadeholds one fade at a time. Its exit sells the YES that fade bought, never shares you held before it, and the fade stays open until those shares are sold, even when a stop below the floor rests at the bid without filling.observation_momentumstops atmax_positionand sells what it holds when the move reverses. Its backtests record no trades, because Polymarket history has no last-trade prices.pre_announcement_driftneeds an end date. With a token-pinned market it never enters.ruleson a built-in are ignored on Polymarket, without an error.- Write params as plain, unquoted values. A quoted number such as
cooldown: "5m", or a quoted"false",no,off,yesoronforpost_onlyorexit_on_release, is rejected. Use unquoted numbers and unquotedtrueandfalse. See Theparamsblock.
Custom rules on Polymarket
With strategy: custom, the Bot runs your rules every loop, top to bottom. The first rule whose conditions match runs its action, and nothing else happens that loop. The full rule syntax is on Custom Rules. This section covers what is specific to Polymarket.
- Rule keys you can use here:
name,when,action,size. - At least one rule must be an exit:
sell_yes,sell_no,sell_allorcancel_all. Acancel_allrule alone passes this check but never sells anything; add a sell rule if you want the Bot to close positions. - Put exits above guards. A
skiporcancel_allrule that matches first also blocks the exits below it. - Do not use a double quote (
") in a rule name or selector value, and do not end one with a backslash. The strategy saves, but the Bot fails to start.
Fields
Twelve fields work on Polymarket:
| Field | On Polymarket | Backtest |
|---|---|---|
price | YES midpoint. Falls back to the ask, the bid, then the last trade; 0 if none. Always the YES side, even in a buy_no rule. | Yes |
spread | YES ask minus YES bid. A missing side counts as bid 0 or ask 1, so the spread can read 1. | Yes |
volume | The market's lifetime volume in dollars, read when the market is looked up (refreshed only at a series rollover). 0 for token-pinned markets. | No |
time_to_expiry | Seconds until the market's end date; 0 if unknown or past, and always 0 for token-pinned markets. Compare with durations such as "90s", "5m", "6h". | Yes |
position_size | YES + NO shares your wallet holds in the current market | Yes |
unrealized_pnl | Dollars: held shares valued at the best bid, minus their cost and fees from this market's trade history (from paper fills on a Paper Bot). 0 when flat. Unavailable when the position can't be valued (see below). | Yes |
balance | Available collateral in dollars | Yes |
order_count | Your open orders in this market, buy and sell, both tokens | Yes, but always 0 (orders never rest in backtests) |
yes_best_bid, yes_best_ask | Best bid and ask in the YES token's book | No |
no_best_bid, no_best_ask | Best bid and ask in the NO token's own book | No |
YES and NO are separate tokens with separate books. Live, the Bot never works out a NO price as 1 minus the YES price:
YES and NO are two separate books
yes_best_bid- 0.30
yes_best_ask- 0.32
price(midpoint)- 0.31
spread- 0.02
buy_yesrests at the midpoint- 0.31
sell_yesat the best bid- 0.30
no_best_bid- 0.66
no_best_ask- 0.70
buy_norests at the NO midpoint- 0.68
sell_noat the NO best bid- 0.66
Backtests only record the YES book. They build NO as its mirror: NO bid = 1 − 0.32 = 0.68 and NO ask = 1 − 0.30 = 0.70. Live, the NO book above (0.66 / 0.70) is used exactly as it is, so NO fills and P&L can differ between a backtest and a live Bot.
Empty books. A book field for an empty side has no value. Every comparison with it is false, whatever the operator, including !=, and on either side of a value_field comparison. If the NO book cannot be read on a loop, the NO fields have no value for that loop and the Bot logs a no_book error. If the YES book cannot be read, the whole loop is skipped.
unrealized_pnl can be unavailable. It is only worked out when one of your rules reads it. If a side you hold has no bid, or, on a live Bot, your holdings can't be matched to this market's recent trade history or that history can't be read, unrealized_pnl is unavailable for that loop: conditions on it are false whatever the operator, including !=, the Bot logs an unrealized_pnl error, and your other rules, exits included, still run. A P&L exit therefore can't fire until the position can be valued again, while a price exit doesn't depend on it. Keep this in mind for thin markets and for shares you moved in or out by hand. A Paper Bot values its position from its own paper fills, so there only a missing bid makes unrealized_pnl unavailable.
Actions
| Action | Order price | Size |
|---|---|---|
buy_yes | YES midpoint, rounded down to the tick | size (1 if omitted), then clamped (see How a custom buy is priced and sized) |
buy_no | NO midpoint, rounded down to the tick | Same as buy_yes |
sell_yes | YES best bid. With no bid, the YES ask (or last trade), so the order rests. | size, or all YES held if omitted |
sell_no | NO best bid. With no bid, the NO ask (or last trade), so the order rests. | size, or all NO held if omitted |
sell_all | Each side's best bid (the ask if there is no bid) | Everything held, both sides; size is ignored |
cancel_all | none | Cancels this Bot's open orders in this market |
skip | none | Does nothing, but stops later rules from running this loop |
sell_all sells everything your wallet holds in this market, including shares you bought by hand. cancel_all cancels only orders this Bot placed: Polymarket orders carry no client id, so the Bot keeps the id of every order the exchange accepts from it, and never cancels your manual orders or another Bot's. An order from before the Bot started keeping ids is not cancelled.
How a custom buy is priced and sized
On Kalshi a custom buy takes the best ask. On Polymarket it rests at the side's midpoint, so it can sit unfilled while the market moves. Without max_entries_per_market, any resting order in the market blocks the next buy; a cancel_all rule is how you clear a stale bid.
This is the path every buy_yes and buy_no takes after its rule fires:
What happens after a buy rule fires
Example: a buy_yes rule with size: 50, max_position: 100, max_notional: 10, a price band of 0.05 to 0.95, a YES midpoint of 0.50, nothing held or spent, and a market minimum of 5 shares.
- 1PriceThe side's midpoint (else the ask, the bid, then the last trade).0.50
- 2Starting size
size, or 1 share if omitted.50 shares - 3Entry guardWith
max_entries_per_market: skip once the cap is used. Without it: skip while any order rests in this market.nothing resting: continue - 4Position roomCut to
max_position−position_size.room 100 − 0 = 100: still 50 - 5Dollar budgetCut to what is left of
max_notional÷ price. Blocked if nothing is left.$10 ÷ 0.50 = 20 shares - 6Market minimumSkip if below the market's minimum order size. Studio logs an
entry_sizeblocked decision.20 ≥ 5: continue - 7Tick and boundslive onlyClamp to
price_floorandprice_ceiling, then round down to the tick.0.50 - 8Cash checkSkip if price × size is more than your available balance (paper cash for Paper Bots).needs $10.00
- 9OrderA limit order: it fills or rests.BUY 20 YES at 0.50
Same rule, different state
- No
max_notional, already holding 95 sharesroom is 5, so 5 shares go out
Sent - $4 already spent$6 ÷ 0.50 = 12 shares go out
Sent - $8 already spent$2 ÷ 0.50 = 4 shares, below the minimum of 5
Skipped sizeomitted1 share, below the minimum of 5
Skipped- $10 already spentthe max_notional budget is used up
Blocked
The position, budget, minimum-size and price steps are covered in more detail under risk.max_position, risk.max_notional, Order size and the market minimum and Price ticks and rounding.
What the validator checks
Besides field and action names, the validator checks rule behavior when you save. It rejects:
- conditions that can never match, or entries that match in every state;
- rules that earlier rules make unreachable;
- exit sets that together fire in every held state, and an exit whose only condition is on
position_sizeand that fires whenever you hold anything (such asposition_size > 0); - entries and exits that would alternate on an unchanged market (add bounds to the entry so the exit cannot fire straight after it);
- buy sizes that
max_positionormax_notionalcan never allow.
It does not know the market minimum, so a buy with no size saves but never trades.
Warning: Paper Bots can differ from live Bots. Paper orders skip tick rounding and the price clamp, and pay no fees by default, so paper results can differ from live near the price bounds. Custom paper buys still respect the market's minimum order size.
max_notionalcounts only resting paper buys, not filled ones, so on a Paper Bot it binds less than live andmax_positionis the cap that holds.max_entries_per_marketcounts a held position and resting buys, not completed paper trades, so after a round trip back to flat a Paper Bot can buy again where a live Bot would stop.