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) |
The core live trading loop is implemented in LiveLogicPrivateService.run():
The LiveUtils class provides a singleton interface for live trading operations:
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:
When a live trading process restarts after a crash:
PersistBase validates all JSON files on initialization:
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 |
Live mode emits events to three distinct channels:
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 |
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:
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 runningrunning: Active infinite loop in progressdone: 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:
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: