backtest-kit enforces a write durability contract: afterDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit-minio-s3-docker/llms.txt
Use this file to discover all available pages before exploring further.
writeXData(...) returns, the very next readXData(...) call from any code path must see the value that was just written. In a traditional database this contract is upheld by transactions or write-ahead logs. This project meets the same guarantee using plain S3 object semantics — no transactions, no two-phase commits, no sequence numbers.
Amazon S3 (and MinIO) provides strong read-after-write consistency for new objects as of late 2020. A
PUT that completes successfully is immediately visible to any subsequent GET or HEAD for that exact key. This project relies on this guarantee as its foundation.Four Durability Principles
Deterministic keys — upserts are single idempotent PUTs
Every record’s object key is a pure function of its context fields. There is no UUID generation at write time, no sequence counter, and no read-before-write step to discover an existing key.
Because the key is deterministic, an upsert is a single
| Adapter | Key function |
|---|---|
| Candle | ccxt_binance/{symbol}/{interval}/{timestamp} |
| Signal | {symbol}/{strategy}/{exchange} |
| Risk | {riskName}/{exchange} |
| Partial | {symbol}/{strategy}/{exchange}/{signalId} |
| Session | {strategy}/{exchange}/{frame}/{symbol}/{backtest} |
PUT. Two concurrent writers for the same context produce the same key and the same serialised value, so the last-write-wins S3 semantic is also correct-by-construction — there is no “wrong” winner.Immutable entities — candles use stat+PUT, logs skip already-persisted keys
Two categories of entities are never rewritten after their first creation:Candles are market data: the OHLCV values for Log and Notification entries are also immutable once written. The in-process index is important because backtest-kit re-sends the full accumulated entry list on every
(exchange, symbol, interval, timestamp) never change. CandleDataService.create issues a statObject (HEAD) before writing. If the object exists, the method returns immediately without downloading the body:CandleDataService.ts
LogDataService.upsert checks an in-process Set<string> of already-persisted keys first (FIFO-capped at 10 000 entries), then falls back to a statObject for keys not yet seen in this process. If either check confirms the object exists, the PUT and Redis register are both skipped entirely:LogDataService.ts
writeLogData call. Without the in-process index, a list of 200 entries would cost 200 statObject calls on every tick; with it, the cost is zero for all entries that have been seen in the current process lifetime.Write order: MinIO first, Redis second
For entities that use the Redis time index (Log, Notification, Storage), the write sequence is always:
this.set(key, row)→ MinIOPUTthis.logConnectionService.register(key)→ RedisSADD+SETNX
listNewest until the Redis index is rebuilt (either by a later register call or by the cold-index fallback path).The reverse — writing Redis first — would create a phantom entry: listNewest would return the key, get(key) would return null, and the caller would silently drop a log entry it believed was persisted.`removed` means absent — physical deletes keep listKeys clean
Soft-delete entities (Measure, Interval, Memory) implement removal by physically deleting the MinIO object rather than writing a tombstone:
src/config/setup.ts
softRemove calls BaseStorage.delete(key), which issues removeObject to MinIO. After this call:readMeasureData(key)callsget(key), which returnsnullforNoSuchKey— no special null-check logic is needed.listMeasureData()callsBaseStorage.keys(prefix)— a purelistObjectsV2stream. Deleted objects are never returned. The listing is O(surviving objects), not O(total historical objects).
{ removed: true }) where every listing would need to download and filter objects, and the bucket would grow without bound over time.Crash Safety Summary
| Crash point | State in MinIO | State in Redis | Effect on reads |
|---|---|---|---|
Before set(key, row) | Object absent | Not registered | readXData returns previous value (or null) — correct |
After set, before register | Object present | Not registered | Point read by key returns the value ✓; does not appear in listNewest until cold fallback or next register |
After both set and register | Object present | Registered | Fully visible to all read paths ✓ |
After delete (soft-remove) | Object absent | N/A | readXData returns null ✓; absent from listKeys ✓ |
Adapter waitForInit Gate
Every adapter delays its first operation until the infrastructure is confirmed ready:
src/config/setup.ts
RedisService.waitForInit waits up to 15 seconds for the Redis ready event. The MinIO client is created lazily by MinioService.getClient on first bucket access, so there is no up-front TCP handshake to gate on. If Redis does not become ready within the timeout, the error propagates and the process exits — the strategy never starts writing to a partially initialised store.