# opendeviationbar-py: System Architecture

**Parent**: [`/CLAUDE.md`](/CLAUDE.md) | **Structure**: [`.planning/codebase/STRUCTURE.md`](./ STRUCTURE.md)

Comprehensive overview of the 10-crate Rust workspace with Python bindings, from core algorithm to streaming sidecar to ClickHouse cache.

---

## Table of Contents

1. [High-Level Data Flow](#high-level-data-flow)
2. [Architectural Layers](#architectural-layers)
3. [Core Components](#core-components)
4. [Streaming Sidecar Architecture](#streaming-sidecar-architecture)
5. [PyO3 Bindings Layer](#pyo3-bindings-layer)
6. [Python API Layer](#python-api-layer)
7. [ClickHouse Cache Infrastructure](#clickhouse-cache-infrastructure)
8. [Key Design Patterns](#key-design-patterns)

---

## High-Level Data Flow

### Diagram: End-to-End Trade → Bar Pipeline

```
                          REAL-TIME STREAMING PATH
                                    ▼
                    Binance WebSocket (per symbol)
                                    │
                                    ▼
                    Rust StreamManager (Issue #161)
                    ├─ Per-symbol WebSocket listener
                    ├─ Trade-ID continuity detection
                    └─ Adaptive rate limiting (Issue #162)
                                    │
                                    ▼
                    Rust LiveBarEngine (Issue #91)
                    ├─ One processor per (symbol, threshold)
                    ├─ OpenDeviationBar construction
                    ├─ 58 microstructure features
                    ├─ Gap detection (Puck, Issue #257)
                    └─ Midnight reset (Ouroboros)
                                    │
                                    ▼
                    Python Sidecar (sidecar.py)
                    ├─ Receives completed bars (~1-6 Hz)
                    ├─ Pre-write validation (L1 gate)
                    ├─ ClickHouse batch writes
                    └─ SSE push to consumers
                                    │
                                    ▼
                    ClickHouse (bigblack GPU host)
                    └─ 42M+ open deviation bars


                        HISTORICAL BACKFILL PATH
                                    ▼
                    User API: get_open_deviation_bars()
                                    │
                    ┌───────────────┼───────────────┐
                    │               │               │
                    ▼ (≤30 days)    ▼ (long range)  ▼ (count-bounded)
            Direct fetch from    Tier-1 Parquet  ClickHouse count
            Binance Vision/REST   ticks cache     lookup
                    │               │               │
                    └───────────────┼───────────────┘
                                    │
                                    ▼
                    Rust: OpenDeviationBarProcessor
                    └─ Batch process trades → bars
                                    │
                                    ▼
                    ClickHouse cache (if storing)
                                    │
                                    ▼
                    Python: Pandas DataFrame
                    ├─ OHLCV core columns
                    ├─ 10 intra-bar features
                    ├─ 16 inter-bar features
                    ├─ 27 trade-ID/exchange columns
                    └─ 5 orphan/day-mode columns
```

### Data Flow Summary

| Path          | Source             | Flow                                                           | Sink              | Latency |
| ------------- | ------------------ | -------------------------------------------------------------- | ----------------- | ------- |
| **Real-Time** | Binance WS         | StreamManager → LiveBarEngine → Python → ClickHouse            | ClickHouse        | <500ms  |
| **Batch**     | Binance Vision CSV | HistoricalDataLoader → Processor → bars                        | Python/ClickHouse | Batch   |
| **Count**     | ClickHouse         | `count_bars()` lookup → ariadne → REST fill → Processor → bars | Python            | <5s     |
| **Tier1**     | Local Parquet      | `TickStorage.read_ticks()` → Processor → bars                  | Python            | ~50ms   |

---

## Architectural Layers

### Layer 0: Foundation (Core Algorithms)

**Location**: `crates/opendeviationbar-core/`, `crates/opendeviationbar-config/`, `crates/opendeviationbar-hurst/`

**Purpose**: Self-contained, zero external opendeviationbar dependencies. Publishable to crates.io independently.

#### opendeviationbar-core

**Key Types**:

- `AggTrade` — Single aggregated trade (millisecond timestamp, price, qty, buy/sell)
- `OpenDeviationBar` — Output bar with 58 columns (OHLCV + 10 intra + 16 inter + trade-ID + metadata)
- `FixedPoint` — 8-decimal fixed-point arithmetic (avoids floating-point errors)
- `OpenDeviationBarProcessor` — Stateful processor maintaining:
  - Current bar state (open/high/low/close/volume/etc.)
  - Threshold ratio (pre-computed for hot path)
  - Trade history (ring buffer for inter-bar features)
  - Checkpoint state (for resuming from interruption)

**Algorithm (Non-Lookahead Breach Detection)**:

```
1. First trade arrives → Opens bar: open = high = low = close = price
   Thresholds: upper = open * (1 + threshold_dbps/10000)
               lower = open * (1 - threshold_dbps/10000)

2. Subsequent trades → Update OHLCV, accumulate volume/turnover/OFI

3. Trade breaches threshold AND timestamp differs from open_time (Issue #36)
   → BREACH DETECTED
   → Include breaching trade in closing bar (final OHLCV update)
   → Compute 58 microstructure features
   → Finalize bar

4. defer_open = true (processor holds NO bar state)

5. Next trade arrives → Because defer_open == true, open fresh bar
   (NOT the breaching trade; Issue #46)
```

**Key Invariant**: Breaching trade belongs to the **closing bar**, not the opening bar. This ensures non-lookahead bias: the breach timestamp is included in bar construction.

**Microstructure Features (v7.0)**:

- **Intra-bar** (10 features): Duration, OFI, VWAP deviation, price impact, Kyle lambda, trade intensity, volume/trade, aggression ratio, aggregation density, turnover imbalance
- **Inter-bar** (16 features): 7 core lookback metrics + 6 statistical + 3 advanced (Hurst, permutation entropy, Kaufman ER)

#### opendeviationbar-config

**Purpose**: Configuration management via pydantic-settings (Python-facing).

**Key Types**: `Settings` (environment-driven config)

#### opendeviationbar-hurst

**Purpose**: Independent Hurst exponent estimator (DFA-based).

**License**: MIT (forked from GPL-3.0 to enable PyPI distribution).

---

### Layer 1: Data Access (Providers)

**Location**: `crates/opendeviationbar-providers/`, `crates/opendeviationbar-io/`

**Purpose**: Abstract data sources and I/O operations.

#### opendeviationbar-providers

**Data Sources**:

- **Binance** (default):
  - REST API (`api.binance.com/api/v3/aggTrades`)
  - Vision archives (`data.binance.vision/data/spot/daily/`)
  - WebSocket (`stream.binance.com:9443/ws/{symbol}@aggTrade`)
  - Features: Spot, UM futures, CM futures (selectable)

- **Exness** (optional):
  - REST API for 10 forex pairs (EURUSD, GBPUSD, etc.)
  - Tick-level data (not aggregated)

**Key Classes**:

- `HistoricalDataLoader` — Batch download from Vision/REST
- `BinanceWebSocketStream` — Real-time WebSocket connection with automatic reconnection
- `AdaptiveRateLimiter` — Paces REST API requests (exponential backoff for 429 responses)

**Features**:

- **Transient retry**: All requests retry network errors, 5xx, and 429 (rate limit) with exponential backoff (1s → 2s → 4s → 8s → 16s cap, 5 attempts).
- **Non-transient fail-fast**: 4xx errors (except 429) returned immediately.
- **TLS**: Uses `rustls-tls` (no OpenSSL dependency) for cross-platform compilation.

#### opendeviationbar-io

**Purpose**: DataFrame and file I/O.

**Export Formats**: CSV, Parquet (via Polars), Arrow IPC

---

### Layer 2: Processing Engines

**Location**: `crates/opendeviationbar-streaming/`, `crates/opendeviationbar-batch/`

**Purpose**: Real-time and batch processing pipelines.

#### opendeviationbar-streaming

**Key Components** (Issue #91, #161):

1. **LiveBarEngine** — Multiplexes Binance WebSocket across symbols
   - One tokio task per symbol (independent reconnection)
   - One `OpenDeviationBarProcessor` per (symbol, threshold) pair
   - Completed bars emitted to bounded ring buffer
   - Graceful shutdown via `CancellationToken`
   - Midnight boundary handling (Ouroboros reset)

2. **StreamManager** (Issue #161) — Wraps LiveBarEngine with:
   - **Trade-ID continuity detection** via `TradeIdGapDetector`
   - **Adaptive rate limiting** for REST gap-fill pacing
   - **Gap event channels** for communicating fill operations
   - **Shadow validation** diagnostics
   - **Per-symbol liveness monitoring** (Argus)

3. **Gap Detection** (Issue #257) — `TradeIdGapDetector`
   - Monitors `bar.last_agg_trade_id` across bars
   - Emits `GapEvent` when `next_bar.first_agg_trade_id != prev_bar.last_agg_trade_id + 1`
   - Triggers inline `puck_fill()` (parallel REST fetch, sub-3s)

4. **Forming Bar Snapshots** (Issue #214) — `watch::Receiver<Option<FormingBar>>`
   - Published at 1Hz to consumers
   - Each (symbol, threshold) has independent watch channel
   - Used by SSE push to emit live bar updates

**Key Metrics**:

- `trades_received`, `bars_emitted`, `reconnections`
- `backpressure_events`, `dropped_bars`, `max_queue_depth`
- `gap_fills`, `gap_trades_recovered`

#### opendeviationbar-batch

**Purpose**: Parallel batch analysis via Rayon.

**Key Types**: `BatchAnalysisEngine`, `AnalysisReport`

---

### Layer 3: Applications (Python + CLI)

**Location**: `python/opendeviationbar/`, `crates/opendeviationbar-cli/`

#### Python API Layer

**Purpose**: User-facing API for bar generation, cache operations, streaming.

**Entry Points** (in `__init__.py`):

- `get_open_deviation_bars(symbol, start_date, end_date, threshold_decimal_bps)` — Date-bounded
- `get_n_open_deviation_bars(symbol, n_bars, threshold_decimal_bps)` — Count-bounded
- `process_trades_to_dataframe(trades_df, threshold_decimal_bps)` — From existing DataFrame
- `process_trades_polars(trades_df, threshold_decimal_bps)` — Polars variant (2-3x faster)
- `process_trades_chunked(trades_iterator, chunk_size)` — Memory-safe for large datasets
- `populate_cache_resumable(symbol, start_date, end_date, threshold_decimal_bps)` — Pre-populate cache

**Subsystems**:

| Subsystem            | File                              | Purpose                                      |
| -------------------- | --------------------------------- | -------------------------------------------- |
| Sidecar orchestrator | `sidecar.py`                      | Run streaming live engine                    |
| Consumer API         | `sidecar_api.py`                  | ariadne, gap-fill, catchup for consumers     |
| Cache operations     | `clickhouse/cache.py`             | ClickHouse read/write                        |
| Tier-1 ticks cache   | `storage/parquet.py`              | Local Parquet daily files                    |
| Gap reconciliation   | `kintsugi.py`                     | Self-healing Stathera gap detection + repair |
| Symbol registry      | `symbol_registry.py`              | Mandatory registration gate (Issue #79)      |
| Validation framework | `validation/tier1.py`, `tier2.py` | Data integrity checks                        |
| Feature plugins      | `plugins/`                        | Runtime-extensible columns via entry points  |

#### CLI (Disabled for PyPI)

**Location**: `crates/opendeviationbar-cli/`

6 binaries preserved as source, not built:

- `tier1-symbol-discovery`
- `opendeviationbar-analyze`
- `data-structure-validator`
- `spot-tier1-processor`
- `polars-benchmark`
- `temporal-integrity-validator`

---

## Streaming Sidecar Architecture

### Diagram: Sidecar Real-Time Pipeline

```
Binance WebSocket (multiple streams per symbol)
        │
        ▼
   StreamManager (Rust, Issue #161)
   ├─ Per-symbol listener (tokio task)
   ├─ TradeIdGapDetector (Issue #257)
   │  └─ Detects: first_tid != last_tid + 1
   ├─ AdaptiveRateLimiter (Issue #162)
   │  └─ Paces REST gap-fill requests
   └─ Gap event channel (mpsc)
        │
        ├─ [Puck fills inline, <3s, via parallel REST fetch]
        │
        ▼
   LiveBarEngine (Rust, Issue #91)
   ├─ One processor per (symbol, threshold)
   ├─ Completed bars → ring buffer
   ├─ Forming bars → watch::Receiver (1Hz)
   └─ Midnight reset → Ouroboros
        │
        ▼
   Python Sidecar Daemon (sidecar.py)
   ├─ Read completed bars from ring buffer (~1-6 Hz)
   ├─ Pre-write gate (Stathera validation, Issue #158)
   ├─ ClickHouse batch write (overlap_strategy="off", L1 lock)
   ├─ Checkpoint persistence (streaming_{symbol}_{threshold}.json)
   └─ SSE push (forming bars, consumer health)
        │
        ├─ [Niffler: Immediate dead-letter replay on write failure]
        ├─ [Charon: Periodic dead-letter ferry (every 5 min)]
        │
        ▼
   ClickHouse (bigblack)
   └─ open_deviation_bars table (ReplacingMergeTree)


   CONSUMER PATHS (sidecar_api.py)
   ├─ SSE: /sse/bars/{symbol}/{threshold}
   ├─ Ariadne: /ariadne/{symbol}/{threshold}
   │  └─ Sources: processor → checkpoint → ClickHouse (committed_only)
   ├─ Gap-fill: /trades/gap-fill (paginated, 100K cap)
   └─ Catchup: /catchup/{symbol}/{threshold} (composite)
```

### Key Sidecar Concepts

#### 1. Puck (Inline Gap Fill, Issue #257)

**Trigger**: Trade-ID discontinuity detected between bars

**Flow**:

1. `TradeIdGapDetector` emits `GapEvent` when `bar[i].last_tid + 1 != bar[i+1].first_tid`
2. Rust engine calls `puck_fill()` with trade-ID range
3. Parallel REST fetch fills missing trades in <3 seconds
4. Processor resumes from filled trades, closing the gap

**Advantage**: Sub-3s latency, inline (no retry loop)

#### 2. Niffler (Immediate Replay, Issue #265)

**Trigger**: ClickHouse write fails (circuit breaker OPEN)

**Flow**:

1. Batch of bars fails to write
2. Save to `/tmp/opendeviationbar-dead-letter/*.parquet`
3. Immediately retry (before processing next batch)
4. If still fails, continue to normal business (Charon will ferry later)

**Advantage**: Reduces time bars spend in dead-letter queue

#### 3. Charon (Periodic Dead-Letter Ferry, Issue #265)

**Trigger**: Every 5 minutes (Ouroboros timer)

**Flow**:

1. Scan `/tmp/opendeviationbar-dead-letter/` for Parquet files
2. For each file:
   - Try replay to ClickHouse
   - If success, delete file
   - If fail (unplayable), log and skip

**Purpose**: Periodically clear dead-letter backlog when ClickHouse recovers

#### 4. Argus (Per-Symbol Liveness, Issue #267)

**Monitoring**: Per-symbol trade freshness

**Logic**:

- Track `last_trade_time` per symbol from WS
- Alert if >5 minutes without trade (market may be closed)
- Resume on market re-open

#### 5. Lethe (Ring Buffer Overflow Detection, Issue #266)

**Trigger**: Ring buffer (10K capacity) overflows

**Action**:

1. Emit warning (backpressure event)
2. Skip oldest completed bar
3. Continue processing

**Purpose**: Prevent sidecar crash on sustained backpressure

#### 6. Antaeus (Resilient WS Reconnection, Issue #268)

**Mechanism**: Exponential backoff + jitter

**Flow**:

1. WS disconnect detected
2. Backoff: 1s → 2s → 4s → 8s → 16s cap
3. Jitter: ±25% randomization (prevents thundering herd)
4. Retry forever (systemd supervises)

---

## PyO3 Bindings Layer

**Location**: `src/lib.rs`, `src/*.rs` (domain modules)

### Binding Architecture

```
Python Code
    ↓ (import opendeviationbar._core)
PyO3 Module (_core.abi3.so)
    ├── PyOpenDeviationBarProcessor
    ├── PyPositionVerification
    ├── PyAggTrade (dict wrapper)
    ├── PyBinanceLiveStream
    ├── PyLiveBarEngine
    ├── PyStreamManager (Issue #161)
    ├── PyGapEventWatcher (Issue #257)
    ├── PyOdbEngine (Issue #178)
    └── Arrow export functions (optional feature)
        ↓ (calls)
Rust Crates
    ├── opendeviationbar-core
    ├── opendeviationbar-providers
    ├── opendeviationbar-streaming
    └── other crates
```

### Module Organization (Issue #94: Domain Modules)

**File**: `src/lib.rs` (200 lines, thin orchestrator)

**Domain Modules**:

| Module                       | Lines | Purpose                                                 |
| ---------------------------- | ----- | ------------------------------------------------------- |
| `helpers.rs`                 | ~100  | f64 ↔ FixedPoint conversion, dict builders              |
| `core_bindings.rs`           | ~400  | `PyOpenDeviationBarProcessor`, `PyPositionVerification` |
| `arrow_bindings.rs`          | ~150  | `bars_to_arrow()`, `trades_to_arrow()` (feature-gated)  |
| `binance_bindings.rs`        | ~500  | `fetch_binance_aggtrades()`, `PyBinanceTradeStream`     |
| `exness_bindings.rs`         | ~300  | Exness data fetching                                    |
| `streaming_bindings.rs`      | ~400  | `PyLiveBarEngine`, `PyStreamingConfig`                  |
| `stream_manager_bindings.rs` | ~350  | `PyStreamManager`, `PyFormingBarWatcher` (Issue #161)   |
| `odb_engine_bindings.rs`     | ~200  | `PyOdbEngine` (Issue #178, unified engine)              |
| `gap_bindings.rs`            | ~150  | `PyGapEventWatcher`, `PyGapFillResult` (Issue #257)     |
| `thread_pool_init.rs`        | ~50   | Rayon thread pool initialization                        |

### Conversion Patterns

**FixedPoint ↔ f64**:

```rust
// Rust to Python
let price_f64 = bar.open.to_f64();

// Python to Rust (in helpers.rs)
fn f64_to_fixed_point(value: f64) -> FixedPoint {
    FixedPoint::from_f64(value)
}
```

**Dict Export** (`helpers.rs` → `opendeviationbar_to_dict()`):

```rust
// All dict keys use _ms suffix for milliseconds
bar_dict.insert("open_time_ms", open_time_us / 1000);
bar_dict.insert("close_time_ms", close_time_us / 1000);
// All intra-bar and inter-bar features included as f64
```

**Arrow Export** (`arrow_bindings.rs`):

```rust
// Arrow preserves microsecond timestamps (raw Rust i64 values)
"open_time" → Int64 (microseconds)
"close_time" → Int64 (microseconds)
```

### Feature Flags

| Feature            | Default | Crates Affected                            | Python Impact                         |
| ------------------ | ------- | ------------------------------------------ | ------------------------------------- |
| `data-providers`   | Yes     | binance_bindings, exness_bindings enabled  | `fetch_binance_aggtrades()` available |
| `arrow-export`     | Yes     | arrow_bindings enabled                     | Arrow I/O functions available         |
| `streaming`        | Yes     | streaming_bindings, stream_manager enabled | Real-time sidecar available           |
| `simd-burstiness`  | Yes     | Rust side (performance)                    | Faster inter-bar feature computation  |
| `simd-kyle-lambda` | Yes     | Rust side (performance)                    | Faster Kyle lambda computation        |

---

## Python API Layer

**Location**: `python/opendeviationbar/`

### Architecture

```
User Code
    ↓
__init__.py (Public API)
    ├─ get_open_deviation_bars()
    ├─ get_n_open_deviation_bars()
    ├─ process_trades_*()
    ├─ populate_cache_resumable()
    └─ sidecar imports
        ↓
orchestration/ (Request pipeline)
    ├─ open_deviation_bars.py (main entry point logic)
    ├─ count_bounded.py (count-bounded API)
    ├─ circuit_breaker.py (failure handling, exponential backoff)
    └─ stream.py (streaming sidecar)
        ↓
        ├─ clickhouse/cache.py (ClickHouse read/write)
        ├─ storage/parquet.py (Tier-1 ticks cache)
        ├─ kintsugi.py (gap reconciliation)
        └─ symbol_registry.py (mandatory gate)
```

### Request Flow: `get_open_deviation_bars("BTCUSDT", "2024-01-01", "2024-06-30")`

```
1. Input validation
   ├─ Symbol registry gate (Issue #79)
   ├─ Threshold range check (1-100,000 dbps)
   ├─ Date sorting

2. Range check
   ├─ If > 30 days AND NOT cached → raise "use populate_cache_resumable()"
   ├─ If ≤ 30 days → proceed

3. Tier-1 check: Local Parquet ticks cache
   ├─ Call TickStorage.has_ticks()
   ├─ If YES → load from local Parquet
   ├─ If NO → proceed to Tier-2

4. Tier-2: Binance Vision/REST download
   ├─ HistoricalDataLoader.download_intraday_chunks()
   ├─ Vision CSV → Parquet (save locally via TickStorage)
   ├─ Fallback to REST if Vision unavailable

5. Process trades via Rust
   ├─ Convert to AggTrade records
   ├─ Call Rust processor: process_agg_trade_records()
   ├─ Get OpenDeviationBar vector

6. Convert to Python DataFrame
   ├─ Bars → list of dicts (via opendeviationbar_to_dict())
   ├─ Dicts → Polars DataFrame
   ├─ Polars → Pandas (if needed)

7. Cache write (optional, if range is good for caching)
   ├─ Call ClickHouse cache.store_bars_batch()
   ├─ Circuit breaker handles transient failures

8. Return to user
   └─ Pandas DataFrame (OHLCV + 53 feature columns)
```

### Caching Tiers

#### Tier 1: Local Parquet Ticks (Issue #277)

**Location**: `~/.cache/opendeviationbar/ticks/{SYMBOL}/YYYY-MM-DD.parquet`

**Content**: Raw aggTrades (agg_id, price, qty, timestamp, is_buy)

**Key Behavior**: Multi-threshold efficiency

- `get_open_deviation_bars("BTCUSDT", "2024-01-01", threshold=250)` → downloads + caches ticks
- `get_open_deviation_bars("BTCUSDT", "2024-01-01", threshold=500)` → reads from local Parquet (zero network)

**Used by**: `populate_cache_resumable()`, `get_open_deviation_bars()`, all backfill operations

#### Tier 2: ClickHouse Bar Cache

**Location**: bigblack (remote GPU host)

**Content**: Pre-computed open deviation bars (58 columns)

**Key Operation**:

1. `count_bars()` — Check if date range cached
2. If partial → `ariadne_lookup()` + `gap_fill_trades()` → fill gap
3. If cached → direct fetch

**Used by**: `get_open_deviation_bars()`, `get_n_open_deviation_bars()`, streaming consumer API

#### Checkpoint Cache

**Location**: `~/.cache/opendeviationbar/checkpoints/{symbol}_{threshold}.json`

**Content**: Last bar close time, last trade ID, processor checkpoint

**Used by**: Streaming sidecar (`sidecar.py`) to resume mid-bar after restart

---

## ClickHouse Cache Infrastructure

### Schema (v7.0+)

**Database**: `opendeviationbar_cache`

**Main Table**: `open_deviation_bars`

**Engine**: `ReplacingMergeTree`

**ORDER BY**: `(symbol, threshold_decimal_bps, ouroboros_mode, first_agg_trade_id)`

**Columns**:

| Group                    | Count | Examples                                    |
| ------------------------ | ----- | ------------------------------------------- |
| Metadata                 | 5     | symbol, threshold, ouroboros_mode, version  |
| OHLCV                    | 5     | open, high, low, close, volume              |
| Timestamps               | 2     | open_time_ms, close_time_ms                 |
| Trade-ID range           | 2     | first_agg_trade_id, last_agg_trade_id       |
| Intra-bar microstructure | 10    | duration_us, ofi, vwap_close_deviation, ... |
| Inter-bar lookback       | 16    | lookback_trade_count, lookback_ofi, ...     |
| Exchange session         | 5     | session_open_hour, is_session_boundary, ... |
| Asset class              | 3     | asset_class, tier, multiplier               |
| Bar flags                | 3     | is_orphan, is_liquidation_cascade, ...      |
| Plugin columns           | ~5    | (runtime-extensible via feature plugins)    |

**Plugin Columns** (Issue #98): Added dynamically by `FeatureProvider` plugins via `ALTER TABLE ADD COLUMN IF NOT EXISTS`. Not hard-coded in `schema.sql`.

### Write Operations

**Path**: `clickhouse/cache.py` → `store_bars_batch()`

**Features**:

1. **Circuit Breaker** (Issue #202): 3 failures → OPEN → (60s) → HALF_OPEN → (success) → CLOSED
2. **Pre-write validation** (L2 dedup): `overlap_strategy="replace"` for historical backfill, `"off"` for sidecar (L1 lock handles concurrency)
3. **Atomic batch**: All bars in batch succeed or all fail

**Dedup Hardening** (v12.4+):

- L1: Redis write lock per (symbol, threshold) — prevents concurrent writers
- L2: ReplacingMergeTree dedup via `(symbol, threshold, ouroboros_mode, first_agg_trade_id)`
- L3: Pre-write overlap check (if `overlap_strategy="replace"`)

### Read Operations

**Paths**:

1. `get_open_deviation_bars()` — Date range query
2. `count_bars()` — Quick bar count for coverage check
3. `get_n_bars()` — Exact N bars (reverse chronological)
4. Ariadne lookup — Latest committed bar for consumers

---

## Key Design Patterns

### 1. Stateful Processor with Checkpoints

**Pattern**: `OpenDeviationBarProcessor` maintains state across calls.

**State**:

- Current incomplete bar (open_time, OHLCV, accumulated features)
- Last processed trade ID (for gap detection)
- Trade history (ring buffer for inter-bar features)
- `defer_open` flag (Issue #46)

**Checkpoint**: Can be serialized to JSON and restored.

**Use Case**: Resume streaming after restart without reprocessing all trades.

**Code Path**: `checkpoint.py` → `create_checkpoint()` / `from_checkpoint()`

### 2. Day-Mode Ouroboros Reset (Issue #264)

**Pattern**: Processor resets state at UTC midnight boundaries.

**Implementation**: `reset_at_ouroboros()` called by:

- Streaming engine at midnight boundary
- Batch processor after each day's trades
- Backfill when crossing day boundary

**Invariant**: Every bar belongs to exactly one UTC day. Cross-midnight bars cause cascading gaps.

**Defense Layers**:

| Layer | Mechanism                | File               |
| ----- | ------------------------ | ------------------ |
| L1    | Sidecar filter           | sidecar.py:2415    |
| L2    | Pre-write gate           | bulk_operations.py |
| L3    | Dead-letter cleanup      | kintsugi.py        |
| L4    | Redis write lock         | write_lock.py      |
| L5    | ReplacingMergeTree dedup | schema.sql         |

### 3. Writer Ownership Model (Issue #280)

**Pattern**: Sidecar owns today (UTC). Kintsugi owns yesterday and older.

**Rationale**: Eliminates concurrent-writer races.

**Implementation**:

- Sidecar: Writes bars with `close_time_ms >= today_start_ms`
- Kintsugi: Only writes bars with `close_time_ms < yesterday_end_ms`
- Asclepius: **PERMANENTLY DISABLED** (unsafe for old gaps on live engine)

### 4. Stathera Trade-ID Continuity (Issue #158)

**Invariant**: `bar[i].last_agg_trade_id + 1 == bar[i+1].first_agg_trade_id`

**Detection**: `detect_gaps.py` scans consecutive bars, reports Stathera anomalies.

**Repair**: Kintsugi fetches trades via ariadne, re-processes with proper day boundaries.

### 5. Circuit Breaker Pattern

**Usage**: ClickHouse write failures

**States**:

1. **CLOSED** (normal) — Writes succeed
2. **OPEN** — 3 failures detected, refuse writes, wait 60s
3. **HALF_OPEN** — Retry a single write
4. **CLOSED** (on success) or **OPEN** (on failure) → restart

**Code**: `orchestration/circuit_breaker.py`

### 6. Thread-Local Connection Reuse

**Pattern**: Never create DB connections in a loop.

**Implementation**: `threading.local()` stores ClickHouse connection, socket connection per thread.

**Code**: `clickhouse/cache.py` → connection pooling via context manager

### 7. Memory Guards (Dual-Layer)

**Layer 1: Soft** — Application-level watermark checks

- 65% memory used → GC collection
- 90% memory used → abort with warning

**Layer 2: Hard** — systemd `MemoryMax`

- Enforced OS limit
- SIGKILL if exceeded

**Code**: `resource_guard.py`, systemd service files

---

## Dependency Graph

```
┌──────────────────────────────────────────────────────────────┐
│                     opendeviationbar (meta-crate)             │
└──────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
   opendeviationbar-cli   opendeviationbar-batch   opendeviationbar-streaming
        │                     │                     │
        └──────────┬──────────┴──────────┬──────────┘
                   │                     │
             opendeviationbar-io    opendeviationbar-providers
                   │                     │
                   └──────────┬──────────┘
                              │
                        opendeviationbar-core
                              │
                        opendeviationbar-config

                   [Independent]
        opendeviationbar-client (SSE consumer)
        opendeviationbar-hurst (Hurst exponent)
```

---

## Summary Table

| Layer         | Technology   | Location                             | Key Responsibility                  |
| ------------- | ------------ | ------------------------------------ | ----------------------------------- |
| **Algorithm** | Rust         | `crates/opendeviationbar-core/`      | Bar construction, microstructure    |
| **Providers** | Rust + Tokio | `crates/opendeviationbar-providers/` | Binance/Exness data fetching        |
| **Streaming** | Rust + Tokio | `crates/opendeviationbar-streaming/` | Real-time multi-symbol bar engine   |
| **PyO3**      | Rust + PyO3  | `src/`                               | Python ↔ Rust bindings              |
| **API**       | Python       | `python/opendeviationbar/`           | User-facing functions, caching, API |
| **Cache**     | ClickHouse   | bigblack (remote)                    | Bar persistence, analytics          |

---

## Related Documentation

- **Project Hub**: [`/CLAUDE.md`](/CLAUDE.md)
- **Directory Structure**: [`.planning/codebase/STRUCTURE.md`](./STRUCTURE.md)
- **Algorithm Details**: [`/docs/OPEN-DEVIATION-BAR.md`](/docs/OPEN-DEVIATION-BAR.md)
- **API Reference**: [`/docs/api/INDEX.md`](/docs/api/INDEX.md)
- **Crates Details**: [`/crates/CLAUDE.md`](/crates/CLAUDE.md)
- **Python Layer**: [`/python/opendeviationbar/CLAUDE.md`](/python/opendeviationbar/CLAUDE.md)
- **ClickHouse Schema**: [`/python/opendeviationbar/clickhouse/CLAUDE.md`](/python/opendeviationbar/clickhouse/CLAUDE.md)
