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.

Cron is a built-in scheduler that fires handlers on candle-interval boundaries in virtual time — the same time stream your strategies see during backtesting. Registered jobs are coordinated across all parallel Backtest.background() runs: when multiple symbol backtests reach the same interval boundary simultaneously, only one handler invocation fires and all parallel waiters share the same in-flight promise. This singleshot coordination prevents double-firing without any external locking mechanism. Call Cron.enable() once at startup to subscribe to the engine’s lifecycle subjects. After that, every strategy tick is automatically forwarded to Cron — no manual wiring needed.

Types

CronCallback

type CronCallback = (
  symbol: string,
  when: Date,
  backtest: boolean
) => void | Promise<void>
symbol
string
In global mode: the symbol of the first parallel backtest to reach this boundary. In fan-out mode: the whitelisted symbol whose tick triggered this invocation.
when
Date
The aligned virtual time at which the entry fires. Already aligned to the entry’s interval boundary.
backtest
boolean
Execution mode from the originating lifecycle event.

CronEntry

interface CronEntry {
  name: string;
  interval?: CandleInterval;
  symbols?: string[];
  handler: CronCallback;
}

CronHandle

A disposer function returned by Cron.register. Calling it removes the entry — equivalent to Cron.unregister(name).
interface CronHandle {
  (): void;
}

Cron.register

Register a periodic or fire-once cron job. Re-registering the same name replaces the previous entry and bumps an internal generation counter, so late writes from an in-flight old handler are silently ignored.
Cron.register(entry: CronEntry): CronHandle
entry.name
string
required
Unique job name. Used as the deduplication key. Must not contain : (reserved as a slot-key segment separator).
entry.interval
CandleInterval
Candle interval at whose boundaries the handler fires (e.g., "1h", "4h", "1d"). Omit for fire-once mode: the handler fires on the very first matching tick and never again until clear() or unregister.
entry.symbols
string[]
Symbol whitelist. Omit for global mode (handler fires once per boundary across all parallel backtests). Provide for fan-out mode (handler fires once per boundary per listed symbol).
entry.handler
CronCallback
required
Async function to execute. Errors are caught and logged — a failed fire-once handler is not marked as fired and will retry on the next tick.
Returns a CronHandle disposer.

Cron.enable

Subscribe Cron to the engine lifecycle: beforeStartSubject, idlePingSubject, activePingSubject, schedulePingSubject. All four subjects are merged into a single serial singlerun queue so concurrent ticks on the same virtual minute cannot race.
Cron.enable(): () => void
Wrapped in singleshot — calling it multiple times is a no-op. Returns the same cleanup function on every call. Must be called before running any backtest or live session.

Cron.disable

Tear down the subscriptions installed by enable(). Does not unregister entries or clear fire-once marks.
Cron.disable(): void

Cron.unregister

Remove a registered job by name. In-flight handler promises are not cancelled — they settle on their own.
Cron.unregister(name: string): void
name
string
required
The name passed to register.

Cron.clear

Reset fire-once marks so that fire-once jobs can fire again.
Cron.clear(symbol?: string): void
symbol
string
When provided, clears only fan-out fire-once marks for that symbol. Global fire-once marks are left intact. Omit to clear all fire-once marks across all jobs and symbols.
clear() does not touch _inFlight promises. An in-flight handler completing after clear() will re-add its mark — use unregister + register to hard-reset a specific job (bumps the generation counter and invalidates any late writes).

Two Modes: Periodic vs Fire-Once

interval setModeWhen fires
PeriodicOnce per aligned boundary of that interval
❌ omittedFire-onceOn the very first matching tick, never again

Two Scopes: Global vs Fan-out

symbols setScopeInvocation cardinality
❌ omitted / emptyGlobalOnce per boundary total (first symbol wins the slot)
["BTC", "ETH"]Fan-outOnce per boundary per whitelisted symbol

Full Example

import { Cron, Backtest } from 'backtest-kit';

// Global hourly job — runs once per virtual hour across all parallel backtests
Cron.register({
  name: 'tg-signal-parser',
  interval: '1h',
  handler: async (symbol, when, backtest) => {
    await parseTelegramSignalsToMongo(when);
  },
});

// Per-symbol fan-out — runs once per hour for each listed symbol
Cron.register({
  name: 'fetch-funding',
  interval: '8h',
  symbols: ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'],
  handler: async (symbol, when, backtest) => {
    await fetchFundingRate(symbol, when);
  },
});

// Fire-once warm-up — runs exactly once globally at the first tick
const disposeWarmup = Cron.register({
  name: 'warm-cache',
  handler: async (symbol, when, backtest) => {
    await warmupCache();
    console.log('Cache warmed up at', when.toISOString());
  },
});

// Wire Cron to the engine once at startup
Cron.enable();

for (const symbol of ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'TRXUSDT']) {
  Backtest.background(symbol, { strategyName, exchangeName, frameName });
}

// On shutdown:
// Cron.disable();
// disposeWarmup();

Build docs developers (and LLMs) love