Skip to content

Actor Model

Arbiter-Bot uses an actor-based architecture with message passing for concurrent execution.

Overview

The system consists of three main actors communicating via tokio MPSC channels:

graph LR
    PM[PolymarketMonitor] -->|MarketUpdate| A[ArbiterActor]
    KM[KalshiMonitor] -->|MarketUpdate| A
    A -->|StartArbitrage| E[ExecutionActor]
    E -->|Leg1Result| E
    E -->|Leg2Result| E
    E -->|HedgeResult| E

Actors

ExecutionActor

Location: arbiter-engine/src/actors/executor.rs

Manages saga state machines for arbitrage executions:

  • Receives opportunities from ArbiterActor
  • Initiates Leg 1 orders (buy on first exchange)
  • Initiates Leg 2 orders (sell on second exchange)
  • Handles failures with hedge logic
  • Maintains concurrent saga state for multiple opportunities

Messages:

pub enum ExecutionMsg {
    StartArbitrage(Opportunity),
    Leg1Result(Uuid, Result<FillDetails, ExecutionError>),
    Leg2Result(Uuid, Result<FillDetails, ExecutionError>),
    HedgeResult(Uuid, Result<FillDetails, ExecutionError>),
}

ArbiterActor

Location: arbiter-engine/src/actors/arbiter.rs

Monitors markets and detects arbitrage opportunities:

  • Receives orderbook updates from monitors
  • Maintains current market state with staleness tracking
  • Runs arbitrage detection algorithm
  • Only uses verified market mappings (safety gate)
  • Sends detected opportunities to ExecutionActor

Messages:

pub enum ArbiterMsg {
    MarketUpdate(OrderBook),
    UpdateMappings(Vec<MarketMapping>),
}

Market Monitors

Locations:

  • arbiter-engine/src/market/polymarket.rs - PolymarketMonitor
  • arbiter-engine/src/market/kalshi.rs - KalshiMonitor

Connect to exchange WebSockets and publish orderbook updates:

  • Maintain persistent WebSocket connections
  • Parse market data messages
  • Convert to normalized OrderBook structs
  • Send updates to ArbiterActor

Actor Trait

All actors implement the base Actor trait:

#[async_trait]
pub trait Actor: Send + 'static {
    type Message: Send + 'static;
    async fn handle(&mut self, message: Self::Message) -> Result<(), ActorError>;
    async fn run(mut self, mut rx: mpsc::Receiver<Self::Message>) -> Result<(), ActorError>;
}

Benefits

  1. Isolation - Component failures don't cascade
  2. Testability - Easy to inject test messages
  3. Scalability - Add more actors for parallelism
  4. Clarity - Clear boundaries and interfaces