backtest-kit ships with a set of persistence interfaces —Documentation Index
Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit-redis-mongo-docker/llms.txt
Use this file to discover all available pages before exploring further.
IPersistSignalInstance, IPersistMeasureInstance, and others — that determine how run-time state is stored and retrieved. Out of the box, the framework uses its own file-based implementation. This project replaces every adapter with MongoDB as the source of truth and Redis as an O(1) ID cache. When you need to persist a new kind of data (custom metrics, position metadata, audit trails), you add a new adapter that follows the same four-file pattern used for signals, measures, and the rest.
When to Add a New Adapter
New data shape
You have a domain object (e.g. a risk event, a fill record) that backtest-kit doesn’t model natively and you want it stored durably in Mongo.
Custom cache key
You need O(1) lookup by a compound key (symbol + strategy + exchange) and don’t want to pay a Mongo round-trip on every tick.
Extending an existing interface
The framework added a new
IPersist* interface in a minor version and you want to wire it up to the shared infrastructure.Per-mode overrides
Backtest, live, and paper modes need different retention policies for the same data type.
The Four-File Pattern
Every adapter in this project is built from four cooperating files. The example below walks through adding a hypotheticalWidget adapter.
Mongoose schema
Define the Mongo document shape in a model file. The exact schema depends on your data, but every document should include the context triple (
symbol, strategyName, exchangeName) so findByContext queries are efficient. A removed boolean flag enables the soft-delete pattern used by Measure, Interval, and Memory adapters when listing historical data:The
removed: true soft-delete flag is what allows listWidgetData operations to filter out logically deleted records without a hard deleteOne. Measure, Interval, and Memory adapters all rely on this pattern.DbService extending BaseCRUD
Create a service class that extends
BaseCRUD(WidgetModel). Implement upsert (write or overwrite by context) and findByContext (read by context triple). The base class provides the Mongo connection lifecycle; you add the domain-specific query logic:CacheService extending BaseMap
Create a Redis-backed cache service by extending
BaseMap(REDIS_KEY, -1). The second argument is the TTL in seconds; -1 means no expiry, which is correct for ID caches that must survive process restarts. Expose typed setXxxId, getXxxId, and hasXxxId helpers so callers never manipulate raw Redis keys:Registration in setup.ts
Implement the The real Signal adapter in
IPersistWidgetInstance interface and register the class with PersistWidgetAdapter.usePersistWidgetAdapter(). The waitForInit guard ensures that Mongo and Redis are both ready before any reads or writes occur. The initial flag is true only for the very first call, so the infrastructure wait runs exactly once per process:src/config/setup.ts follows this exact structure:Wiring DI Tokens
After creating the service classes, expose them through the project’s inversion-of-control container in three places.1. Add tokens to src/lib/core/types.ts
DI tokens are Symbol values that serve as unique keys in the container. Add one for the new DbService and one for the CacheService:
2. Add provide() calls in src/lib/core/provide.ts
Register both services using provide(token, factory) so the container constructs them once and shares the instance:
3. Add inject() calls in src/lib/index.ts
Expose the services as typed properties on the ioc object so the rest of the codebase can access them without importing from types.ts directly. inject() is lazy — it resolves the instance at access time, not at construction time:
The waitForInfra() Gate
The singleshot wrapper from functools-kit turns the infrastructure wait into a one-shot promise. The first call to waitForInfra() actually awaits Mongo and Redis connectivity; every subsequent call returns the already-resolved promise immediately:
waitForInit(initial: boolean), the initial flag short-circuits the gate for every adapter instance that isn’t the first constructed. This means the infrastructure handshake happens exactly once per process regardless of how many symbols the runner processes in parallel:
Soft-Delete Pattern
Measure, Interval, and Memory adapters use aremoved: true field for logical deletion rather than hard deleteOne calls. When implementing listXxxData for any adapter that needs to return a collection of historical records, always filter out soft-deleted documents:
removed flag is not strictly necessary, but including it keeps the schema consistent with the rest of the project and makes future migrations easier.