Skip to main content

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.

Measure, Interval, and Memory are the three soft-delete adapters. Instead of physically removing documents, all three set removed: true on the record (and payload.removed: true inside the payload itself) to mark it as deleted. Listing operations filter on removed: false to skip tombstones. This matches the behavior of the default file-based persist implementation bundled with backtest-kit and makes it safe to replay or audit the full history of creates and deletes.

Soft-delete mechanics

When softRemove is called, a findOneAndUpdate with $set: { removed: true, "payload.removed": true } is issued — no document is physically deleted:
// Example from IntervalDbService
public softRemove = async (bucket: string, entryKey: string): Promise<void> => {
  const filter = { bucket, entryKey };
  const document = await IntervalModel.findOneAndUpdate(
    filter,
    { $set: { removed: true, "payload.removed": true } },
    { new: true },
  );
  if (!document) return;
  const result = readTransform(document.toJSON()) as unknown as IIntervalRow;
  await this.intervalCacheService.setIntervalId(result);
};
The Redis cache entry is updated with the soft-deleted document so that subsequent reads via findByKey correctly see removed: true without hitting MongoDB. listKeys / listEntries always passes { removed: false } as an additional filter to findAll, ensuring tombstones are invisible to the engine:
public listKeys = async (bucket: string): Promise<string[]> => {
  const rows = await super.findAll({ bucket, removed: false }) as IIntervalRow[];
  return rows.map((row) => row.entryKey);
};

Measure adapter

Caches LLM and external API responses. Because the data is fetched from an external service rather than derived from market data, it is intentionally exempt from look-ahead bias checking. Context key: (bucket: string, entryKey: string)
Collection: measure-items

IPersistMeasureInstance implementation

PersistMeasureAdapter.usePersistMeasureAdapter(class implements IPersistMeasureInstance {
  constructor(readonly bucket: string) {}

  async waitForInit(initial: boolean) {
    if (!initial) return;
    await waitForInfra();
  }

  async readMeasureData(key: string): Promise<MeasureData | null> {
    const row = await ioc.measureDbService.findByKey(this.bucket, key);
    if (!row || row.removed) return null;
    return row.payload;
  }

  async writeMeasureData(data: MeasureData, key: string, _when: Date): Promise<void> {
    await ioc.measureDbService.upsert(this.bucket, key, data);
  }

  async removeMeasureData(key: string): Promise<void> {
    await ioc.measureDbService.softRemove(this.bucket, key);
  }

  async *listMeasureData(): AsyncGenerator<string> {
    const keys = await ioc.measureDbService.listKeys(this.bucket);
    for (const key of keys) {
      yield key;
    }
  }
});
Note that _when is accepted but ignored in writeMeasureData — the Measure schema has no when field.

Measure Mongoose schema

From src/schema/Measure.schema.ts:
import mongoose, { Document, Schema } from "mongoose";
import { MeasureData } from "backtest-kit";

interface IMeasureDto {
  bucket: string;
  entryKey: string;
  payload: MeasureData;
  removed: boolean;
}

interface MeasureDocument extends IMeasureDto, Document {}

interface IMeasureRow extends IMeasureDto {
  id: string;
  createDate: Date;
  updatedDate: Date;
}

const MeasureSchema: Schema<MeasureDocument> = new Schema(
  {
    bucket:   { type: String, required: true, index: true },
    entryKey: { type: String, required: true, index: true },
    payload:  { type: Schema.Types.Mixed, required: true },
    removed:  { type: Boolean, required: true, default: false, index: true },
  },
  { timestamps: { createdAt: "createDate", updatedAt: "updatedDate" }, minimize: false }
);

MeasureSchema.index({ bucket: 1, entryKey: 1 }, { unique: true });

const MeasureModel = mongoose.model<MeasureDocument>("measure-items", MeasureSchema);
There is no when field. Measure data is treated as eternal cache — it has no temporal relationship with market data.

MeasureDbService — upsert and soft-remove

