Skip to main content

Overview

Parsecular includes a strategy framework for building automated trading strategies. It provides a BaseStrategy class with common functionality like position tracking, order management, and event handling.

Core Components

Strategy Trait

#[async_trait]
pub trait Strategy: Send + Sync {
    fn name(&self) -> &str;
    fn config(&self) -> &StrategyConfig;
    fn state(&self) -> StrategyState;

    async fn on_tick(&mut self) -> Result<(), ParsecError>;

    async fn start(&mut self) -> Result<(), ParsecError>;
    async fn stop(&mut self) -> Result<(), ParsecError>;
    fn pause(&mut self);
    fn resume(&mut self);
}

Strategy States

pub enum StrategyState {
    Stopped,   // Strategy not running
    Running,   // Strategy actively trading
    Paused,    // Strategy paused (no new orders)
}

Strategy Events

pub enum StrategyEvent {
    Started,           // Strategy started
    Stopped,           // Strategy stopped
    Paused,            // Strategy paused
    Resumed,           // Strategy resumed
    Order(Order),      // Order placed
    Error(String),     // Error occurred
    Tick,              // Tick completed
}

Configuration

StrategyConfig

pub struct StrategyConfig {
    pub tick_interval_ms: u64,      // Default: 1000
    pub max_position_size: f64,     // Default: 100.0
    pub spread_bps: u32,            // Default: 100
    pub verbose: bool,              // Default: false
}

MarketMakingConfig

For market-making strategies:
pub struct MarketMakingConfig {
    pub max_exposure: f64,         // Default: 1000.0
    pub check_interval_ms: u64,    // Default: 2000
    pub min_spread_bps: u32,       // Default: 50
    pub max_order_size: f64,       // Default: 100.0
    pub verbose: bool,             // Default: false
}

BaseStrategy

The BaseStrategy provides common trading functionality:
pub struct BaseStrategy<E: Exchange + 'static> {
    pub exchange: Arc<E>,
    pub market_id: String,
    pub market: Option<Market>,
    pub state: StrategyState,
    pub config: StrategyConfig,
    pub positions: Vec<Position>,
    pub open_orders: Vec<Order>,
    pub event_tx: broadcast::Sender<StrategyEvent>,
}

Key Methods

MethodDescription
new()Create a new strategy instance
subscribe()Subscribe to strategy events
refresh_state()Refresh positions and orders
cancel_all_orders()Cancel all open orders
get_position()Get position for an outcome
get_net_position()Get net exposure
place_order()Place a new order
run_loop()Run the main strategy loop
pause() / resume()Control strategy state
get_account_state()Get balance and positions

Basic Example

use pc_core::{BaseStrategy, Exchange, StrategyConfig, OrderSide, ParsecError};
use pc_exchange_polymarket::{Polymarket, PolymarketConfig};
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize exchange with credentials
    let config = PolymarketConfig::new()
        .with_private_key("0x...")
        .with_funder("0x...");
    let exchange = Arc::new(Polymarket::new(config)?);

    // Configure strategy
    let strategy_config = StrategyConfig {
        tick_interval_ms: 5000,   // 5 second ticks
        max_position_size: 50.0,
        spread_bps: 100,          // 1% spread
        verbose: true,
    };

    // Create strategy
    let mut strategy = BaseStrategy::new(
        exchange,
        "market-id".to_string(),
        strategy_config,
    );

    // Subscribe to events
    let mut events = strategy.subscribe();

    // Spawn event handler
    tokio::spawn(async move {
        while let Ok(event) = events.recv().await {
            match event {
                StrategyEvent::Order(order) => {
                    println!("Order placed: {} {:?} @ {:.4}",
                        order.outcome, order.side, order.price);
                }
                StrategyEvent::Error(msg) => {
                    println!("Error: {}", msg);
                }
                StrategyEvent::Tick => {
                    println!("Tick completed");
                }
                _ => {}
            }
        }
    });

    // Run strategy loop
    strategy.run_loop(|strat| async move {
        // Your trading logic here
        let net_position = strat.get_net_position();
        println!("Net position: {}", net_position);

        // Check account state
        let account = strat.get_account_state().await?;
        let balance = account.balance.get("USDC").copied().unwrap_or(0.0);
        println!("Balance: ${:.2}", balance);

        Ok(())
    }).await?;

    Ok(())
}

