A Rust bot for Polymarket that trades sports (binary) markets by slug using a trailing stop strategy. This document describes how it works in detail. More features may be added later.
| Binary | Description |
|---|---|
main_sports_trailing |
Sports trailing bot — slug-based, trailing only |
# Build
cargo build --release
# Simulation (no real orders)
cargo run --release -- --simulation
# Live
cargo run --release -- --no-simulationThe bot trades a single binary market identified by its slug (e.g. nfl-team-a-vs-team-b). The market has two outcome tokens (e.g. Team A / Team B). The strategy:
- Track both tokens: For each token, the bot keeps the lowest ask and highest ask seen over time.
- Trailing trigger: When the current ask recovers so that
ask ≥ lowest_seen + trailing_stop_point, that side is considered to have “recovered” and is a buy candidate. - First buy: The first side that triggers (or the cheaper one if both trigger in the same tick) is bought with a market order. Size is determined by
trailing_shares(with a minimum cost of $1). - Second buy: After the first buy, the bot trails only the opposite token. When that side’s ask reaches
opposite_lowest + trailing_stop_point, it buys the opposite token with the same share count as the first buy. - Once vs continuous: If
continuousisfalse, the bot stops after one pair of buys. Ifcontinuousistrue, it resets and repeats (trail both → buy first → trail opposite → buy second) until the market end time.
Prices are read from the Polymarket CLOB (order book): the bot uses the ask (SELL side) as the price to beat for the trailing logic.
The main loop is driven by a state machine with four states:
| State | Meaning |
|---|---|
| WaitingFirst | Tracking low/high for both tokens. When one (or both) satisfies ask ≥ low + trailing_stop, pick one and send first buy. |
| FirstBuyPending | First order is in flight. Loop does not process new triggers until the order completes (success or failure). |
| FirstBought | First token bought. Tracking only the opposite token’s lowest ask; when opp_ask ≥ opposite_lowest + trailing_stop, buy opposite side. |
| Done | Both sides bought for this round. If continuous, reset to WaitingFirst; otherwise keep sleeping until market ends. |
On first-buy failure, state reverts to WaitingFirst with the last known low/high so the bot can trigger again later.
flowchart TB
subgraph init["Startup"]
A[Load config.json] --> B[Authenticate with Polymarket]
B --> C[Get market by slug]
C --> D[Get condition_id + token0, token1 + end_date]
D --> E[Init state: WaitingFirst]
end
subgraph loop["Main loop (every check_interval_ms)"]
E --> F{Market ended?}
F -->|Yes| EXIT[Exit]
F -->|No| G[Fetch ask prices for token0, token1]
G --> H{State?}
H -->|WaitingFirst| I[Update low/high for both]
I --> J{Trigger? ask ≥ low + trail}
J -->|One or both| K[Execute first buy]
K --> L[State = FirstBuyPending]
L --> M[Wait for order result]
M -->|OK| N[State = FirstBought]
M -->|Fail| E
J -->|None| F
H -->|FirstBuyPending| F
H -->|FirstBought| O[Update opposite_lowest]
O --> P{opp_ask ≥ opposite_lowest + trail?}
P -->|Yes| Q[Execute second buy]
Q --> R{continuous?}
R -->|Yes| E
R -->|No| S[State = Done]
P -->|No| F
H -->|Done| R
S --> F
end
style EXIT fill:#f9f
style K fill:#9f9
style Q fill:#9f9
State transitions (simplified):
stateDiagram-v2
[*] --> WaitingFirst
WaitingFirst --> FirstBuyPending : trigger (one side)
FirstBuyPending --> FirstBought : first buy OK
FirstBuyPending --> WaitingFirst : first buy fail
FirstBought --> WaitingFirst : second buy OK + continuous
FirstBought --> Done : second buy OK + !continuous
Done --> WaitingFirst : continuous (next round)
Done --> [*] : market end or exit
| Component | Role |
|---|---|
| main_sports_trailing.rs | Entry point: parse args, load config, authenticate, resolve market by slug, run the state-machine loop. |
| PolymarketApi | get_market_by_slug, get_market (tokens, end_date_iso), get_price (BUY/SELL per token). |
| Trader | execute_buy(BuyOpportunity) — places a market buy (or simulates it when --simulation). |
| Config | slug, continuous, trailing_stop_point, trailing_shares, check_interval_ms, plus Polymarket API credentials. |
--simulation/--no-simulation— If simulation, no real orders are sent.--config <path>— Config file (default:config.json).
Relevant config fields:
- polymarket:
gamma_api_url,clob_api_url,api_key,api_secret,api_passphrase,private_key, optionalproxy_wallet_address,signature_type. - trading:
slug(required),continuous,trailing_stop_point,trailing_shares,check_interval_ms.
-
Install Rust (if needed):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
Build:
cargo build --release
-
Configure: Create or edit
config.jsonwith:- polymarket:
gamma_api_url,clob_api_url,api_key,api_secret,api_passphrase,private_key - Optional:
proxy_wallet_address,signature_type(1 = POLY_PROXY, 2 = GNOSIS_SAFE) - trading:
slug(required) — market slug, e.g."nfl-team-a-vs-team-b"continuous—true= repeat trail/buy both sides until market ends;false= one pair per markettrailing_stop_point(e.g.0.03)trailing_shares(e.g.10)check_interval_ms(e.g.1000)
- polymarket:
- The bot runs until the market end time or you stop it (Ctrl+C).
- Simulation mode logs trades but does not place orders.
- First buy uses at least $1 cost or
trailing_shares, whichever is larger (in share terms); second buy uses the same number of shares as the first.
- Do not commit
config.jsonwith real keys or secrets. - Prefer simulation and small sizes when testing.
- Monitor logs and balances when running in production.
- Optional limit orders instead of market-only for first/second buy.
- Configurable minimum time remaining before placing a buy (e.g. skip last N seconds).
- Support multiple slugs or a list of markets in one run.
- Logging/export of trailing state (low/high, trigger levels) for debugging and backtests.
- Optional webhook or notification on first buy / second buy / errors.
- Health check / readiness probe for running under a process manager.
- More robust handling of API rate limits and transient failures (retries, backoff).