Skip to main content

Overview

The Autonome simulator provides a complete trading environment that mirrors live exchange behavior without risking real capital. It simulates order execution, position management, margin calculations, and portfolio tracking using real-time market data.

Environment Configuration

Configure the simulator through environment variables in your .env file:
.env
# Trading mode: "simulated" or "live"
TRADING_MODE=simulated

# Simulator configuration
SIM_INITIAL_CAPITAL=10000        # Starting capital in quote currency
SIM_QUOTE_CURRENCY=USDT          # Quote currency for all positions
SIM_REFRESH_INTERVAL_MS=10000    # Market data refresh interval (10s)

Configuration Options

TRADING_MODE
enum
required
Controls whether the application uses simulated or live trading.
  • simulated - Uses ExchangeSimulator (default, recommended for development)
  • live - Connects to actual exchange via Lighter API
SIM_INITIAL_CAPITAL
number
default:"10000"
Starting portfolio value in quote currency. This capital is available for margin allocation across positions.
SIM_QUOTE_CURRENCY
string
default:"USDT"
Quote currency used for all valuations and P&L calculations.
SIM_REFRESH_INTERVAL_MS
number
default:"10000"
Interval in milliseconds for refreshing market data and checking exit plan triggers. Lower values increase price accuracy but consume more API resources.

Initialization

Bootstrap Process

The simulator is initialized automatically during server startup through the scheduler bootstrap:
src/server/schedulers/bootstrap.ts
import { DEFAULT_SIMULATOR_OPTIONS, IS_SIMULATION_ENABLED } from "@/env";
import { ExchangeSimulator } from "@/server/features/simulator/exchangeSimulator";

export async function bootstrapSchedulers() {
  if (IS_SIMULATION_ENABLED) {
    await ExchangeSimulator.bootstrap(DEFAULT_SIMULATOR_OPTIONS);
  }
  // ... other schedulers
}
The bootstrap method ensures singleton behavior - subsequent calls return the existing instance:
src/server/features/simulator/exchangeSimulator.ts
static async bootstrap(
  options?: Partial<ExchangeSimulatorOptions>,
): Promise<ExchangeSimulator> {
  if (!globalThis.__exchangeSimulator) {
    globalThis.__exchangeSimulator = ExchangeSimulator.create({
      ...DEFAULT_SIMULATOR_OPTIONS,
      ...options,
    });
  }
  return globalThis.__exchangeSimulator;
}

Initialization Steps

During initialization, the simulator:
  1. Loads market metadata - Initializes MarketState for each symbol in MARKETS configuration
  2. Refreshes order books - Fetches initial prices from Lighter API /api/v1/orderBooks endpoint
  3. Rehydrates positions - Restores open positions from the Orders database table
  4. Starts polling - Begins periodic market data refresh and exit plan monitoring
src/server/features/simulator/exchangeSimulator.ts
private async initialise() {
  // Initialize markets with current prices
  for (const metadata of buildMarketMetadata()) {
    const market = new MarketState(metadata, orderApi);
    try {
      await market.refresh();
    } catch (error) {
      console.error(
        `[Simulator] Failed to initialize market ${metadata.symbol}:`,
        error instanceof Error ? error.message : error,
      );
    }
    this.markets.set(metadata.symbol, market);
  }

  // Restore open positions from database
  await this.restorePositionsFromDb();

  // Start periodic refresh
  this.startPolling();
}

Position Rehydration

The simulator restores open positions from the database on startup to maintain state across server restarts:
src/server/features/simulator/exchangeSimulator.ts
private async restorePositionsFromDb() {
  const { getAllOpenOrders } = await import(
    "@/server/db/ordersRepository.server"
  );
  const openOrders = await getAllOpenOrders();

  for (const order of openOrders) {
    const account = this.getOrCreateAccount(order.modelId);
    const symbol = normalizeSymbol(order.symbol);
    const market = this.markets.get(symbol);
    const markPrice = market?.getMidPrice() ?? parseFloat(order.entryPrice);

    // Restore position via synthetic execution
    const quantity = parseFloat(order.quantity);
    const entryPrice = parseFloat(order.entryPrice);
    const leverage = order.leverage ? parseFloat(order.leverage) : 1;
    const side = order.side === "LONG" ? "buy" : "sell";

    account.applyExecution(
      symbol,
      side,
      {
        fills: [{ quantity, price: entryPrice }],
        averagePrice: entryPrice,
        totalQuantity: quantity,
        totalFees: 0,
        status: "filled",
      },
      leverage,
    );

    // Restore exit plan if present
    if (order.exitPlan) {
      account.setExitPlan(symbol, order.exitPlan);
    }

    // Update mark price
    account.updateMarkPrice(symbol, markPrice);
  }
}
The Orders table serves as the single source of truth for positions. The simulator state is derived from this table on startup.

Switching Between Modes

Simulated Mode (Default)