Market Making Example

use pc_core::{BaseStrategy, StrategyConfig, OrderSide};
use std::sync::Arc;

async fn market_making_strategy<E: Exchange + 'static>(
    exchange: Arc<E>,
    market_id: String,
) -> anyhow::Result<()> {
    let config = StrategyConfig {
        tick_interval_ms: 2000,
        max_position_size: 100.0,
        spread_bps: 50, // 0.5% spread
        verbose: true,
    };

    let mut strategy = BaseStrategy::new(exchange, market_id, config);

    strategy.run_loop(|strat| async move {
        // Get market info
        let market = strat.market.as_ref().unwrap();
        let tokens = market.get_outcome_tokens();

        // Get current prices
        let yes_price = market.prices.get("Yes").copied().unwrap_or(0.5);

        // Calculate bid/ask with spread
        let (bid, ask) = strat.calculate_spread_prices(yes_price, strat.config.spread_bps);

        // Check position limits
        let net_pos = strat.get_net_position();
        let max_pos = strat.config.max_position_size;

        // Place orders if within limits
        if net_pos < max_pos {
            // Place bid
            let size = strat.calculate_order_size(bid, 100.0);
            if let Some(token) = tokens.iter().find(|t| t.outcome == "Yes") {
                strat.place_order("Yes", OrderSide::Buy, bid, size, Some(&token.token_id)).await?;
            }
        }

        if net_pos > -max_pos {
            // Place ask
            let size = strat.calculate_order_size(ask, 100.0);
            if let Some(token) = tokens.iter().find(|t| t.outcome == "Yes") {
                strat.place_order("Yes", OrderSide::Sell, ask, size, Some(&token.token_id)).await?;
            }
        }

        Ok(())
    }).await?;

    Ok(())
}

Event Subscription

let mut strategy = BaseStrategy::new(exchange, market_id, config);

// Subscribe to events
let mut rx = strategy.subscribe();

// Handle events in a separate task
tokio::spawn(async move {
    while let Ok(event) = rx.recv().await {
        match event {
            StrategyEvent::Started => println!("Strategy started"),
            StrategyEvent::Stopped => println!("Strategy stopped"),
            StrategyEvent::Paused => println!("Strategy paused"),
            StrategyEvent::Resumed => println!("Strategy resumed"),
            StrategyEvent::Order(order) => {
                println!("Order: {} {:?} {} @ ${:.4}",
                    order.outcome, order.side, order.size, order.price);
            }
            StrategyEvent::Error(msg) => println!("Error: {}", msg),
            StrategyEvent::Tick => println!("Tick"),
        }
    }
});

Controlling Strategy

// Start the strategy
strategy.run_loop(on_tick).await?;

// Pause trading (stops new orders but maintains positions)
strategy.pause();

// Resume trading
strategy.resume();

// Signal stop (will exit run_loop gracefully)
strategy.signal_stop().await;

Account State

let account = strategy.get_account_state().await?;

// Balance by asset
for (asset, amount) in &account.balance {
    println!("{}: ${:.2}", asset, amount);
}

// Positions
for pos in &account.positions {
    println!("{} {}: {} @ ${:.4}",
        pos.market_id, pos.outcome, pos.size, pos.average_price);
}

Utility Methods

Calculate Order Size

// Calculate size based on liquidity and max exposure
let size = strategy.calculate_order_size(price, max_exposure);

Calculate Spread Prices

// Calculate bid/ask from mid price
let mid_price = 0.50;
let spread_bps = 100; // 1%
let (bid, ask) = strategy.calculate_spread_prices(mid_price, spread_bps);
// bid = 0.4975, ask = 0.5025

Best Practices

The run_loop automatically refreshes state each tick. If running custom logic, call refresh_state() periodically.
Errors in the tick handler are logged and emitted as events but don’t stop the strategy.
Always check position limits before placing orders to avoid excessive exposure.
Consider canceling all orders when stopping the strategy to avoid orphaned orders.
strategy.cancel_all_orders().await?;

Next Steps