# Day-Mode Invariant Validation

**Parent**: [/python/opendeviationbar/CLAUDE.md](/python/opendeviationbar/CLAUDE.md)

## Purpose

Enforces the **day-mode invariant**: every open deviation bar belongs to exactly one UTC day.
`toDate(open_time_ms / 1000, 'UTC') == toDate(close_time_ms / 1000, 'UTC')` for all bars.

Violation of this invariant causes cascading Sisyphean repair cycles in kintsugi (#264, #263).

## Architecture: 3 Defense Layers

| Layer                | File             | Where It Runs                             | Fail Mode                             |
| -------------------- | ---------------- | ----------------------------------------- | ------------------------------------- |
| **Pre-write gate**   | `guards.py`      | `store_bars_batch()`, `store_bars_bulk()` | Reject bar before ClickHouse INSERT   |
| **Heartbeat oracle** | `oracle.py`      | `health_heartbeat.py` every 5 min         | Persistent Telegram alert             |
| **CLI observation**  | `observation.py` | Claude Code `/loop` or manual             | 4 atomic queries with expected values |

## Invariant Definition

- **Day-mode** (`OPENDEVIATIONBAR_OUROBOROS_MODE=day`): processor resets at UTC midnight
- **Cross-midnight bar**: `open_time_ms` and `close_time_ms` fall on different UTC days
- **Structural day-boundary gap**: `tid_delta > 0` where previous bar's close and current bar's open span different UTC days — these are EXPECTED and must be EXCLUDED from gap detection

## Key Functions

| Function                                                 | Module           | Purpose                                            |
| -------------------------------------------------------- | ---------------- | -------------------------------------------------- |
| `validate_bar_day_mode(bar)`                             | `guards.py`      | Returns bool; raises on violation if `strict=True` |
| `validate_bars_day_mode(bars)`                           | `guards.py`      | Batch validation for list of bar dicts             |
| `split_trades_by_day(trades)`                            | `guards.py`      | Split trade list at UTC midnight boundaries        |
| `cross_midnight_count(client, symbol, threshold)`        | `oracle.py`      | ClickHouse query returning violation count         |
| `stathera_day_classification(client, symbol, threshold)` | `oracle.py`      | Returns (structural_gaps, real_gaps, overlaps)     |
| `run_all_observations(client)`                           | `observation.py` | Run all 4 atomic observations, return pass/fail    |

## Integration Points

- `backfill.py:_fetch_and_process_gap()` — calls `split_trades_by_day()` for day-mode reset
- `backfill.py:_process_gap_fallback_starttime()` — calls `split_trades_by_day()`
- `kintsugi.py:verify_repair()` — calls `cross_midnight_count_range()` as oracle check
- `clickhouse/bulk_operations.py:store_bars_batch()` — calls `validate_bars_day_mode()` pre-write gate
- `health_heartbeat.py` — calls `cross_midnight_count_global()` in Stathera audit section
