Low-latency C++ quantitative market making infrastructure. Mirrors portfolio allocation predictions ("Pawn state") from Valkey into a lock-free in-memory store, providing the foundation for automated trading.
| # | Title | Status |
|---|---|---|
| PRD-001 | Pawn Trading Engine | Active |
| # | Title | PRD | Status |
|---|---|---|---|
| EPIC-001 | Pawn State Module | PRD-001 | Done |
| EPIC-001 | Concurrency Model (wait-free atomic pointer + ring buffer) | PRD-2026-03-16 | Accepted (revised) |
| EPIC-002 | Production Data Format Alignment (JSON strings, multi-timeframe) | PRD-2026-03-15 | Accepted |
2026-03-15 (updated 2026-03-16)
Replaced with a truly lock-free design:
```cpp
std::atomic
2026-03-15
Updated all code to match production format: - `GET` instead of `HGETALL` for state values - Minimal JSON parser (no library dependency) for the fixed 6-field format - Local map key: `timeframe:base:quote` (e.g., `1min:XAG:GBP`) - Config exposes `state_key()`, `pairs_key()`, `last_ts_key()` builders
Context: Building lock-free state store for Pawn State Module
Learning: `std::atomic
Pattern: Use `std::atomic
Context: Writing torn-read detection test for StateStore
Learning: When encoding a consistency invariant (e.g., `base + quote == 1000000`) across map entries in a concurrency test, the invariant must hold for EVERY entry in the map, not just the first one. Initially wrote `BTCUSD: {base * 0.001, quote}` where quote was `1000000 - base` — this broke the invariant for BTCUSD.
Pattern: If using a sum-invariant for torn read detection, compute both fields from the same source value for each entry independently.
Context: Built Pawn State Module against PRD assumptions, then discovered production Valkey uses JSON strings (not hashes), different field names, negative positions, and multi-timeframe keys.
Learning: Always ask for a sample of real production data (SCAN + GET/TYPE) before implementing the data layer. PRD assumptions about storage format were wrong on 4 counts: value type, field names, sign semantics, and key structure.
Pattern: Before implementing any Valkey/Redis data access, run `TYPE key`, `GET`/`HGETALL` on a real key, and `SCAN` to see actual key patterns. Build the parser against real sample data.
Context: Reviewing whether delisted symbols persist in the state map
Learning: If a symbol is removed from Valkey (brain stops publishing, delisted pair), a naive incremental update would retain the stale entry forever. Building a fresh map from SCAN results each cycle and atomically swapping it guarantees that only currently-published keys exist in the local state.
Pattern: For state mirrors, prefer full rebuild + atomic swap over incremental merge. It's naturally self-cleaning — stale keys disappear when they stop appearing in SCAN. The cost is O(N) per cycle, but for <1000 keys at 100ms intervals this is negligible.
Context: Building logger macros for Nova trading engine
Learning: If a macro parameter is named `level` and the macro body calls `_l.level()`, the compiler substitutes the parameter into the method call, producing `_l.::nova::LogLevel::Error()`. Use unique parameter names like `lvl_` in macros to avoid collisions with member function names.
Pattern: Always suffix macro parameter names with `_` (e.g., `lvl_`, `mod_`) to prevent collisions with any method or variable in the expansion context.
Context: Reviewing Valkey key scanning in Pawn State Module
Learning: A single `SCAN 0 MATCH pattern COUNT 100` only returns a partial result. Redis/Valkey SCAN is cursor-based — you must loop `do { SCAN cursor } while (cursor != 0)` to guarantee all keys are visited. COUNT is a hint, not a limit.
Pattern: Always implement SCAN as a do-while loop on the cursor. Never assume a single call returns all keys.
Context: Replacing shared_mutex-based state store with truly lock-free reads
Learning: `std::shared_mutex` with `shared_lock` is contention-minimal but NOT lock-free — a reader can block while waiting for a writer's exclusive lock. True lock-free (actually wait-free) reads require `std::atomic
Pattern: Use `atomic