Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit/llms.txt

Use this file to discover all available pages before exploring further.

Live trading bots fail. Processes crash, servers reboot, deployments happen mid-trade. Backtest Kit handles all of this through a persist-and-restart pattern: every state-changing operation writes to disk before returning, so if the process dies at any moment, the next restart reads the last known state from disk and resumes monitoring exactly where it left off — no duplicate signals, no orphaned positions, no missed closes.

How it works

The core of crash recovery is PersistSignalAdapter — a singleton utility class that manages one IPersistSignalInstance per (symbol, strategyName, exchangeName) triple. Each instance writes the current ISignalRow (or null on close) as a JSON file using writeFileAtomic, which performs a write-then-rename so the file is never left in a corrupt state. File path convention:
./dump/data/signal/{strategyName}/{symbol}.json
On every live-mode tick, ClientStrategy.waitForInit() runs once (via singleshot) to load the persisted signal before the first getSignal call. If the file exists, the signal is restored and the strategy immediately starts monitoring TP/SL as if the position had just been opened.

waitForInit and the singleshot pattern

// Inside ClientStrategy (simplified)
public waitForInit = singleshot(async () => {
  if (!this.params.execution.context.backtest) {
    this._pendingSignal = await PersistSignalAdapter.readSignalData(
      this.params.execution.context.symbol,
      this.params.strategyName,
      this.params.exchangeName,
    );
  }
});
singleshot ensures the initialization runs exactly once per ClientStrategy instance, no matter how many ticks arrive concurrently. In backtest mode the read is skipped entirely — backtest always starts from a clean slate for speed.
In backtest mode, setPendingSignal always writes null (a no-op to disk). Persistence is disabled automatically — there is no configuration needed.

setPendingSignal — centralized state updates

All signal changes go through a single method:
// Inside ClientStrategy (simplified)
public async setPendingSignal(pendingSignal: ISignalRow | null) {
  this._pendingSignal = pendingSignal;
  if (!this.params.execution.context.backtest) {
    await PersistSignalAdapter.writeSignalData(
      this._pendingSignal,
      this.params.execution.context.symbol,
      this.params.strategyName,
      this.params.exchangeName,
    );
  }
}
Every state transition — signal opened, closed, TP moved, SL moved, DCA entry added, partial executed — flows through setPendingSignal. This means the on-disk state is always in sync with in-memory state. There is no separate “flush” step to remember.

Custom persistence adapter

The default adapter writes JSON files to ./dump/. You can replace it with any storage backend — Redis, MongoDB, PostgreSQL — by implementing IPersistSignalInstance and calling usePersistSignalAdapter():
import { PersistSignalAdapter, PersistBase } from "backtest-kit";

class RedisPersistInstance implements IPersistSignalInstance {
  constructor(
    private readonly symbol: string,
    private readonly strategyName: string,
    private readonly exchangeName: string,
  ) {}

  async waitForInit(initial: boolean): Promise<void> {
    // connect to Redis if needed
  }

  async readSignalData(): Promise<ISignalRow | null> {
    const key = `signal:${this.strategyName}:${this.exchangeName}:${this.symbol}`;
    const raw = await redisClient.get(key);
    return raw ? JSON.parse(raw) : null;
  }

  async writeSignalData(signalRow: ISignalRow | null): Promise<void> {
    const key = `signal:${this.strategyName}:${this.exchangeName}:${this.symbol}`;
    if (signalRow === null) {
      await redisClient.del(key);
    } else {
      await redisClient.set(key, JSON.stringify(signalRow));
    }
  }
}

// Register before starting Live.background()
PersistSignalAdapter.usePersistSignalAdapter(RedisPersistInstance);
The IPersistSignalInstance interface requires three methods. Alternatively, you can extend the PersistBase class (which uses readValue(entityId) and writeValue(entityId, entity) for raw key-value storage) and wrap it inside your IPersistSignalInstance implementation — the same pattern used by the built-in PersistSignalInstance.
waitForInit(initial)
Promise<void>
Initialize the storage backend for this context. Called once before first use via singleshot.
readSignalData()
Promise<ISignalRow | null>
Read and return the persisted signal for this (symbol, strategyName, exchangeName) triple, or null if none exists.
writeSignalData(signalRow)
Promise<void>
Write (or clear, if null) the signal state for this triple. Must be atomic to prevent corruption on crash.

Recovery flow

The full restart sequence is:
1

Process restarts

Live.background() is called with the same strategyName and exchangeName as before.
2

waitForInit loads persisted state

On the first tick, ClientStrategy.waitForInit() reads the signal JSON from disk (or your custom adapter). If a signal exists, _pendingSignal is set before getSignal is ever called.
3

Strategy resumes monitoring

The strategy immediately starts checking TP/SL on every tick, exactly as if the position had just been opened. getSignal will not be called again until the position closes.
4

Position closes normally

When TP or SL is hit, setPendingSignal(null) clears the on-disk state. The next restart will find no persisted signal and start fresh.

Production recommendations

For production deployments, consider using @backtest-kit/mongo which replaces the file-based storage with MongoDB as the source of truth and Redis as an O(1) lookup cache. All 15 IPersist*Instance contracts are implemented — strategy code stays unchanged.
npm install @backtest-kit/mongo backtest-kit mongoose ioredis
The default file-based storage works well for single-symbol bots but has two limitations at scale:
  1. Concurrent writes — multiple symbols writing simultaneously can saturate filesystem I/O
  2. No query capability — finding all open positions across strategies requires scanning the directory
MongoDB + Redis eliminates both: atomic findOneAndUpdate with unique compound indexes handles concurrent writes, and Redis provides O(1) per-context lookups so each tick is effectively a single GET + one findById, with no B-tree scans on the hot path.

Build docs developers (and LLMs) love