Skip to main content

Documentation Index

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

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

Strategies in backtest-kit-redis-mongo-docker are composed of two cooperating pieces: a frame that defines the time window and candle interval to replay, and a strategy that receives each candle tick and decides whether to open or manage a position. Both are registered through backtest-kit’s schema functions, then wired together at run time via the names you declare in the project’s enums.

Registering a Frame

A frame tells the backtester which date range and candle resolution to use. Call addFrameSchema() from backtest-kit and supply a frameName that matches an entry in FrameName, an interval string understood by your exchange (e.g. "1m", "4h"), startDate, endDate, and an optional human-readable note.
// src/logic/frame/jan_2026.frame.ts
import { addFrameSchema } from "backtest-kit";
import { FrameName } from "../../enum/FrameName";

addFrameSchema({
  frameName: FrameName.Jan2026Frame,
  interval: "1m",
  startDate: new Date("2026-01-01T00:00:00Z"),
  endDate: new Date("2026-01-31T23:59:59Z"),
  note: "January 2026",
});
The FrameName enum keeps every frame identifier in one place so TypeScript catches typos at compile time:
// src/enum/FrameName.ts
export enum FrameName {
    Jan2026Frame = "jan_2026_frame",
}

Registering a Strategy

Strategies are registered with addStrategySchema(). The only required fields are a strategyName string and a getSignal async callback. The callback fires on every candle tick for every symbol in the run and must return either a signal object (to open a position) or null (to do nothing).
// src/enum/StrategyName.ts
export enum StrategyName {
    Jan2026Strategy = "jan_2026_strategy",
}
// src/logic/strategy/jan_2026.strategy.ts (excerpt)
import {
  addStrategySchema,
  alignToInterval,
  getClosePrice,
  Position,
  getCandles,
} from "backtest-kit";
import { SignalEntryModel } from "../../model/SignalEntry.model";
import { readFileSync } from "fs";

const SIGNALS: SignalEntryModel[] = readFileSync("./assets/entry.jsonl", "utf-8")
  .split("\n")
  .filter(Boolean)
  .map((line) => JSON.parse(line));

function getActiveSignal(symbol: string, when: Date): SignalEntryModel | null {
  const now = when.getTime();
  const match = SIGNALS.find((s) => {
    if (s.symbol !== symbol) return false;
    const publishedAt = alignToInterval(new Date(s.publishedAt), "1m");
    return publishedAt.getTime() === now;
  });
  return match ?? null;
}

addStrategySchema({
  strategyName: "jan_2026_strategy",
  getSignal: async (symbol, when, currentPrice) => {
    const signal = getActiveSignal(symbol, when);
    if (!signal) return null;

    const close_1m = await getClosePrice(symbol, "1m");
    if (close_1m < signal.entry.from || close_1m > signal.entry.to) return null;

    const [close_4h_prev, close_4h_cur] = await getCandles(symbol, "4h", 2);
    const range_high = Math.max(close_4h_prev.high, close_4h_cur.high);
    const range_low = Math.max(close_4h_prev.low, close_4h_cur.low);
    const range_middle = range_high + range_low / 2;

    const position = close_1m > range_middle ? "short" : "long";

    return {
      position,
      ...Position.moonbag({
        position,
        currentPrice,
        percentStopLoss: 1.0,
      }),
      minuteEstimatedTime: 24 * 60,
      note: signal.note,
    };
  },
});

The getSignal callback signature

getSignal: (
  symbol: string,
  when: Date,
  currentPrice: number
) => Promise<signal | null>
ParameterDescription
symbolTrading pair being evaluated, e.g. "TRXUSDT"
whenThe timestamp of the current candle, aligned to interval
currentPriceThe candle’s close price at when
Return null to skip the tick. Return a signal object to open a position.

Loading Entry Signals from JSONL

External signals are loaded once at module startup from assets/entry.jsonl. Each line is a self-contained JSON object:
{"publishedAt": "2026-01-06T10:16:16Z", "symbol": "TRXUSDT", "direction": "short", "entry": {"from": 0.2898, "to": 0.293}, "targets": [0.2875, 0.2864, 0.2838, 0.2809, 0.2765], "stoploss": 0.3027, "note": "SIGNAL note text"}
The alignToInterval utility snaps an arbitrary timestamp to the nearest candle boundary so that signal publishedAt values line up exactly with when during replay:
const publishedAt = alignToInterval(new Date(s.publishedAt), "1m");
return publishedAt.getTime() === now;
alignToInterval is essential when signal timestamps come from external sources that may have sub-minute precision. Without it, strict equality comparisons against candle when values will never match.

Sizing Entries with Position.moonbag()

