Skip to content

martinezpl/polysteer

Repository files navigation

Polysteer

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).

Architecture

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

Market Actor

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

Decision Actor

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.

Execution Actor

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.

Setup

git clone --recurse-submodules git@github.com:martinezpl/polysteer.git

See .env.example for required CLOB credentials

GUI

cargo run --bin polysteer-gui

The GUI lets you:

  1. Create events -- paste a Polymarket event slug from a URL to fetch market metadata. You can stream events to collect data for testing purposes.
  2. Launch bots -- pick an event, choose a strategy, tune config parameters, and start
  3. Monitor -- watch live order book, positions, P&L, and bot logs
  4. Stop -- graceful shutdown flushes logs and writes a session summary
image image

Adding a New Strategy

1. Create the strategy file

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
    }
}

2. Register it

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.

Key types available to strategies

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

Logging & Retrospective Analysis

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)

stream.log

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.

trades.csv

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.

session_summary.json

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.

License

MIT

About

Trading automation framework for Polymarket bots

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages