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.
Signal, Schedule, and Strategy are three adapters that share an identical context key shape — (symbol, strategyName, exchangeName) — and an identical table layout. Each stores a single payload column of type jsonb (nullable) alongside the three key fields, plus createDate and updatedDate audit timestamps. They differ only in the type of data held in payload and the semantic role that data plays within backtest-kit’s execution model.
Shared Unique Index Pattern
All three tables have a (symbol, strategyName, exchangeName) unique index. Every write uses an atomic INSERT … ON CONFLICT … DO UPDATE SET payload = EXCLUDED.payload RETURNING * statement — no separate read then write. The conflict target is always the unique index, so concurrent writers serialise at the PostgreSQL storage layer and never produce a constraint violation in application code.
Signal Adapter
The Signal adapter records the live signal state for a (symbol, strategyName, exchangeName) context. Its payload is a nullable ISignalRow from backtest-kit, which captures the current open/closed state of the active trade signal.
Registration
PersistSignalAdapter.usePersistSignalAdapter(class implements IPersistSignalInstance {
constructor(
readonly symbol: string,
readonly strategyName: string,
readonly exchangeName: string,
) {}
async waitForInit(initial: boolean) {
if (!initial) return;
await waitForInfra();
}
async readSignalData(): Promise<ISignalRow | null> {
const row = await ioc.signalDbService.findByContext(
this.symbol, this.strategyName, this.exchangeName
);
return row ? row.payload : null;
}
async writeSignalData(signalRow: ISignalRow | null): Promise<void> {
await ioc.signalDbService.upsert(
this.symbol, this.strategyName, this.exchangeName, signalRow
);
}
});
Table: signal-items
const SignalModel = new EntitySchema<ISignalRowDoc>({
name: "signal-items",
columns: {
id: { type: "uuid", primary: true, generated: "uuid" },
symbol: { type: String },
strategyName: { type: String },
exchangeName: { type: String },
payload: { type: "jsonb", nullable: true },
createDate: { type: "timestamptz", createDate: true },
updatedDate: { type: "timestamptz", updateDate: true },
},
indices: [
{ name: "signal_items_uq", columns: ["symbol", "strategyName", "exchangeName"], unique: true },
],
});
SignalDbService.upsert — Atomic Write
public upsert = async (
symbol: string,
strategyName: string,
exchangeName: string,
payload: ISignalRow | null,
): Promise<void> => {
const repo = await this.repo<ISignalRowDoc>();
const { raw } = await repo
.createQueryBuilder()
.insert()
.values({ symbol, strategyName, exchangeName, payload })
.orUpdate(["payload"], ["symbol", "strategyName", "exchangeName"])
.returning("*")
.execute();
const result = raw[0] as ISignalRowDoc;
await this.signalCacheService.setSignalId(result); // seed Redis from RETURNING row
};
Column Reference
Auto-generated UUID primary key.
Trading pair, e.g. "TRXUSDT". Part of the unique index.
Strategy identifier. Part of the unique index.
Exchange identifier. Part of the unique index.
payload
ISignalRow | null
required
Nullable JSONB blob holding the full signal row from backtest-kit. null when no active signal exists for this context.
Set by TypeORM on first insert.
Updated by TypeORM on every write.
Schedule Adapter
The Schedule adapter stores a pending scheduled signal for a given context. Its payload is a nullable IScheduledSignalRow, which represents a signal that has been queued for future execution but has not yet fired.
Registration
PersistScheduleAdapter.usePersistScheduleAdapter(class implements IPersistScheduleInstance {
constructor(
readonly symbol: string,
readonly strategyName: string,
readonly exchangeName: string,
) {}
async waitForInit(initial: boolean) {
if (!initial) return;
await waitForInfra();
}
async readScheduleData(): Promise<IScheduledSignalRow | null> {
const row = await ioc.scheduleDbService.findByContext(
this.symbol, this.strategyName, this.exchangeName
);
return row ? row.payload : null;
}
async writeScheduleData(scheduleRow: IScheduledSignalRow | null): Promise<void> {
await ioc.scheduleDbService.upsert(
this.symbol, this.strategyName, this.exchangeName, scheduleRow
);
}
});
Table: schedule-items
const ScheduleModel = new EntitySchema<IScheduleRow>({
name: "schedule-items",
columns: {
id: { type: "uuid", primary: true, generated: "uuid" },
symbol: { type: String },
strategyName: { type: String },
exchangeName: { type: String },
payload: { type: "jsonb", nullable: true },
createDate: { type: "timestamptz", createDate: true },
updatedDate: { type: "timestamptz", updateDate: true },
},
indices: [
{ name: "schedule_items_uq", columns: ["symbol", "strategyName", "exchangeName"], unique: true },
],
});
The payload column holds an IScheduledSignalRow — the scheduled-signal type from backtest-kit — or null when no pending schedule exists for the context. The table layout is otherwise identical to signal-items.
Strategy Adapter
The Strategy adapter persists arbitrary strategy state across runs. Its payload is a nullable StrategyData object (an opaque Record<string, unknown> defined by backtest-kit) that strategies use to survive restarts without losing internal counters, flags, or computed fields.
Registration
PersistStrategyAdapter.usePersistStrategyAdapter(class implements IPersistStrategyInstance {
constructor(
readonly symbol: string,
readonly strategyName: string,
readonly exchangeName: string,
) {}
async waitForInit(initial: boolean) {
if (!initial) return;
await waitForInfra();
}
async readStrategyData(): Promise<StrategyData | null> {
const row = await ioc.strategyDbService.findByContext(
this.symbol, this.strategyName, this.exchangeName
);
return row ? row.payload : null;
}
async writeStrategyData(strategyRow: StrategyData | null): Promise<void> {
await ioc.strategyDbService.upsert(
this.symbol, this.strategyName, this.exchangeName, strategyRow
);
}
});
Table: strategy-items
const StrategyModel = new EntitySchema<IStrategyRow>({
name: "strategy-items",
columns: {
id: { type: "uuid", primary: true, generated: "uuid" },
symbol: { type: String },
strategyName: { type: String },
exchangeName: { type: String },
payload: { type: "jsonb", nullable: true },
createDate: { type: "timestamptz", createDate: true },
updatedDate: { type: "timestamptz", updateDate: true },
},
indices: [
{ name: "strategy_items_uq", columns: ["symbol", "strategyName", "exchangeName"], unique: true },
],
});
Strategy data has no when column and no look-ahead bias protection. It is intended for durable cross-run state — configuration, computed constants, warmup data — rather than time-series values that must be replayed in strict temporal order.
Common Read Path (all three)
All three DbServices (SignalDbService, ScheduleDbService, StrategyDbService) implement the same two-level read path via findByContext:
Redis cache check
Look up the context key in the corresponding *CacheService. A hit returns the row’s UUID string.
PostgreSQL primary-key lookup
Fetch the row by UUID — an O(1) heap access.
Fallback to filter query
On a cache miss, query PostgreSQL by the full (symbol, strategyName, exchangeName) filter on the unique B-tree index, then backfill Redis.
The Redis cache stores only the UUID, never the document itself. After an upsert the cache is seeded from the RETURNING row in the same critical section — so a read that immediately follows a write always hits the cache and always returns the just-written value, even on a cluster with async replicas.