public upsert = async (
  bucket: string,
  entryKey: string,
  payload: MeasureData,
): Promise<void> => {
  const filter = { bucket, entryKey };
  const document = await MeasureModel.findOneAndUpdate(
    filter,
    { $set: { payload, removed: payload.removed } },
    { upsert: true, new: true, setDefaultsOnInsert: true },
  );
  const result = readTransform(document.toJSON()) as unknown as IMeasureRow;
  await this.measureCacheService.setMeasureId(result);
};

public softRemove = async (bucket: string, entryKey: string): Promise<void> => {
  const filter = { bucket, entryKey };
  const document = await MeasureModel.findOneAndUpdate(
    filter,
    { $set: { removed: true, "payload.removed": true } },
    { new: true },
  );
  if (!document) return;
  const result = readTransform(document.toJSON()) as unknown as IMeasureRow;
  await this.measureCacheService.setMeasureId(result);
};

public listKeys = async (bucket: string): Promise<string[]> => {
  const rows = await super.findAll({ bucket, removed: false }) as IMeasureRow[];
  return rows.map((row) => row.entryKey);
};
writeMeasureData also propagates payload.removed from the incoming MeasureData object itself. This means a caller can pre-mark a payload as removed by setting payload.removed = true before calling write, though calling removeMeasureData explicitly is the conventional approach.

Interval adapter

Tracks whether a one-time task has been completed within the current backtest interval. Useful for “run only once per candle” guards. Has a when: Date column for look-ahead bias protection. Context key: (bucket: string, entryKey: string)
Collection: interval-items

IPersistIntervalInstance implementation

PersistIntervalAdapter.usePersistIntervalAdapter(class implements IPersistIntervalInstance {
  constructor(readonly bucket: string) {}

  async waitForInit(initial: boolean) {
    if (!initial) return;
    await waitForInfra();
  }

  async readIntervalData(key: string): Promise<IntervalData | null> {
    const row = await ioc.intervalDbService.findByKey(this.bucket, key);
    if (!row || row.removed) return null;
    return row.payload;
  }

  async writeIntervalData(data: IntervalData, key: string, when: Date): Promise<void> {
    await ioc.intervalDbService.upsert(this.bucket, key, data, when);
  }

  async removeIntervalData(key: string): Promise<void> {
    await ioc.intervalDbService.softRemove(this.bucket, key);
  }

  async *listIntervalData(): AsyncGenerator<string> {
    const keys = await ioc.intervalDbService.listKeys(this.bucket);
    for (const key of keys) {
      yield key;
    }
  }
});

Interval Mongoose schema

From src/schema/Interval.schema.ts:
import mongoose, { Document, Schema } from "mongoose";
import { IntervalData } from "backtest-kit";

interface IIntervalDto {
  bucket: string;
  entryKey: string;
  payload: IntervalData;
  removed: boolean;
  when: number;
}

interface IntervalDocument extends IIntervalDto, Document {}

interface IIntervalRow extends IIntervalDto {
  id: string;
  createDate: Date;
  updatedDate: Date;
}

const IntervalSchema: Schema<IntervalDocument> = new Schema(
  {
    bucket:   { type: String, required: true, index: true },
    entryKey: { type: String, required: true, index: true },
    payload:  { type: Schema.Types.Mixed, required: true },
    removed:  { type: Boolean, required: true, default: false, index: true },
    when:     { type: Number, required: true, index: true },
  },
  { timestamps: { createdAt: "createDate", updatedAt: "updatedDate" }, minimize: false }
);

IntervalSchema.index({ bucket: 1, entryKey: 1 }, { unique: true });

const IntervalModel = mongoose.model<IntervalDocument>("interval-items", IntervalSchema);
Compared to MeasureSchema, IntervalSchema adds the when: Number field for bias protection.

IntervalDbServiceclearBucket for test cleanup

IntervalDbService provides one extra method not present on Measure or Memory: clearBucket. It physically deletes all documents for a bucket and removes their Redis cache entries, which is useful for resetting interval guards between integration test runs:
public clearBucket = async (bucket: string): Promise<void> => {
  const rows = await super.findAll({ bucket }) as IIntervalRow[];
  for (const row of rows) {
    await this.intervalCacheService.deleteIntervalId(bucket, row.entryKey);
  }
  await IntervalModel.deleteMany({ bucket });
};

Memory adapter

