Triad is a research-grade, event-driven algorithmic trading engine written in Rust and Python. It ingests live Level-2 order book data from Binance, computes real-time microstructure signals, and uses a combination of a neural network (ONNX) and episodic memory (HNSW k-NN) to make sub-millisecond paper trading decisions.
- Architecture Overview
- System Components
- Data Flow
- Signal Glossary
- Database Schema
- Prerequisites
- Infrastructure Setup
- Model Generation
- Running the System
- Project Structure
- Configuration Reference
- Testing
- Roadmap
Triad is structured as a three-layer pipeline:
┌────────────────────────────────────────────────────────────────────┐
│ FAST PATH (Rust) │
│ │
│ Binance WebSocket │
│ │ (trade + depth@100ms) │
│ ▼ │
│ ┌────────────────┐ NATS publish ┌──────────────────────┐ │
│ │ Witness Node │ ─────────────────► │ Execution Engine │ │
│ │ (monitoring/) │ ObservationEvent │ (fast-path/) │ │
│ │ │ │ │ │
│ │ • LocalOrderBook│ │ • ModelLoader (hot) │ │
│ │ • VpinTracker │ │ • MemoryBank (HNSW) │ │
│ │ • Commitment │ │ • OnnxBrain │ │
│ │ Hash Chain │ │ • PaperPortfolio │ │
│ └────────────────┘ └──────────┬───────────┘ │
│ │ ClosedEpisode │
│ │ NATS publish │
└───────────────────────────────────────────────────┼───────────────┘
│
┌───────────────────────────────────────────────────▼───────────────┐
│ SLOW PATH (Python) │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────────┐ │
│ │ episode_store.py │ ───► │ Postgres (pgvector) │ │
│ │ (experiencing/) │ │ • episodes table (128-dim │ │
│ │ │ │ context vectors) │ │
│ │ • JetStream sub │ │ • IVFFlat ANN index │ │
│ │ • pgvector insert │ └──────────────────────────────┘ │
│ └─────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
▲ HNSW index rebuilt every 60s from Postgres
└── MemoryBank background task (in fast-path)
The Witness Node is the data ingestion layer. It connects to the Binance Combined WebSocket Stream and maintains a live, incrementally-updated Level-2 order book.
Key files:
| File | Responsibility |
|---|---|
src/main.rs |
WebSocket connection, reconnection loop, NATS publisher |
src/orderbook.rs |
[LocalOrderBook]: diff-based order book with spoofing detection; [VpinTracker]: rolling VPIN metric |
src/events.rs |
Canonical event types (ObservationEvent, ClosedEpisode) and SHA-256 commitment hash chain |
src/lib.rs |
Re-exports events and orderbook for use by fast-path |
Produces: ObservationEvent messages on NATS subject observations.microstructure.BTC-USD
The Execution Engine subscribes to the observation stream and makes real-time paper trading decisions using a 3-stage decision pipeline.
Key files:
| File | Responsibility |
|---|---|
src/main.rs |
Main event loop: hot-reload check → exit logic → entry logic |
src/model_loader.rs |
Watches a binary model file for changes and hot-reloads weights without restart |
src/classifier.rs |
Numerically-stable softmax classifier for regime detection |
src/memory.rs |
HNSW-based episodic memory bank with background Postgres refresh |
src/onnx_engine.rs |
ONNX Runtime wrapper for the neural net decision model |
Consumes: ObservationEvent from NATS
Produces: ClosedEpisode on NATS subject episodes.closed.BTC-USD
ObservationEvent
│
├─ [1] Hot-reload check (every 5s) ──► ModelLoader
│
├─ [2] Active trade? ──► YES: track PnL, check Take Profit / Stop Loss
│ NO: ──► [3]
│
└─ [3] Entry evaluation
├─ KNN win-rate lookup ──► MemoryBank (HNSW, <1ms)
└─ ONNX inference ──► OnnxBrain (RandomForest, <1ms)
└─ direction ∈ {long, short, flat}
└─ open position if not flat
| Rule | Threshold |
|---|---|
| Take Profit | +$20 unrealized PnL |
| Stop Loss | -$10 unrealized PnL |
| Position size | 1.0 BTC fixed |
| Starting capital | $100,000 USD |
The Episode Archiver (episode_store.py) consumes closed trade episodes from NATS JetStream and persists them to Postgres with a 128-dimensional pgvector context embedding.
This store is the long-term memory of the system — future episodes will be retrieved from it via the MemoryBank's HNSW index for KNN win-rate estimation.
| Script | Purpose |
|---|---|
export_fast_brain.py |
Exports the 116-byte regime classifier binary (regime_fast_v1.bin) |
export_onnx_brain.py |
Trains a Random Forest and exports it as decision_brain.onnx |
nats_consumer.py |
Debug utility: prints a one-liner summary every 100 observation events |
| Resource | Description |
|---|---|
docker-compose.yml |
NATS 2.10 (JetStream), Postgres 16 + pgvector, TimescaleDB |
postgres/init.sql |
Database schema: episodes, chain_heads, signal_scores, model_metadata, sandbox schema |
models/ |
Model binaries: regime_fast_v1.bin, onnx/decision_brain.onnx |
Every order-book depth update emits one ObservationEvent JSON message:
{
"id": "uuid-v4",
"timestamp_ns": 1710000000000000000,
"asset_id": "BTC-USD",
"signal_class": "microstructure",
"regime_hint": "unknown",
"confidence": 0.5,
"signal_vector": [0.45, 0.72, 0.0, 0.31, 0.0, ...],
"payload": { "best_bid": 68000.0, "best_ask": 68001.0 },
"system_health": { "latency_p99_ms": 1.0, "slippage_bps": 0.0, "api_status": "ok", "queue_consumer_lag": 0 },
"feature_version": "v1.0",
"prev_hash": [0, 0, ...],
"commitment_hash": [34, 17, ...]
}Active signal_vector indices:
| Index | Signal | Description |
|---|---|---|
| 0 | spread |
Absolute bid-ask spread in USD |
| 1 | imbalance |
Top-5 level volume imbalance [0, 1] |
| 2 | spoofing |
Volume cancelled at top-of-book this tick |
| 3 | VPIN |
Rolling Volume-Synchronized PIN estimate [0, 1] |
When a trade is closed, a ClosedEpisode is published and consumed by episode_store.py for Postgres archival.
| Signal | Range | High value means |
|---|---|---|
| Spread | 0 → ∞ (USD) | Low liquidity, wider market |
| Imbalance | 0 → 1 | Near 1.0: buy pressure; near 0.0: sell pressure |
| Spoofing Score | 0 → ∞ | Large orders placed and rapidly cancelled near top-of-book |
| VPIN | 0 → 1 | High informed order flow; elevated adverse selection risk |
| KNN Win-rate | 0 → 1 | Historical win fraction in similar market contexts |
Managed by infra/postgres/init.sql, initialized automatically by Docker on first start.
| Table | Purpose |
|---|---|
episodes |
One row per closed trade. Stores entry/exit prices, PnL, MAE, regime, and the 128-dim pgvector context embedding. |
chain_heads |
Records periodic snapshots of the SHA-256 hash chain head for auditability. |
signal_scores |
Rolling accuracy metrics per signal class × feature × regime (signal marketplace). |
model_metadata |
Tracks model lifecycle: SHADOW → LIVE → RETIRED with accuracy and drawdown stats. |
sandbox.episodes |
Physically isolated copy of episodes for black-swan stress testing (all rows enforced synthetic = TRUE). |
The context_vector column uses an IVFFlat (Inverted File + Flat) index from pgvector for approximate nearest-neighbour retrieval on the 128-dimensional embeddings:
CREATE INDEX ON episodes
USING ivfflat (context_vector vector_l2_ops)
WITH (lists = 100)
WHERE synthetic = FALSE;- OS: Ubuntu 20.04+ (or WSL2 on Windows)
- Rust:
1.75+(rustup toolchain install stable) - Python:
3.10+ - Docker & Docker Compose: for NATS and Postgres
pip install asyncpg nats-py numpy scikit-learn skl2onnxThe ort crate downloads ONNX Runtime automatically via Cargo. No manual installation is needed.
Start NATS (with JetStream) and Postgres (with pgvector) using Docker Compose:
cd infra/
docker compose up -dVerify services are healthy:
docker compose ps| Service | Port | Purpose |
|---|---|---|
nats |
4222 (client), 8222 (monitoring) |
Message bus with JetStream persistence |
postgres |
5435 |
Episode storage with pgvector ANN |
timescaledb |
5433 |
Time-series signal storage (for future use) |
Both model files must exist before the fast-path engine can start.
cd learning/
python export_fast_brain.py
# → infra/models/regime_fast_v1.bincd learning/
python export_onnx_brain.py
# → infra/models/onnx/decision_brain.onnxStart all processes in separate terminals in this order:
cd infra/
docker compose upcd /path/to/triad/
cargo run -p monitoringcargo run -p fast-pathpython experiencing/episode_store.pypython learning/nats_consumer.pytriad/
├── Cargo.toml # Workspace manifest (fast-path + monitoring crates)
├── Cargo.lock
│
├── fast-path/ # Rust: Real-time execution engine
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # Entry point + main event loop
│ ├── classifier.rs # Softmax regime classifier
│ ├── memory.rs # HNSW episodic memory bank
│ ├── model_loader.rs # Hot-reloadable binary model loader
│ └── onnx_engine.rs # ONNX Runtime wrapper
│
├── monitoring/ # Rust: Witness Node (data ingestion)
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # WebSocket loop + NATS publisher
│ ├── orderbook.rs # LocalOrderBook + VpinTracker
│ ├── events.rs # Shared event types + SHA-256 hash chain
│ └── lib.rs # Re-exports for fast-path dependency
│
├── experiencing/ # Python: Slow-path data pipeline
│ └── episode_store.py # NATS JetStream → Postgres archiver
│
├── learning/ # Python: Model training & debugging
│ ├── export_fast_brain.py # Export 116-byte regime classifier binary
│ ├── export_onnx_brain.py # Train + export ONNX decision model
│ └── nats_consumer.py # Debug: inspect live observation stream
│
├── infra/ # Infrastructure configuration
│ ├── docker-compose.yml # NATS + Postgres + TimescaleDB
│ ├── models/
│ │ ├── regime_fast_v1.bin # Regime classifier weights (116 bytes)
│ │ └── onnx/
│ │ └── decision_brain.onnx
│ └── postgres/
│ └── init.sql # DB schema (auto-applied on first docker compose up)
│
└── shared/ # (Reserved) Future shared Rust crate
All configuration is currently via hardcoded constants or environment assumptions. Key values:
| Setting | Location | Default |
|---|---|---|
| NATS URL | monitoring/src/main.rs, fast-path/src/main.rs |
nats://localhost:4222 |
| Postgres DSN | fast-path/src/memory.rs, experiencing/episode_store.py |
postgres://triad:triad@localhost:5435/triad |
| Starting capital | fast-path/src/main.rs |
$100,000 USD |
| Take-profit threshold | fast-path/src/main.rs |
$20.00 |
| Stop-loss threshold | fast-path/src/main.rs |
-$10.00 |
| Model hot-reload interval | fast-path/src/main.rs |
5 seconds |
| MemoryBank refresh interval | fast-path/src/memory.rs |
60 seconds |
| KNN neighbours | fast-path/src/main.rs |
k = 5 |
| KNN distance gate | fast-path/src/memory.rs |
avg_dist > 2.0 → reject |
| VPIN bucket size | monitoring/src/main.rs |
1.0 BTC |
| VPIN window | monitoring/src/main.rs |
50 buckets |
| Confidence threshold | infra/models/regime_fast_v1.bin |
0.90 |
| Signal vector dimensions | monitoring/src/events.rs |
32 (4 active) |
# Run all unit tests across the workspace
cargo test
# Run tests for a specific crate
cargo test -p fast-path
cargo test -p monitoringCovered by tests:
model_loader: valid blob parsing, size/magic rejection, mtime-based reload logicclassifier: correct softmax output, Unknown threshold gating, insufficient-features guard
- Start infrastructure and all processes.
- Watch
fast-pathlogs forTRADE OPENEDandTRADE CLOSEDevents. - Verify
episode_store.pylogsArchived Episode ...for each closed trade. - Connect to Postgres and inspect:
SELECT id, direction, outcome_pnl, max_adverse_excursion FROM episodes ORDER BY created_at DESC LIMIT 10;
- Live regime labeling — slow-path updates
regime_authoritativein Postgres after each episode - Proper ML training pipeline — replace synthetic weights with Postgres-trained LogisticRegression
- Signal marketplace — populate
signal_scorestable with rolling per-feature accuracy - Model promotion gates — automate
SHADOW → LIVEbased onmodel_metadatathresholds - TimescaleDB integration — write raw signal time-series for backtesting
- Blockchain anchoring — periodically commit
chain_heads.head_hashto a public chain - Multi-asset support — parameterize asset symbol and add per-asset NATS subjects
- Configuration file — move hardcoded thresholds to a
config.toml - Live exchange integration — replace
PaperPortfoliowith a real exchange OMS adapter
This project is for research and educational purposes. All trading decisions are simulated (paper trading only). Do not use in production with real funds without appropriate risk controls.