Live Trading

Live Trading mode executes trading strategies in real-time against current market conditions with automatic crash recovery. Unlike Backtest mode (see Backtesting), which simulates historical data, Live mode operates continuously with Date.now() timestamps and persists state to disk after every tick. This enables production-grade trading that survives process crashes, power failures, and system restarts.

For basic execution concepts, see Execution Modes. For persistence details, see Persistence Layer. For signal state management, see Signal Lifecycle.


Live Trading provides:

Feature Description
Infinite Loop Continuous while(true) monitoring that never completes
Real-time Timestamps Uses new Date() for current market time
Crash Recovery Restores active positions from disk on restart
State Persistence Atomic file writes after every tick
Graceful Shutdown Allows open positions to complete before stopping
Aspect Backtest Live
Timestamps Historical timeframes from FrameCoreService Real-time new Date()
Completion Finite (ends when timeframes exhausted) Infinite (runs until stopped)
Persistence Disabled (in-memory only) Enabled (atomic disk writes)
Speed Fast-forward through candles Real-time (1 tick per TICK_TTL)

Mermaid Diagram

The core live trading loop is implemented in LiveLogicPrivateService.run():

Mermaid Diagram

The LiveUtils class provides a singleton interface for live trading operations:

Mermaid Diagram


Live Trading persists four types of state to disk after every tick:

Adapter Path Content Purpose
PersistSignalAdapter ./dump/data/signal/{strategy}/{symbol}.json Active pending signals Restore open positions
PersistRiskAdapter ./dump/data/risk/{riskName}.json Portfolio positions Enforce risk limits
PersistScheduleAdapter ./dump/data/schedule/{strategy}/{symbol}.json Scheduled signals Restore pending limit orders
PersistPartialAdapter ./dump/data/partial/{strategy}/{symbol}.json Partial TP/SL levels Track milestone progress

All persistence uses the atomic write pattern to prevent data corruption:

Mermaid Diagram

When a live trading process restarts after a crash:

Mermaid Diagram

PersistBase validates all JSON files on initialization:

Mermaid Diagram


Each tick evaluates the current signal state and returns a discriminated union:

Result Action When Yielded to User Description
idle No active signal ❌ No Strategy is ready for new signal
scheduled Signal waiting for priceOpen ❌ No Limit order pending activation
active Signal open, monitoring TP/SL ❌ No Position is active
opened Signal just activated ✅ Yes New position opened
closed Signal hit TP/SL/time ✅ Yes Position closed with PNL

Mermaid Diagram

Live mode emits events to three distinct channels:

Mermaid Diagram


Each strategy declares a minimum interval that throttles signal generation:

addStrategy({
strategyName: "my-strategy",
interval: "5m", // Only generate signals every 5 minutes
getSignal: async (payload) => {
// Called at most once per 5 minutes
}
})
Interval Minutes Description
1m 1 Every tick (no throttling)
3m 3 Every 3 minutes
5m 5 Every 5 minutes
15m 15 Every 15 minutes
30m 30 Every 30 minutes
1h 60 Every hour

Mermaid Diagram

The TICK_TTL constant defines sleep duration between ticks:

TICK_TTL = 1 * 60 * 1_000 + 1
= 60,001 milliseconds
60 seconds

This ensures approximately 1 tick per minute, regardless of strategy interval. Strategies with longer intervals (e.g., 5m, 1h) simply skip calling getSignal() on intermediate ticks.


import { Live, addStrategy, addExchange } from "backtest-kit";

// Configure components
addStrategy({
strategyName: "my-live-strategy",
interval: "5m",
getSignal: async (payload) => {
// Strategy logic
}
});

addExchange({
exchangeName: "binance",
getCandles: async (symbol, interval, startTime, limit) => {
// Fetch real-time data from Binance API
}
});

