Skip to main content

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.

Build docs developers (and LLMs) love