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>
| Parameter | Description |
|---|
symbol | Trading pair being evaluated, e.g. "TRXUSDT" |
when | The timestamp of the current candle, aligned to interval |
currentPrice | The 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.
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
}
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",
});
Create the strategy file
Create src/logic/strategy/feb_2026.strategy.ts and call addStrategySchema() with your new strategyName and a getSignal implementation.
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
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.