// Run live trading
for await (const result of Live.run("BTCUSDT", {
strategyName: "my-live-strategy",
exchangeName: "binance"
})) {
if (result.action === "opened") {
console.log("Position opened:", result.signal.id);
}
if (result.action === "closed") {
console.log("Position closed:", result.pnl.pnlPercentage);
}
}
// Run in background without yielding results
const cancel = Live.background("BTCUSDT", {
strategyName: "my-live-strategy",
exchangeName: "binance"
});

// Stop gracefully after some condition
setTimeout(() => {
cancel(); // Sets stop flag
}, 3600000); // 1 hour
// Stop strategy from generating new signals
await Live.stop("BTCUSDT", "my-live-strategy");

// Get pending signal to check if position is still open
const pending = await strategyCoreService.getPendingSignal(
false, // backtest = false (live mode)
"BTCUSDT",
"my-live-strategy"
);

if (pending) {
console.log("Position still open, waiting for close...");
// Allow signal to complete normally
}
// Initial run - process crashes at 14:30 with open position
Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});
// [Process crashes]

// Restart - position is automatically recovered
Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});
// ClientStrategy.waitForInit() loads state from:
// - ./dump/data/signal/my-strategy/BTCUSDT.json
// - ./dump/data/risk/{riskName}.json
// - ./dump/data/schedule/my-strategy/BTCUSDT.json
// - ./dump/data/partial/my-strategy/BTCUSDT.json
// Trading resumes monitoring TP/SL exactly where it left off

LiveUtils uses memoize() to ensure one LiveInstance per symbol-strategy pair:

Key Format: "${symbol}:${strategyName}"
Example: "BTCUSDT:my-strategy"

Cache: Map<string, LiveInstance>

This prevents:

  • Multiple concurrent instances for the same pair
  • State conflicts between instances
  • Resource leaks from duplicate connections
const statusList = await Live.list();
statusList.forEach(status => {
console.log(`${status.id}: ${status.symbol} - ${status.strategyName}`);
console.log(`Status: ${status.status}`); // "idle" | "running" | "done"
});

Status Values:

  • idle: Instance created but not running
  • running: Active infinite loop in progress
  • done: Loop terminated (rare in live mode)

Context Property Backtest Live
backtest flag true false
when timestamp Historical from FrameCoreService Real-time new Date()
symbol Provided by user Provided by user

StrategyConnectionService.getStrategy() creates separate instances based on mode:

Backtest: memoize key = "${symbol}:${strategyName}:backtest"
Live: memoize key = "${symbol}:${strategyName}:live"

This allows running the same strategy in both modes simultaneously without interference.

Operation Backtest Live
PersistSignalAdapter Skipped Active
PersistRiskAdapter Skipped Active
PersistScheduleAdapter Skipped Active
PersistPartialAdapter Skipped Active
ClientStrategy.waitForInit() No-op Loads state from disk

Backtest mode skips all persistence for performance, while Live mode persists state after every tick to enable crash recovery.


When strategyCoreService.tick() throws an error:

try {
result = await this.strategyCoreService.tick(symbol, when, false);
} catch (error) {
console.warn(`tick failed when=${when.toISOString()} symbol=${symbol}`);
this.loggerService.warn("tick failed, retrying after sleep", { error });
await errorEmitter.next(error);
await sleep(TICK_TTL);
continue; // Retry on next iteration
}

The loop continues, allowing transient errors (network timeouts, API rate limits) to self-recover.

If atomic write fails:

  1. Temporary file is not renamed
  2. Original file remains unchanged
  3. Error is logged but does not crash the loop
  4. Next tick will retry persistence

This ensures trading continues even if disk I/O is temporarily unavailable.


Live mode emits performance metrics for each tick:

await performanceEmitter.next({
timestamp: Date.now(),
previousTimestamp: previousEventTimestamp,
metricType: "live_tick",
duration: tickEndTime - tickStartTime,
strategyName: this.methodContextService.context.strategyName,
exchangeName: this.methodContextService.context.exchangeName,
symbol,
backtest: false,
});

Subscribe to these events to monitor:

  • Tick processing duration
  • Time between ticks
  • System performance trends