Some entities need newest-first listings that S3 lexicographic ordering alone cannot provide efficiently. Log, Notification, and Storage solve this by maintaining a Redis minute-bucket index alongside their MinIO objects. Each write registers the new object key in a RedisDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-minio-s3-docker/llms.txt
Use this file to discover all available pages before exploring further.
SET partitioned by aligned minute; reads walk the index backwards from the current minute, fetching only as many keys as needed — independent of how many objects the bucket holds in total. The remaining three adapters — Measure, Interval, and Memory — are utility caches that use physical deletion instead of tombstones, keeping listKeys a pure prefix LIST with zero body reads.
Log
The Log adapter persists strategy log entries emitted bybacktest-kit. There is no per-context constructor argument — all log entries share a single global namespace within the log-items/ prefix.
Constructor
LogDataService.listAll() queries the Redis time index first via logConnectionService.listNewest(200), then issues one point GET per returned key. If the Redis index is empty (cold start after a flush), the method falls back to a lexicographic bucket listing — which is already newest-first thanks to the inverted timestamp key — and warms the index back up:
logDataService.upsert(entry.id, entry) for each one. upsert uses an in-process _persistedKeys set to skip entries that have already been written this process lifetime — no network round-trip needed. For entries not in the set, a has() stat checks MinIO existence before writing. The write order is MinIO first, Redis second: a crash between the two leaves the object readable by direct key lookup but invisible to listAll — never a phantom index entry pointing at nothing.
Object key — ⟲ts_entryId
The inverted timestamp is zero-padded to a fixed width so that lexicographic order equals reverse-chronological order:
LogDataService maintains an in-process _persistedKeys Set capped at
10,000 entries (FIFO eviction). backtest-kit re-sends the entire
accumulated log list on every writeLogData call. Without this set, each
call would cost one has() stat per entry against MinIO. The in-process
index reduces steady-state write cost to zero network round-trips for
entries already written in this process lifetime.Notification
The Notification adapter persists event notifications — trade executions, alerts, system messages — with the same Redis-backed newest-first read pattern as Log. Thebacktest flag partitions live and backtest notifications into separate key prefixes.
Constructor
notificationDataService.listByMode(this.backtest), which queries the Redis index with listNewest(200, "${backtest}/"). The service sorts the returned rows by createDate descending and slices to 200. The adapter then calls .reverse() on the result before returning — backtest-kit expects the array in oldest-first order for replay:
createdAt field. Like Log, it maintains an in-process _persistedKeys set (capped at 10,000) to skip already-written entries without a MinIO stat.
Object key — <backtest>/⟲ts_notificationId
Storage
The Storage adapter persists the closed and opened signal log — a record of every signal that has passed through the strategy, partitioned by mode (backtest: true or false). Unlike Log and Notification, signal objects are mutable: a signal is rewritten on every state transition under its stable key. The Redis index tracks which keys exist, not their current content.
Constructor
storageDataService.listByMode(this.backtest), which queries the Redis index with listNewest(Infinity, "${backtest}/") to retrieve all signal keys for the current mode, then fetches each object body. Cold-index fallback walks the bucket prefix "${backtest}/" directly and warms Redis:
storageDataService.upsert(this.backtest, signal.id, signal) for each. Every upsert is a full overwrite of the MinIO object under the stable key, followed by a one-time Redis registration per key (guarded by an in-process _registeredKeys set capped at 10,000).
Object key — <backtest>/<signalId>
Utility Adapters: Measure, Interval, Memory
These three adapters implement soft-delete semantics by physically deleting the MinIO object when an entry is removed. There are no tombstone markers to skip during listing —listKeys is a pure prefix LIST that returns only live entries, with zero body reads.
Measure
The Measure adapter is a keyed cache for LLM and external API responses. It is scoped to abucket string that groups related entries.
Constructor
measure-items/<bucket>/<entryKey>
listMeasureData() is an async generator that yields each entryKey string (with the bucket/ prefix stripped) by issuing a single keys(prefix) LIST against MinIO:
removeMeasureData physically deletes the object via softRemove, which calls this.delete(key) on the underlying BaseStorage. The removed flag on MeasureData is also checked in upsert: if payload.removed === true, the upsert path itself calls delete instead of set.
Interval
The Interval adapter stores once-per-interval markers — flags that record whether a periodic action has already been taken within a given interval window. Like Measure, it is scoped to abucket.
Constructor
interval-items/<bucket>/<entryKey>
listIntervalData() generator.
Memory
The Memory adapter provides a per-signal memory store used for BM25 index rebuilds and other signal-scoped caches. It is doubly scoped bysignalId and bucketName, and each entry is additionally identified by a memoryId.
Constructor
memory-items/<signalId>/<bucket>/<memoryId>
listMemoryData() yields { memoryId, data } pairs. Unlike Measure and Interval (which only need keys), Memory fetches full object bodies because callers need the content for index reconstruction:
dispose() is a no-op (void 0) — per-signal memory cleanup is managed by backtest-kit at the framework level, not by the adapter.