Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt

Use this file to discover all available pages before exploring further.

@backtest-kit/mongo is a production persistence layer for strategies that outgrow the default file-based ./dump/ storage. It implements all 15 IPersist*Instance contracts from backtest-kit using MongoDB as the source of truth and Redis as an O(1) lookup cache. A single setup() call wires everything up before the first persistence call — strategy code stays unchanged.

Install

npm install @backtest-kit/mongo backtest-kit mongoose ioredis

Setup

Call setup() from your config/setup.config.ts file. The CLI loads this file once before any module hooks or strategy code run, so the adapters are registered before backtest-kit makes its first persistence call:
// config/setup.config.ts
import { setup } from '@backtest-kit/mongo';

setup();
To pass connection parameters explicitly instead of relying on environment variables:
// config/setup.config.ts
import { setup } from '@backtest-kit/mongo';

setup({
  CC_MONGO_CONNECTION_STRING: 'mongodb://mongo:27017/mydb',
  CC_REDIS_HOST: 'redis',
  CC_REDIS_PORT: 6379,
  CC_REDIS_PASSWORD: 'secret',
});

Environment Variables

CC_MONGO_CONNECTION_STRING=mongodb://localhost:27017/backtest-kit
CC_REDIS_HOST=127.0.0.1
CC_REDIS_PORT=6379
VariableDefaultDescription
CC_MONGO_CONNECTION_STRINGmongodb://localhost:27017/backtest-kit?wtimeoutMS=15000MongoDB connection string
CC_REDIS_HOST127.0.0.1Redis host
CC_REDIS_PORT6379Redis port
CC_REDIS_USER(empty)Redis username
CC_REDIS_PASSWORD(empty)Redis password

Persistence Adapters

Each of the 15 adapters covers one persistence slot in backtest-kit. Every write uses findOneAndUpdate with upsert: true on the compound unique index for that domain:
AdapterMongoDB CollectionUnique Index
Candlecandle-itemssymbol + interval + timestamp
Signalsignal-itemssymbol + strategyName + exchangeName
Scheduleschedule-itemssymbol + strategyName + exchangeName
Riskrisk-itemsriskName + exchangeName
Partialpartial-itemssymbol + strategyName + exchangeName + signalId
Breakevenbreakeven-itemssymbol + strategyName + exchangeName + signalId
Storagestorage-itemsbacktest + signalId
Notificationnotification-itemsbacktest + notificationId
Loglog-itemsentryId
Measuremeasure-itemsbucket + entryKey
Intervalinterval-itemsbucket + entryKey
Memorymemory-itemssignalId + bucketName + memoryId
Recentrecent-itemssymbol + strategyName + exchangeName + frameName + backtest
Statestate-itemssignalId + bucketName
Sessionsession-itemsstrategyName + exchangeName + frameName
Candle records are immutable — the first write wins, subsequent writes to the same (symbol, interval, timestamp) are silently ignored via $setOnInsert. All other adapters use $set, replacing the previous value on each write.

O(1) Redis Caching

Every domain has two layers: a DbService that talks to MongoDB and a CacheService that talks to Redis. On each read, the cache service checks Redis for the MongoDB _id of the document. A cache hit means two O(1) operations total — one Redis GET and one MongoDB findById. A cache miss falls back to a regular indexed MongoDB query, then writes the _id to Redis so the next call is instant.
read signal for (BTCUSDT, my_strategy, binance)

  ├─ Redis GET  → hit  → Mongo findById(_id)   ← O(1) + O(1)

  └─ Redis GET  → miss → Mongo findOne(filter) → Redis SET → return
After every write the Redis entry is updated in the same call, so a write followed immediately by a read always hits the cache. During backtests, backtest-kit performs thousands of context-keyed reads per second — Redis eliminates the per-request B-tree traversal and makes repeated reads effectively free.

Atomic Writes

backtest-kit requires that once write*Data() returns, the very next read*Data() must see the new value. Every write is a single findOneAndUpdate round-trip:
const document = await SignalModel.findOneAndUpdate(
  { symbol, strategyName, exchangeName },
  { $set: { payload } },
  { upsert: true, new: true, setDefaultsOnInsert: true },
);
await signalCacheService.setSignalId(readTransform(document.toJSON()));
The filter matches the unique compound index, so MongoDB rejects any concurrent duplicate insert at the storage-engine level. The returned document is immediately written to Redis, making the next read O(1) with the fresh data.

Look-Ahead Bias Protection

Adapters that influence trading decisions — Risk, Partial, Breakeven, Recent, State, Session, Memory, and Interval — store a when: Number field alongside the payload. This is the simulation timestamp in milliseconds. backtest-kit uses it to verify that no read returns data written at a future simulation time, preserving temporal correctness across backtest runs. Measure is exempt from this requirement because it caches LLM and external API responses, where look-ahead bias is not meaningful.

Soft Delete

Measure, Interval, and Memory adapters support soft delete. Calling their respective remove*Data() functions sets removed: true on the document instead of physically deleting it. Listing operations filter on removed: false, so logically deleted records are excluded from queries while the full audit trail is preserved in MongoDB.
Use config/loader.config.ts to wait for the MongoDB connection before the backtest starts. setup() registers adapters synchronously but does not block until the connection is established. Gate the run on a verified connection to get a fast failure instead of a mid-run error:
// config/loader.config.ts
import mongoose from 'mongoose';

export default async () => {
  await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
  console.log('mongo connection verified, starting backtest');
};

Build docs developers (and LLMs) love