Per-signal key-value store. Any data a strategy wants to associate with a specific signal for its lifetime can be stored here. Entries are namespaced by (signalId, bucketName, memoryId) and retrieved as { memoryId, data } pairs via the async generator. Has a when: Date column. Context key: (signalId: string, bucketName: string, memoryId: string)
Collection: memory-items

IPersistMemoryInstance implementation

PersistMemoryAdapter.usePersistMemoryAdapter(class implements IPersistMemoryInstance {
  constructor(
    readonly signalId: string,
    readonly bucketName: string,
  ) {}

  async waitForInit(initial: boolean) {
    if (!initial) return;
    await waitForInfra();
  }

  async readMemoryData(memoryId: string): Promise<MemoryData | null> {
    const row = await ioc.memoryDbService.findByMemoryId(
      this.signalId, this.bucketName, memoryId,
    );
    if (!row || row.removed) return null;
    return row.payload;
  }

  async hasMemoryData(memoryId: string): Promise<boolean> {
    return await ioc.memoryDbService.hasMemoryEntry(
      this.signalId, this.bucketName, memoryId,
    );
  }

  async writeMemoryData(data: MemoryData, memoryId: string, when: Date): Promise<void> {
    await ioc.memoryDbService.upsert(
      this.signalId, this.bucketName, memoryId, data, when,
    );
  }

  async removeMemoryData(memoryId: string): Promise<void> {
    await ioc.memoryDbService.softRemove(
      this.signalId, this.bucketName, memoryId,
    );
  }

  async *listMemoryData(): AsyncGenerator<{ memoryId: string; data: MemoryData }> {
    const rows = await ioc.memoryDbService.listEntries(
      this.signalId, this.bucketName,
    );
    for (const row of rows) {
      yield { memoryId: row.memoryId, data: row.payload };
    }
  }

  dispose(): void { void 0; }
});
listMemoryData yields { memoryId, data } objects — not just keys — unlike Measure and Interval which yield only the key strings.

Memory Mongoose schema

From src/schema/Memory.schema.ts:
import mongoose, { Document, Schema } from "mongoose";
import { MemoryData } from "backtest-kit";

interface IMemoryDto {
  signalId: string;
  bucketName: string;
  memoryId: string;
  payload: MemoryData;
  removed: boolean;
  when: number;
}

interface MemoryDocument extends IMemoryDto, Document {}

interface IMemoryRow extends IMemoryDto {
  id: string;
  createDate: Date;
  updatedDate: Date;
}

const MemorySchema: Schema<MemoryDocument> = new Schema(
  {
    signalId:   { type: String, required: true, index: true },
    bucketName: { type: String, required: true, index: true },
    memoryId:   { type: String, required: true, index: true },
    payload:    { type: Schema.Types.Mixed, required: true },
    removed:    { type: Boolean, required: true, default: false, index: true },
    when:       { type: Number, required: true, index: true },
  },
  { timestamps: { createdAt: "createDate", updatedAt: "updatedDate" }, minimize: false }
);

MemorySchema.index(
  { signalId: 1, bucketName: 1, memoryId: 1 },
  { unique: true }
);

const MemoryModel = mongoose.model<MemoryDocument>("memory-items", MemorySchema);

MemoryDbServicehasMemoryEntry and listEntries

hasMemoryEntry is an existence check that tries Redis first, then falls back to MongoDB without returning the full payload:
public hasMemoryEntry = async (
  signalId: string,
  bucketName: string,
  memoryId: string,
): Promise<boolean> => {
  if (await this.memoryCacheService.hasMemoryEntryId(signalId, bucketName, memoryId)) {
    return true;
  }
  const row = await super.findByFilter({ signalId, bucketName, memoryId }) as IMemoryRow | null;
  if (row) {
    await this.memoryCacheService.setMemoryEntryId(row);
    return true;
  }
  return false;
};
listEntries returns all non-removed documents for a (signalId, bucketName) pair, providing the full IMemoryRow objects (including payload) rather than just the keys:
public listEntries = async (
  signalId: string,
  bucketName: string,
): Promise<IMemoryRow[]> => {
  return await super.findAll({ signalId, bucketName, removed: false }) as IMemoryRow[];
};

Build docs developers (and LLMs) love