A framework for building and running automated trading strategies on Polymarket, written in Rust.
Polysteer provides an actor-based runtime that streams real-time market data, executes pluggable strategies, and posts orders -- all through a desktop GUI where you can create bots, configure parameters, and monitor live P&L.
It's designed for strategies targeting specific markets curated by user. It's not designed for intermarket strategies (e.g. arbitrage).
The system runs on three concurrent actor lanes, each on its own Tokio task, communicating through Arc<RwLock<_>> shared state and mpsc channels:
WebSocket Shared State Polymarket API
│ │ ▲
▼ ▼ │
┌──────────┐ MarketData ┌──────────┐ DecisionIntent ┌───────────┐
│ Market │─────────────▶│ Decision │────────────────▶│ Execution │
│ Actor │ │ Actor │ │ Actor │
└──────────┘ └──────────┘ └───────────┘
5ms book/trade 5ms tick Order posting
aggregation Strategy.on_tick() Fill tracking
Connects to the Polymarket WebSocket feed and aggregates raw events into a rich MarketData snapshot updated continuously. Computed features include:
- Best bid/ask, spread, microprice, VWAP
- Trade and book imbalance (rolling windows)
- EWMA volatility, trade rate, book update rate
- Depth ratios, bid-ask disagreement detection
Runs a 5 ms tick loop. On each tick it acquires a read lock on MarketData and ExecutionState, calls strategy.on_tick(), and forwards any resulting DecisionIntent values to the execution channel. The strategy has read-only access to the world -- it can only express intents, never touch the API directly.
Receives DecisionIntent messages and translates them into API calls against the Polymarket CLOB. The CLOB client (included as submodule) is a fork of another repo with some patches I needed for this project.
Supported operations:
- GTC (Good-Till-Cancel) limit orders
- FOK (Fill-or-Kill) immediate orders
- Batch FOK for atomic-style multi-leg execution
- Order cancellation (single or per-token)
- Position tracking with weighted-average entry price
- Budget enforcement via
max_spend
Fill confirmations arrive through a separate user WebSocket stream and are reconciled into ExecutionState.
git clone --recurse-submodules git@github.com:martinezpl/polysteer.git
See .env.example for required CLOB credentials
cargo run --bin polysteer-guiThe GUI lets you:
- Create events -- paste a Polymarket event slug from a URL to fetch market metadata. You can stream events to collect data for testing purposes.
- Launch bots -- pick an event, choose a strategy, tune config parameters, and start
- Monitor -- watch live order book, positions, P&L, and bot logs
- Stop -- graceful shutdown flushes logs and writes a session summary
Add src/actors/strategies/my_strategy.rs:
use rust_decimal::Decimal;
use crate::actors::execution::ExecutionState;
use crate::actors::market::MarketData;
use crate::actors::DecisionIntent;
use super::{ConfigField, ConfigFieldType, Strategy};
pub struct MyStrategy {
// your state here
}
impl Strategy for MyStrategy {
fn on_tick(
&mut self,
market: &MarketData,
execution: &ExecutionState,
) -> Vec<DecisionIntent> {
let mut intents = Vec::new();
// Read market features from market.get(token_id)
// Check positions in execution.positions
// Push DecisionIntent::FokBuy / FokSell / etc.
intents
}
fn kind(&self) -> &'static str { "my_strategy" }
fn default_config(&self) -> Option<serde_json::Value> {
// Return your default config as JSON, or None
None
}
fn config_schema(&self) -> Option<Vec<ConfigField>> {
// Return field definitions for GUI rendering, or None
None
}
fn set_log_dir(&mut self, path: &std::path::Path) {
// Initialize a TradeLogger here if you want trade-level CSV logging
}
fn on_stop(&mut self) {
// Flush logs, write session_summary.json
}
}In src/actors/strategies/mod.rs:
mod my_strategy;
pub use my_strategy::MyStrategy;Add an entry to available_strategies() and a match arm in build_strategy():
"my_strategy" => Box::new(MyStrategy::new()),That's it -- the new strategy will appear in the GUI's strategy dropdown on next build.
| Type | What it gives you |
|---|---|
AssetState |
Per-token snapshot: best_bid, best_ask, spread, microprice, vwap, volatility, trade_imbalance, top_imbalance, depth_ratio, trade_rate, min_order_size, and more |
MarketData |
Collection of AssetState values; get(token_id) to look up a token; get_condition_pairs() for binary-market token pairs |
ExecutionState |
positions (HashMap of token holdings), remaining_spend(), open_orders |
DecisionIntent |
FokBuy, FokSell, Buy, Sell, CancelOrder, CancelAllForToken, BatchFok |
Every bot run writes structured logs to appdata/bots/{bot_id}/:
appdata/bots/79d46f41-.../
├── bot.log # Execution state changes, heartbeats, PnL
├── config.json # Strategy config snapshot for reproducibility
├── stream.log # Raw WebSocket messages (timestamped JSON lines)
├── trades.csv # Per-trade log (ENTRY / DCA_BUY / EXIT events)
└── session_summary.json # Aggregated session stats (P&L, win rate, trade count)
Every WebSocket message received from the Polymarket feed is appended with an RFC 3339 timestamp:
[2026-02-08T14:32:01.234Z] {"event_type":"book","market":"0xabc...","bids":[...],"asks":[...]}
[2026-02-08T14:32:01.238Z] {"event_type":"last_trade_price","market":"0xabc...","price":"0.55"}
This is the full replay-able market tape. The framework includes a MockStream that can replay these logs at original or accelerated speed for backtesting.
Strategies that implement set_log_dir / on_stop (like volatile) write a CSV with one row per trade event:
timestamp, id, event, pair_id, token_id, outcome, price, shares, notional,
confidence, kelly_frac, spread, volatility, trade_imbalance, microprice_signal,
vwap_signal, top_imbalance, depth_signal, disagreement, exit_reason,
entry_price, pnl_per_share, pnl_total, hold_ms, high_water_mark
Each row captures the full market microstructure snapshot at the moment of the trade, making it straightforward to analyze signal quality, hold times, and P&L distribution after the fact.
Written on shutdown, this file aggregates the session into a single JSON object: total P&L, win/loss count, average hold time, and the config that produced the run -- everything needed to compare parameter sets across sessions.
MIT