Documentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-redis-mongo-docker/llms.txt
Use this file to discover all available pages before exploring further.
The Candle adapter persists OHLCV (Open, High, Low, Close, Volume) bars in the candle-items MongoDB collection. Candle data is treated as immutable: the very first write for a given (symbol, interval, timestamp) triplet wins, and all subsequent write attempts for the same key are no-ops. Reads walk the candle series timestamp-by-timestamp, returning null for the whole batch if any bar is missing — ensuring backtest-kit always gets a gapless window or nothing at all.
IPersistCandleInstance implementation
The adapter is registered in src/config/setup.ts:
PersistCandleAdapter.usePersistCandleAdapter(class implements IPersistCandleInstance {
constructor(
readonly symbol: string,
readonly interval: CandleInterval,
readonly exchangeName: string,
) {}
async waitForInit(initial: boolean) {
if (!initial) return;
await waitForInfra();
}
async writeCandlesData(candles: CandleData[]): Promise<void> {
for (const candle of candles) {
await ioc.candleDbService.create({
symbol: this.symbol,
interval: this.interval,
close: candle.close,
high: candle.high,
low: candle.low,
open: candle.open,
timestamp: candle.timestamp,
volume: candle.volume,
});
}
}
async readCandlesData(limit: number, sinceTimestamp: number) {
const stepMs = INTERVAL_MINUTES[this.interval] * MS_PER_MINUTE;
const result: CandleData[] = [];
for (let i = 0; i < limit; i++) {
const ts = sinceTimestamp + i * stepMs;
const row = await ioc.candleDbService.findBySymbolIntervalTimestamp(
this.symbol, this.interval, ts,
);
if (!row) return null;
result.push({
timestamp: row.timestamp,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume,
});
}
return result;
}
});
readCandlesData returns null as soon as any bar in the requested window is missing. This is intentional — backtest-kit expects a fully contiguous slice or nothing, to avoid silent gaps in indicator calculations.
Candle Mongoose schema
From src/schema/Candle.schema.ts:
import { CandleInterval } from "backtest-kit";
import mongoose, { Document, Schema } from "mongoose";
const INTERVAL_ENUM = [
"1m", "3m", "5m", "15m", "30m",
"1h", "2h", "4h", "6h", "8h", "1d",
] as const;
interface ICandleDto {
symbol: string;
interval: CandleInterval;
timestamp: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
interface CandleDocument extends ICandleDto, Document {
exchangeName: string;
}
interface ICandleRow extends ICandleDto {
id: string;
exchangeName: string;
createDate: Date;
updatedDate: Date;
}
const CandleSchema: Schema<CandleDocument> = new Schema(
{
symbol: { type: String, required: true, index: true },
interval: { type: String, required: true, enum: INTERVAL_ENUM, index: true },
timestamp: { type: Number, required: true, index: true },
exchangeName: { type: String, required: true, index: true },
open: { type: Number, required: true },
high: { type: Number, required: true },
low: { type: Number, required: true },
close: { type: Number, required: true },
volume: { type: Number, required: true },
},
{ timestamps: { createdAt: "createDate", updatedAt: "updatedDate" } }
);
CandleSchema.index({ symbol: 1, interval: 1, timestamp: 1 }, { unique: true });
const CandleModel = mongoose.model<CandleDocument>("candle-items", CandleSchema);
$setOnInsert — first write wins
CandleDbService.create uses $setOnInsert with upsert: true. MongoDB only applies $setOnInsert fields when a document is actually inserted (i.e. no existing document matched the filter). If a document with the same (symbol, interval, timestamp) already exists, the operation becomes a no-op and the existing data is left untouched:
public create = async (dto: ICandleDto): Promise<ICandleRow> => {
const filter = {
symbol: dto.symbol,
interval: dto.interval,
timestamp: dto.timestamp,
};
const insertOnly = {
exchangeName: EXCHANGE_NAME, // hardcoded to "ccxt_binance"
open: dto.open,
high: dto.high,
low: dto.low,
close: dto.close,
volume: dto.volume,
};
const document = await CandleModel.findOneAndUpdate(
filter,
{ $setOnInsert: insertOnly },
{ upsert: true, new: true, setDefaultsOnInsert: true },
);
const result = readTransform(document.toJSON()) as unknown as ICandleRow;
await this.candleCacheService.setCandleId(result);
return result;
};
This guarantees historical bar integrity: a candle ingested from a data feed can never be silently overwritten by a later, potentially stale, re-delivery of the same bar.
Redis cache for findBySymbolIntervalTimestamp
findBySymbolIntervalTimestamp first asks Redis for the cached MongoDB _id. On a cache hit it goes straight to findById (an indexed _id lookup). On a miss it falls back to a full filter query and then primes the cache:
public findBySymbolIntervalTimestamp = async (
symbol: string,
interval: CandleInterval,
timestamp: number,
): Promise<ICandleRow | null> => {
try {
const cachedId = await this.candleCacheService.getCandleId(
symbol, interval, EXCHANGE_NAME, timestamp,
);
return await super.findById(cachedId) as ICandleRow | null;
} catch {
const result = await super.findByFilter({
symbol, interval, exchangeName: EXCHANGE_NAME, timestamp,
});
if (result) {
await this.candleCacheService.setCandleId(result);
}
return result;
}
};
INTERVAL_MINUTES step mapping
readCandlesData converts the CandleInterval string to milliseconds using this lookup table, which is defined in src/config/setup.ts:
const MS_PER_MINUTE = 60_000;
const INTERVAL_MINUTES: Record<CandleInterval, number> = {
"1m": 1,
"3m": 3,
"5m": 5,
"15m": 15,
"30m": 30,
"1h": 60,
"2h": 120,
"4h": 240,
"6h": 360,
"8h": 480,
"1d": 1440,
};
The step between consecutive bars is INTERVAL_MINUTES[interval] * MS_PER_MINUTE. Starting from sinceTimestamp, each iteration increments by one step to build the series.
The Candle adapter has no when: Date column. Because historical candles are immutable facts about past prices, there is no look-ahead bias concern — the data that existed at any point in time is exactly the data that will always exist for that bar.