Documentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-minio-s3-docker/llms.txt
Use this file to discover all available pages before exploring further.
Candles are immutable market data: once a specific (exchange, symbol, interval, timestamp) tuple has been written, the stored value will never change. The Candle adapter enforces this contract at the persistence layer with insert-only semantics — a stat (existence check) is performed before every PUT, and if the object already exists the write is skipped entirely. No body is downloaded during the check. This means replaying a warmup pass over a candle set that already exists in MinIO is cheap: every candle incurs one stat and zero writes.
The IPersistCandleInstance Contract
The adapter registered in setup.ts satisfies the IPersistCandleInstance interface from backtest-kit:
PersistCandleAdapter.usePersistCandleAdapter(class implements IPersistCandleInstance {
constructor(
readonly symbol: string,
readonly interval: CandleInterval,
readonly exchangeName: string,
) {}
async waitForInit(initial: boolean): Promise<void> {
if (!initial) return;
await waitForInfra(); // shared singleshot — gates on Redis ready
}
async writeCandlesData(candles: CandleData[]): Promise<void> {
for (const candle of candles) {
await ioc.candleDataService.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): Promise<CandleData[] | null> {
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.candleDataService.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;
}
});
backtest-kit constructs one adapter instance per (symbol, interval, exchangeName) triple. waitForInit is called once with initial: true on first use; subsequent calls with initial: false return immediately.
Object Key Schema
Every candle object is stored under a deterministic key composed of four segments:
exchange/symbol/interval/timestamp
The exchange segment is hard-coded to ccxt_binance in CandleDataService:
const EXCHANGE_NAME = "ccxt_binance";
const GET_STORAGE_KEY_FN = (
symbol: string,
interval: CandleInterval,
timestamp: number
) => {
return `${EXCHANGE_NAME}/${symbol}/${interval}/${timestamp}`;
};
Example keys inside the backtest-kit bucket under the candle-items/ prefix:
candle-items/ccxt_binance/BTCUSDT/1h/1704067200000
candle-items/ccxt_binance/TRXUSDT/15m/1704067200000
candle-items/ccxt_binance/ETHUSDT/4h/1704153600000
Because the key is a pure function of the candle’s identity, there is no ambiguity and no need for a lookup table.
Insert-Only create()
The CandleDataService.create() method is the sole write path. It first checks object existence with has() (a HEAD request — no body transfer) and only calls set() if the object does not yet exist:
public create = async (dto: ICandleDto): Promise<ICandleRow> => {
const key = GET_STORAGE_KEY_FN(dto.symbol, dto.interval, dto.timestamp);
const now = new Date();
const row: ICandleRow = {
id: key,
exchangeName: EXCHANGE_NAME,
symbol: dto.symbol,
interval: dto.interval,
timestamp: dto.timestamp,
open: dto.open,
high: dto.high,
low: dto.low,
close: dto.close,
volume: dto.volume,
createDate: now,
updatedDate: now,
};
// Candles are immutable: the row is fully determined by the dto, so an
// existence check (stat) replaces downloading the stored body.
if (await this.has(key)) {
return row;
}
await this.set(key, row);
return row;
};
The returned ICandleRow is reconstructed from the input DTO regardless of whether the write actually happened, so callers always receive a fully populated row.
Because candles are immutable, there is no update or merge path. If you need
to correct a stored candle — for example after a data-provider revision — you
must delete the object key manually from MinIO (via the MinIO console at
:9001, the mc CLI, or the S3 API) and then re-run the warmup pass that
originally wrote it. The adapter will then treat the missing key as new and
write the corrected value.
hasCandle() — Existence Check
CandleDataService exposes a dedicated existence check that the adapter can call without downloading the object body. It is used internally by create() and is also available to any code that needs a lightweight presence test before committing to a full read:
public hasCandle = async (
symbol: string,
interval: CandleInterval,
timestamp: number,
): Promise<boolean> => {
return await this.has(GET_STORAGE_KEY_FN(symbol, interval, timestamp));
};
Like the has() call inside create(), this is a HEAD-only request against MinIO — no object body is transferred. Returns true if the candle object exists in the bucket, false otherwise.
readCandlesData() Implementation
backtest-kit requests candles as a contiguous window: limit bars starting from sinceTimestamp. The adapter reconstructs this window by computing each bar’s timestamp from the arithmetic series sinceTimestamp + i × stepMs, then performing one point GET per bar:
async readCandlesData(limit: number, sinceTimestamp: number): Promise<CandleData[] | null> {
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.candleDataService.findBySymbolIntervalTimestamp(
this.symbol, this.interval, ts
);
if (!row) return null; // gap in the series → signal missing data to backtest-kit
result.push({
timestamp: row.timestamp,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume,
});
}
return result;
}
If any bar in the window is absent, the method returns null immediately, signalling to backtest-kit that the requested range is not fully available. The interval-to-milliseconds mapping is defined in setup.ts:
The INTERVAL_MINUTES record in setup.ts maps every supported
CandleInterval string to its duration in minutes: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,
};
stepMs = INTERVAL_MINUTES[interval] * MS_PER_MINUTE gives the exact
millisecond offset between consecutive bars, which is used to reconstruct
timestamp values for each point read.
ICandleDto and ICandleRow Schemas
The DTO is the write-side payload supplied by backtest-kit. The row is the persisted object shape stored in MinIO:
// src/schema/Candle.schema.ts
interface ICandleDto {
symbol: string;
interval: CandleInterval; // e.g. "1h", "15m", "1d"
timestamp: number; // Unix ms — open-time of the bar
open: number;
high: number;
low: number;
close: number;
volume: number;
}
interface ICandleRow extends ICandleDto {
id: string; // == GET_STORAGE_KEY_FN(symbol, interval, timestamp)
exchangeName: string; // always "ccxt_binance" in this project
createDate: Date;
updatedDate: Date;
}
ICandleRow extends ICandleDto — every field from the DTO is preserved verbatim in the stored row, making round-trips lossless.