Documentation Index
Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit-redis-postgres-pgpool-docker/llms.txt
Use this file to discover all available pages before exploring further.
Measure, Interval, and Memory share a soft-delete pattern instead of hard-deleting rows. When a key is removed, a single server-side UPDATE sets removed = true and flips a removed flag inside the payload JSONB blob via jsonb_set — no row is ever physically deleted from the database. The listKeys / listEntries queries filter on removed = false to skip tombstones. This mirrors the semantics of the default file-based PersistMeasureInstance, PersistIntervalInstance, and PersistMemoryInstance.
Soft-delete is implemented with a single server-side UPDATE … SET removed = true, payload = jsonb_set("payload", '{removed}', 'true') WHERE … RETURNING *. There is no read-modify-write cycle, so a concurrent upsert that arrives while a remove is in flight cannot produce a lost update.
Measure Adapter
The Measure adapter is a general-purpose key/value cache for expensive computations — LLM API responses, parsed data, or any result that should be fetched once and reused. It is intentionally exempt from look-ahead bias protection: its writeMeasureData signature accepts a _when: Date argument but discards it, and the schema has no when column.
Context Key and Table
| Field | Type | Role |
|---|
bucket | string | Namespace grouping related keys |
entryKey | string | Key within the bucket — passed at read/write/remove time |
Table: measure-items — unique index measure_items_uq on (bucket, entryKey)
Registration
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;
}
}
});
Table Schema
const MeasureModel = new EntitySchema<IMeasureRow>({
name: "measure-items",
columns: {
id: { type: "uuid", primary: true, generated: "uuid" },
bucket: { type: String },
entryKey: { type: String },
payload: { type: "jsonb" },
removed: { type: "boolean", default: false },
createDate: { type: "timestamptz", createDate: true },
updatedDate: { type: "timestamptz", updateDate: true },
},
indices: [
{ name: "measure_items_uq", columns: ["bucket", "entryKey"], unique: true },
],
});
MeasureDbService.softRemove — Atomic Soft-Delete
public softRemove = async (bucket: string, entryKey: string): Promise<void> => {
const repo = await this.repo<IMeasureRow>();
const { raw } = await repo
.createQueryBuilder()
.update()
.set({
removed: true,
payload: () => `jsonb_set("payload", '{removed}', 'true')`,
})
.where({ bucket, entryKey })
.returning("*")
.execute();
const saved = raw[0] as IMeasureRow | undefined;
if (!saved) return;
await this.measureCacheService.setMeasureId(saved); // update Redis with tombstone row
};
Column Reference
Namespace grouping multiple related entries. Part of the unique index.
Key within the bucket. Part of the unique index.
payload
MeasureData (jsonb)
required
Cached data blob. MeasureData is the type exported by backtest-kit.
Soft-delete tombstone flag. Default false. Set to true by softRemove. readMeasureData returns null when this is true.
Interval Adapter
The Interval adapter tracks once-per-interval markers — timestamps or flags that record whether a strategy has already acted during the current candle period, day, or other time window. Unlike Measure, Interval carries a when column for look-ahead bias protection, and its upsert includes when in the conflict-update clause.
Context Key and Table
| Field | Type | Role |
|---|
bucket | string | Namespace grouping related interval keys |
entryKey | string | Key within the bucket — passed at read/write/remove time |
Table: interval-items — unique index interval_items_uq on (bucket, entryKey)
Registration
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;
}
}
});
Table Schema
const IntervalModel = new EntitySchema<IIntervalRow>({
name: "interval-items",
columns: {
id: { type: "uuid", primary: true, generated: "uuid" },
bucket: { type: String },
entryKey: { type: String },
payload: { type: "jsonb" },
removed: { type: "boolean", default: false },
when: { type: "bigint", transformer: epochTransformer },
createDate: { type: "timestamptz", createDate: true },
updatedDate: { type: "timestamptz", updateDate: true },
},
indices: [
{ name: "interval_items_uq", columns: ["bucket", "entryKey"], unique: true },
],
});
IntervalDbService.softRemove — Atomic Soft-Delete
public softRemove = async (bucket: string, entryKey: string): Promise<void> => {
const repo = await this.repo<IIntervalRow>();
const { raw } = await repo
.createQueryBuilder()
.update()
.set({
removed: true,
payload: () => `jsonb_set("payload", '{removed}', 'true')`,
})
.where({ bucket, entryKey })
.returning("*")
.execute();
const saved = raw[0] as IIntervalRow | undefined;
if (!saved) return;
await this.intervalCacheService.setIntervalId(saved);
};
The clearBucket helper (not part of the IPersistIntervalInstance contract) physically deletes all rows for a bucket and evicts their Redis cache entries — useful for test teardown or bucket resets.
Column Reference
Soft-delete tombstone flag. Default false.
Logical simulation timestamp at write time. Included in the orUpdate clause so every write refreshes the look-ahead bias timestamp alongside the payload.
Memory Adapter
The Memory adapter provides a per-signal key/value store scoped to (signalId, bucketName). Each entry within that scope is addressed by a memoryId. Unlike Measure and Interval — which yield plain string keys from their async generators — Memory’s generator yields { memoryId: string; data: MemoryData } objects so the caller receives both the key and the value in one pass. Memory also carries a when column for look-ahead bias protection.
Context Key and Table
| Field | Type | Role |
|---|
signalId | string | Signal scope — set in constructor |
bucketName | string | Sub-scope within the signal — set in constructor |
memoryId | string | Entry key — passed at read/write/remove time |
Table: memory-items — unique index memory_items_uq on (signalId, bucketName, memoryId)
Registration
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; } // no-op cleanup hook
});
Table Schema
const MemoryModel = new EntitySchema<IMemoryRow>({
name: "memory-items",
columns: {
id: { type: "uuid", primary: true, generated: "uuid" },
signalId: { type: String },
bucketName: { type: String },
memoryId: { type: String },
payload: { type: "jsonb" },
removed: { type: "boolean", default: false },
when: { type: "bigint", transformer: epochTransformer },
createDate: { type: "timestamptz", createDate: true },
updatedDate: { type: "timestamptz", updateDate: true },
},
indices: [
{
name: "memory_items_uq",
columns: ["signalId", "bucketName", "memoryId"],
unique: true,
},
],
});
hasMemoryData and Redis
hasMemoryData checks MemoryCacheService.hasMemoryEntryId first — a pure Redis EXISTS call — before falling back to a PostgreSQL findByFilter. This is the cheapest existence check available: it avoids fetching the full row when the caller only needs a boolean.
dispose() — No-Op
The dispose() lifecycle hook is a no-op (void 0). Memory entries survive for the lifetime of the process and are cleaned up only by explicit removeMemoryData calls from strategy code, not by garbage collection.
Comparison: Measure vs. Interval vs. Memory
- Constructor field:
bucket: string
- Entry key:
entryKey (string argument)
- Table:
measure-items
when column: ❌ exempt from look-ahead bias
- Async generator yields:
string (key only)
dispose(): n/a
- Constructor field:
bucket: string
- Entry key:
entryKey (string argument)
- Table:
interval-items
when column: ✅ included in upsert
- Async generator yields:
string (key only)
dispose(): n/a
- Constructor fields:
signalId: string, bucketName: string
- Entry key:
memoryId (string argument)
- Table:
memory-items
when column: ✅ included in upsert
- Async generator yields:
{ memoryId: string; data: MemoryData }
dispose(): no-op (void 0)
All three adapters treat a soft-deleted entry as absent: read*Data returns null when row.removed === true. A subsequent write*Data to the same key will upsert a new row (with removed: false) over the tombstone, effectively un-deleting it.