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.

The 16 persist adapters in backtest-kit-redis-postgres-pgpool-docker replace the framework’s default file-based ./dump/ layer with a production-grade PostgreSQL + Redis backend. Each adapter implements the corresponding IPersist*Instance interface from backtest-kit and is registered once at startup via a static usePersist*Adapter(class ...) call. Strategy code, runners, and the CLI entrypoint are completely unchanged — only the persistence layer is swapped.

What Is a Persist Adapter?

A persist adapter is a class that satisfies a IPersist*Instance contract defined by backtest-kit. The framework instantiates a fresh adapter object for every unique context key (e.g., one per (symbol, strategyName, exchangeName) tuple) and calls waitForInit(initial) the first time it touches that instance. You register your implementation globally by passing it to the static factory method on the corresponding adapter class:
PersistSignalAdapter.usePersistSignalAdapter(class implements IPersistSignalInstance {
  constructor(
    readonly symbol: string,
    readonly strategyName: string,
    readonly exchangeName: string,
  ) {}

  async waitForInit(initial: boolean) {
    if (!initial) return;
    await waitForInfra();        // singleshot — gates first-touch on Postgres + Redis ready
  }

  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
    );
  }
});
This skeleton is the same across all 16 adapters. The only variation is the constructor fields (the context key), the data types, and whether the adapter carries a when: Date look-ahead-bias column.

Infrastructure Gate: waitForInfra()

All adapters share a single waitForInfra() promise, wrapped with singleshot from functools-kit. The very first adapter instance to be touched triggers a parallel Promise.all that waits for both PostgreSQL (via TypeORM synchronize) and Redis to be ready. Every subsequent waitForInit call resolves instantly because singleshot memoises the result.
const waitForInfra = singleshot(async () => {
  await Promise.all([
    ioc.postgresService.waitForInit(),
    ioc.redisService.waitForInit(),
  ]);
});
TypeORM runs with synchronize: true on first connect, so all 16 tables and their unique indexes are created automatically against an empty database. There is no manual migration step.

The 16 Adapters at a Glance

AdapterTableContext Key (= unique index)Purpose
Candlecandle-items(symbol, interval, timestamp)OHLCV cache; immutable inserts
Signalsignal-items(symbol, strategyName, exchangeName)Live signal state per context
Scheduleschedule-items(symbol, strategyName, exchangeName)Pending scheduled signal
Strategystrategy-items(symbol, strategyName, exchangeName)Persistent strategy state per context
Riskrisk-items(riskName, exchangeName)Active risk positions snapshot
Partialpartial-items(symbol, strategyName, exchangeName, signalId)Partial profit/loss levels per signal
Breakevenbreakeven-items(symbol, strategyName, exchangeName, signalId)Breakeven reached flag
Storagestorage-items(backtest, signalId)Closed/opened signal log per mode
Notificationnotification-items(backtest, notificationId)Event notifications
Loglog-items(entryId)Strategy log entries
Measuremeasure-items(bucket, entryKey)LLM/API response cache (soft-delete)
Intervalinterval-items(bucket, entryKey)Once-per-interval markers (soft-delete)
Memorymemory-items(signalId, bucketName, memoryId)Per-signal memory store (soft-delete)
Recentrecent-items(symbol, strategyName, exchangeName, frameName, backtest)Last public signal per context
Statestate-items(signalId, bucketName)Per-signal state buckets
Sessionsession-items(strategyName, exchangeName, frameName, symbol, backtest)One session per running strategy

Atomicity Model

Every write goes through a single INSERT … ON CONFLICT … DO UPDATE … RETURNING * statement — no separate read then write. The conflict target is always the unique index whose columns exactly match the context key fields. PostgreSQL serialises concurrent inserts on that key at the storage-engine level; the loser of a race takes the DO UPDATE branch instead of throwing, so no unique-constraint violation ever surfaces to the application. The RETURNING * clause yields the just-written row in the same statement, and that row is fed directly to the Redis cache — never from a follow-up SELECT that could hit a lagging read replica.
The only exception to the full-update rule is the Candle adapter, which uses a no-op DO UPDATE SET symbol = EXCLUDED.symbol. OHLCV columns are never overwritten once inserted — historical market data is immutable.
Soft-delete adapters (Measure, Interval, Memory) use a parallel atomic pattern — a single server-side UPDATE with jsonb_set rather than a read-modify-write cycle:
await repo
  .createQueryBuilder()
  .update()
  .set({
    removed: true,
    payload: () => `jsonb_set("payload", '{removed}', 'true')`,
  })
  .where({ bucket, entryKey })
  .returning("*")
  .execute();

Redis O(1) ID Cache

Each domain has a companion *CacheService that stores only the row’s UUID in Redis, keyed by a namespaced context string. On read, the adapter checks Redis first; on a cache hit it fires a primary-key lookup on PostgreSQL (both O(1)); on a miss it falls back to a full filter query and backfills Redis. After every upsert, the cache is updated synchronously from the RETURNING row — never from a subsequent SELECT.

Look-Ahead Bias Protection

Adapters that affect signal logic carry a when column (bigint, epoch milliseconds) representing the logical simulation timestamp. The epochTransformer value transformer keeps the JS-visible value a plain number since the pg driver returns bigint as a string. The affected adapters are: Risk, Partial, Breakeven, Recent, State, Session, Memory, and Interval. The Measure adapter is intentionally exempt — it caches LLM responses where look-ahead bias does not apply.

Adapter Detail Pages

Candle

Immutable OHLCV cache with no-op conflict update

Signal, Schedule, Strategy

Three adapters sharing the (symbol, strategyName, exchangeName) context key

Risk, Partial, Breakeven

Three look-ahead-bias–protected adapters carrying the when column

Storage, Notification, Log

Audit-trail adapters for signal logs, events, and strategy output

Measure, Interval, Memory

Soft-delete adapters using server-side jsonb_set

Recent, State, Session

Strategy lifecycle adapters with look-ahead bias protection

Build docs developers (and LLMs) love