Skip to content

Paper Trading

Paper trading lets you test strategies with simulated order execution before risking real capital. The simulation uses the same ExchangeClient interface as production, so your strategy code works unchanged.

Quick Start

# Run in paper trading mode
cargo run --manifest-path arbiter-engine/Cargo.toml -- --paper-trade

# With realistic fill simulation (crosses order book)
cargo run --manifest-path arbiter-engine/Cargo.toml -- --paper-trade --fidelity realistic

# With simulated network latency
cargo run --manifest-path arbiter-engine/Cargo.toml -- --paper-trade --latency 50ms

# Combined with Kalshi demo environment (recommended for testing)
cargo run --manifest-path arbiter-engine/Cargo.toml -- --paper-trade --kalshi-demo

Using Kalshi Demo Environment

For the safest testing experience, combine paper trading with Kalshi's demo environment:

# Set demo credentials
export KALSHI_DEMO_KEY_ID=your_demo_key_id
export KALSHI_DEMO_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...your demo key...
-----END RSA PRIVATE KEY-----"

# Run with both paper trading and Kalshi demo
cargo run --manifest-path arbiter-engine/Cargo.toml -- \
  --paper-trade --kalshi-demo --fidelity realistic

This provides:

  • Real market data from Kalshi's demo environment (mirrors production)
  • Simulated order execution via paper trading mode
  • No risk to production credentials (separate demo credentials)
  • Mock funds in Kalshi demo (no real capital)

See Kalshi Demo Environment to create demo credentials.

Simulation Fidelity Levels

Level Name Fill Logic Use Case
1 Basic Instant fill at mid-price Quick validation
2 Realistic Cross order book, partial fills Paper trading

Basic Fidelity (Default)

Orders fill instantly at the mid-price between best bid and ask. Fast but optimistic.

// Mid-price = (best_bid + best_ask) / 2
// Order fills completely at mid-price

Realistic Fidelity

Orders cross the order book level by level with partial fills. More accurate for sizing and slippage analysis.

// Buy order walks up the ask book
// Sell order walks down the bid book
// Respects limit prices

Configuration

Environment Variables

# Enable paper trading mode
ARBITER_PAPER_TRADE=true

# Set fidelity level (basic or realistic)
ARBITER_FIDELITY=realistic

# Simulated network latency
ARBITER_LATENCY_MS=50

Programmatic Configuration

use arbiter_engine::simulation::{SimulationConfig, FidelityLevel};
use arbiter_engine::clock::SimulatedClock;
use std::sync::Arc;
use std::time::Duration;

// Create simulation config
let config = SimulationConfig::default()
    .with_fidelity(FidelityLevel::Realistic)
    .with_latency(Duration::from_millis(50));

// Create simulated exchange client
let clock = Arc::new(SimulatedClock::new(Utc::now()));
let client = SimulatedExchangeClient::new(clock, config);

// Use like any ExchangeClient
let fill = client.place_order(order).await?;

Fee Simulation

Fees are calculated based on the exchange being simulated:

Kalshi Fees

// Formula: ceil(0.07 * contracts * price * (1 - price)) / 100
let config = SimulationConfig::kalshi();
Contracts Price Fee
100 0.50 $0.02
100 0.60 $0.02
100 0.90 $0.01

Polymarket Fees

// Currently 0% maker/taker fees
let config = SimulationConfig::polymarket();

Position Tracking

Paper positions are automatically tracked with realized and unrealized PnL:

use arbiter_engine::position::{PositionTracker, FillRecord, MarketId};

let tracker = PositionTracker::new(clock);

// Record a fill
let trade = tracker.record_fill(&FillRecord {
    market_id: MarketId::new("BTC-50K-2026"),
    side: OrderSide::Buy,
    quantity: dec!(100),
    price: dec!(0.55),
    fee: dec!(0.02),
})?;

// Check position
let position = tracker.get_position(&MarketId::new("BTC-50K-2026"));
println!("Position: {:?}", position);

// Update unrealized PnL with current price
tracker.update_unrealized_pnl(&market_id, dec!(0.60));

// Get total PnL
println!("Total PnL: {}", tracker.total_pnl());

Position Limits

Enforce risk controls during paper trading:

use arbiter_engine::position::PositionLimits;

let limits = PositionLimits {
    max_position_per_market: Some(dec!(1000)),  // Max 1000 contracts per market
    max_total_exposure: Some(dec!(10000)),       // Max $10k total notional
    max_open_positions: Some(5),                 // Max 5 open positions
};

let tracker = PositionTracker::with_limits(clock, limits);

// This will fail if limits exceeded
let result = tracker.record_fill(&large_fill);

Viewing Results

Console Output

# Run with verbose logging
RUST_LOG=info cargo run -- --paper-trade

Metrics

// Get trade statistics
let stats = tracker.trade_statistics()?;
println!("Win rate: {:.1}%", stats.win_rate * 100);
println!("Profit factor: {:.2}", stats.profit_factor);
println!("Total PnL: ${}", stats.total_pnl);

Comparison with Dry Run

Feature Dry Run (--dry-run) Paper Trade (--paper-trade)
Signs orders Yes No
Submits to exchange No No
Simulates fills No Yes
Tracks positions No Yes
Calculates PnL No Yes
Uses real market data Yes Yes

Use --dry-run to test API integration without execution. Use --paper-trade to simulate full trading with position tracking.

Next Steps