Skip to main content

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

id
uuid
required
Auto-generated UUID primary key.
symbol
string
required
Trading pair, e.g. "TRXUSDT". Part of the unique index.
strategyName
string
required
Strategy identifier. Part of the unique index.
exchangeName
string
required
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.
createDate
timestamptz
required
Set by TypeORM on first insert.
updatedDate
timestamptz
required
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:
1

Redis cache check

Look up the context key in the corresponding *CacheService. A hit returns the row’s UUID string.
2

PostgreSQL primary-key lookup

Fetch the row by UUID — an O(1) heap access.
3

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.

Build docs developers (and LLMs) love