ðŸ’ū @backtest-kit/mongo

MongoDB + Redis persistence adapter for backtest-kit. Swaps the default file-based storage for a production-grade backend — one setup() call, no changes to strategy code.

screenshot

Ask DeepWiki npm TypeScript

📚 Backtest Kit Docs | 🌟 GitHub

New to backtest-kit? The fastest way to get a real, production-ready setup is to clone the reference implementation — a fully working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there instead of from scratch.

npm install @backtest-kit/mongo backtest-kit
import { setup } from '@backtest-kit/mongo';

// Reads connection settings from environment variables.
// Call once before any trading operations.
setup();
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',
});
Export Description
setup(config?) Configure and register all 15 adapters in one call. Reads from env vars when config is omitted.
install() Register adapters only — use when configuration was already applied via setConfig or env vars.
setConfig(config) Override individual connection parameters at runtime.
getConfig() Returns the current merged configuration (env vars + any setConfig overrides).
setLogger(logger) Replace the internal logger with your own implementation.
getMongo() Returns the connected Mongoose instance (lazy singleton).
getRedis() Returns the connected ioredis instance (lazy singleton).
Variable Default Description
CC_MONGO_CONNECTION_STRING mongodb://localhost:27017/backtest-kit?wtimeoutMS=15000 MongoDB connection string
CC_REDIS_HOST 127.0.0.1 Redis host
CC_REDIS_PORT 6379 Redis port
CC_REDIS_USER (empty) Redis username
CC_REDIS_PASSWORD (empty) Redis password

Values passed to setup() or setConfig() always take precedence over environment variables.

Each adapter covers one persistence slot in backtest-kit. The table shows what it stores and which fields form its unique index in MongoDB:

Adapter MongoDB collection Unique index
Candle candle-items symbol + interval + timestamp
Signal signal-items symbol + strategyName + exchangeName
Schedule schedule-items symbol + strategyName + exchangeName
Risk risk-items riskName + exchangeName
Partial partial-items symbol + strategyName + exchangeName + signalId
Breakeven breakeven-items symbol + strategyName + exchangeName + signalId
Storage storage-items backtest + signalId
Notification notification-items backtest + notificationId
Log log-items entryId
Measure measure-items bucket + entryKey
Interval interval-items bucket + entryKey
Memory memory-items signalId + bucketName + memoryId
Recent recent-items symbol + strategyName + exchangeName + frameName + backtest
State state-items signalId + bucketName
Session session-items strategyName + 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, so each write replaces the previous value.

Measure, Interval, and Memory support soft delete — calling removeMeasureData / removeIntervalData / removeMemoryData sets removed: true on the document instead of deleting it. Listing operations filter on removed: false.

  • 🗄ïļ MongoDB backend: all 15 IPersist*Instance contracts from backtest-kit implemented with Mongoose
  • ⚡ O(1) reads via Redis: every context-key lookup goes through ioredis — one GET + one findById, no B-tree scans
  • 🔒 Atomic writes: findOneAndUpdate with upsert: true guarantees read-after-write correctness with no race conditions
  • ðŸ›Ąïļ Look-ahead bias protection: adapters that affect signal logic store the simulation timestamp so backtest-kit can enforce temporal correctness
  • ðŸŠĶ Soft delete: Measure, Interval, and Memory records are never physically removed — they carry a removed flag instead
  • 🔌 Zero strategy changes: drop setup() into your entry point, everything else stays the same

Every domain has two layers: a DbService that talks to MongoDB and a CacheService that talks to Redis.

When the strategy reads state for a given context key (e.g. symbol + strategyName + exchangeName for a signal), the DbService first asks Redis for the MongoDB _id. If it exists, the document is fetched directly by _id — two O(1) operations total. On a cache miss it 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.

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 to MongoDB:

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.

Adapters whose data influences trading decisions (Risk, Partial, Breakeven, Recent, State, Session, Memory, Interval) store when: Number — the simulation timestamp in milliseconds — alongside the payload. This lets backtest-kit verify that no read returns data that was written at a future simulation time.

Measure is exempt because it caches LLM and external API responses, where look-ahead bias is not meaningful.

Fork/PR on GitHub.

MIT ÂĐ tripolskypetr