Position.moonbag() computes entry sizing and stop-loss levels from a percentage distance. Spread its return value into your signal object alongside any additional fields:
return {
  position,
  ...Position.moonbag({
    position,        // "long" | "short"
    currentPrice,
    percentStopLoss: 1.0,  // 1 % hard stop
  }),
  minuteEstimatedTime: 24 * 60,
  note: signal.note,
};

Adding Active-Ping Listeners

listenActivePing fires on every tick where there is an open position for the given symbol. Use it to implement exit logic that runs independently of entry logic.

Trailing Take-Profit

The listener below closes a position once the price has retreated 1 % from its peak profit:
import {
  listenActivePing,
  commitClosePending,
  getPositionHighestProfitDistancePnlPercentage,
  getPositionPnlPercent,
} from "backtest-kit";

const TRAILING_TAKE = 1.0;

listenActivePing(async ({ symbol, data }) => {
  const peakProfitDistance = await getPositionHighestProfitDistancePnlPercentage(symbol);
  const currentProfit = await getPositionPnlPercent(symbol);
  if (currentProfit < 0) return;
  if (peakProfitDistance < TRAILING_TAKE) return;
  await commitClosePending(symbol, { id: "unknown", note: "# Позиция закрыта по trailing take" });
});

Peak Staleness

This listener closes a position that reached a minimum profit but has since stagnated for too long without further progress:
import {
  listenActivePing,
  commitClosePending,
  getPositionHighestPnlPercentage,
  getPositionHighestProfitMinutes,
} from "backtest-kit";

const PEAK_STALENESS_SINCE_PROFIT = 1.0;
const PEAK_STALENESS_SINCE_MINUTES = 240;

listenActivePing(async ({ symbol, data }) => {
  const peakProfitCost = await getPositionHighestPnlPercentage(symbol);
  const peakProfitMinutes = await getPositionHighestProfitMinutes(symbol);
  if (peakProfitCost < PEAK_STALENESS_SINCE_PROFIT) return;
  if (peakProfitMinutes < PEAK_STALENESS_SINCE_MINUTES) return;
  await commitClosePending(symbol, { id: "unknown", note: "# Позиция закрыта по peak staleness" });
});
Both listeners can coexist. Each listenActivePing callback is independent — backtest-kit will call all registered listeners in registration order on every active-position tick.

Closing a Position with commitClosePending

commitClosePending marks a position for closure on the next tick. It requires the symbol and a close descriptor with an id and human-readable note:
await commitClosePending(symbol, { id: "unknown", note: "Reason for closing" });

Adding a New Frame and Strategy

Follow these steps whenever you need to introduce a new backtesting period or a new trading logic variant.
1

Add enum values

Add the new identifier to FrameName and StrategyName:
// src/enum/FrameName.ts
export enum FrameName {
    Jan2026Frame = "jan_2026_frame",
    Feb2026Frame = "feb_2026_frame",   // new
}
// src/enum/StrategyName.ts
export enum StrategyName {
    Jan2026Strategy = "jan_2026_strategy",
    Feb2026Strategy = "feb_2026_strategy",  // new
}
2

Create the frame file

Create src/logic/frame/feb_2026.frame.ts following the same pattern:
import { addFrameSchema } from "backtest-kit";
import { FrameName } from "../../enum/FrameName";

addFrameSchema({
  frameName: FrameName.Feb2026Frame,
  interval: "1m",
  startDate: new Date("2026-02-01T00:00:00Z"),
  endDate: new Date("2026-02-28T23:59:59Z"),
  note: "February 2026",
});
3

Create the strategy file

Create src/logic/strategy/feb_2026.strategy.ts and call addStrategySchema() with your new strategyName and a getSignal implementation.
4

Register both in index.ts

Import the new files in src/logic/index.ts so they execute at startup:
// src/logic/index.ts
import "./frame/jan_2026.frame";
import "./strategy/jan_2026.strategy";
import "./frame/feb_2026.frame";    // new
import "./strategy/feb_2026.strategy";  // new
5

Update the module file

Update modules/backtest.module.ts (and live.module.ts, paper.module.ts as needed) to reference the new frame and strategy names when configuring the run:
import { FrameName } from "../src/enum/FrameName";
import { StrategyName } from "../src/enum/StrategyName";

// pass FrameName.Feb2026Frame and StrategyName.Feb2026Strategy
// to the backtest runner

Error Handling

Register a global error listener with listenError to capture any exception thrown inside getSignal or the active-ping callbacks:
import { listenError, Log } from "backtest-kit";
import { errorData, getErrorMessage } from "functools-kit";

listenError((error) => {
  Log.debug("error", { error: errorData(error), message: getErrorMessage(error) });
});
Unhandled exceptions inside getSignal will silently skip the tick unless you register a listenError handler. Always add one at the bottom of your strategy file.

Build docs developers (and LLMs) love