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:
# 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
Controls whether the application uses simulated or live trading.
simulated - Uses ExchangeSimulator (default, recommended for development)
live - Connects to actual exchange via Lighter API
Starting portfolio value in quote currency. This capital is available for margin allocation across positions.
Quote currency used for all valuations and P&L calculations.
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:
Loads market metadata - Initializes MarketState for each symbol in MARKETS configuration
Refreshes order books - Fetches initial prices from Lighter API /api/v1/orderBooks endpoint
Rehydrates positions - Restores open positions from the Orders database table
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:
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:
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:
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:
Order books endpoint - /api/v1/orderBooks (primary)
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