Recommended for development, testing, and AI agent training:
.env
TRADING_MODE=simulated
Characteristics:
  • Zero risk to real capital
  • Instant execution with simulated slippage
  • Order book depth from real exchange data
  • Exit plans (stop-loss/take-profit) automatically executed
  • Full position and margin tracking

Live Mode

Connects to actual Lighter exchange:
.env
TRADING_MODE=live
LIGHTER_API_KEY_INDEX=2
LIGHTER_BASE_URL=https://mainnet.zklighter.elliot.ai
Live mode executes real trades with real capital. Ensure proper risk management and thoroughly test strategies in simulated mode first.

Runtime Detection

The trading layer automatically detects the mode:
src/env.ts
export const TRADING_MODE: TradingMode = env.TRADING_MODE;
export const IS_SIMULATION_ENABLED = env.TRADING_MODE === "simulated";
src/server/features/trading/createPosition.ts
if (IS_SIMULATION_ENABLED) {
  const simulator = await ExchangeSimulator.bootstrap(DEFAULT_SIMULATOR_OPTIONS);
  const result = await simulator.placeOrder(request, modelId);
  // ... handle simulated result
} else {
  // Use Lighter SDK for live trading
  const result = await lighterApi.placeOrder(request);
  // ... handle live result
}

Resetting the Simulator

Reset an account to initial capital and clear all positions:
import { orpc } from "@/server/orpc/client";

const { account } = await orpc.simulator.resetAccount({
  accountId: "model-apex-123",
});

console.log(`Reset to ${account.equity} ${account.quoteCurrency}`);
// Reset to 10000 USDT

Reset Implementation

src/server/features/simulator/exchangeSimulator.ts
resetAccount(accountId: string): AccountSnapshot {
  const normalized = accountId.trim().length > 0 ? accountId.trim() : "default";

  // Clear any pending auto-close triggers
  const pendingPrefix = `${normalized}:`;
  for (const key of Array.from(this.pendingAutoCloses)) {
    if (key.startsWith(pendingPrefix)) {
      this.pendingAutoCloses.delete(key);
    }
  }

  // Create fresh account state
  const account = new AccountState(this.options);
  this.accounts.set(normalized, account);
  const snapshot = account.getSnapshot();
  this.emitAccountSnapshot(normalized, snapshot);
  return snapshot;
}
Resetting an account only affects in-memory simulator state. To fully reset, also clear open orders from the database.

Market Data Refresh

The simulator polls market data at the configured interval:
src/server/features/simulator/exchangeSimulator.ts
private startPolling() {
  if (this.refreshHandle) return;
  this.refreshHandle = setInterval(() => {
    void this.refreshAll();
  }, this.options.refreshIntervalMs);
}

private async refreshAll() {
  // Update prices for all markets
  for (const [symbol, market] of this.markets) {
    try {
      const snapshot = await market.refresh();
      for (const account of this.accounts.values()) {
        account.updateMarkPrice(symbol, snapshot.midPrice);
      }
    } catch (error) {
      console.warn(`[Simulator] Market refresh failed for ${symbol}`);
    }
  }

  // Check exit plan triggers (stop-loss/take-profit)
  const autoCloseQueue = [];
  for (const [accountId, account] of this.accounts.entries()) {
    const triggers = account.collectExitPlanTriggers();
    for (const trigger of triggers) {
      autoCloseQueue.push({ accountId, symbol: trigger.symbol, trigger: trigger.trigger });
    }
    this.emitAccountSnapshot(accountId);
  }

  // Execute auto-closes
  for (const request of autoCloseQueue) {
    await this.closePositions([request.symbol], request.accountId);
  }
}

Price Data Sources

Market data is fetched from Lighter API in priority order:
  1. Order books endpoint - /api/v1/orderBooks (primary)
  2. Candles endpoint - /api/v1/candles with 1-minute resolution (fallback)
src/server/features/simulator/market.ts
async refresh(): Promise<OrderBookSnapshot> {
  // Try order books endpoint first
  const response = await axios.get(`${BASE_URL}/api/v1/orderBooks`);
  const orderBooks = response.data?.order_books ?? [];
  const marketData = orderBooks.find(
    (ob: any) => ob.market_id === this.metadata.marketId
  );

  if (marketData?.last_trade_price) {
    return this.orderBook.updateFromPrice(marketData.last_trade_price);
  }

  // Fallback to candles
  const candlesResponse = await axios.get(`${BASE_URL}/api/v1/candles`, {
    params: {
      market_id: this.metadata.marketId,
      resolution: '1m',
      count_back: 1,
    },
  });

  const candles = candlesResponse.data?.c ?? [];
  if (candles.length > 0) {
    return this.orderBook.updateFromPrice(candles[candles.length - 1].c);
  }

  throw new Error(`No price data for market ${this.metadata.symbol}`);
}

Next Steps

Simulator Features

Learn about order execution, portfolio tracking, and position management

Simulator API

API reference for simulator operations

Build docs developers (and LLMs) love