---
phase: 15-oracle-validation
plan: 01
type: tdd
wave: 1
depends_on: []
files_modified:
  - tests/test_aion_oracle.py
autonomous: true
requirements: [VAL-01, VAL-02, VAL-03]

must_haves:
  truths:
    - "Aion mode produces bit-exact identical completed bars as Day mode within a single UTC day"
    - "Aion bars spanning UTC midnight have continuous trade IDs, no orphan bar, and correct VWAP"
    - "Bars constructed from gapped trades have has_gap=true; bars from complete trades have has_gap=false"
  artifacts:
    - path: "tests/test_aion_oracle.py"
      provides: "Oracle tests for all 3 VAL requirements"
      min_lines: 150
      contains: "test_single_day_equivalence"
  key_links:
    - from: "tests/test_aion_oracle.py"
      to: "opendeviationbar._core.PyOpenDeviationBarProcessor"
      via: "process_trades_streaming + reset_at_ouroboros"
      pattern: "process_trades_streaming|reset_at_ouroboros|get_incomplete_bar"
---

<objective>
Bit-exact oracle tests proving Aion mode correctness: single-day equivalence with Day mode, multi-day midnight-spanning continuity, and gap flag lifecycle.

Purpose: Gate all downstream Aion integration (sidecar, kintsugi, monitors) behind proven correctness. No service wiring until the processor behavior is oracle-verified.
Output: `tests/test_aion_oracle.py` with 3+ passing tests covering VAL-01, VAL-02, VAL-03.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/15-oracle-validation/15-RESEARCH.md

@tests/conftest.py
@tests/test_checkpoint_oracle.py
@src/core_bindings.rs
@src/helpers.rs

<interfaces>
<!-- From tests/conftest.py -->
```python
def make_test_trade(agg_trade_id: int, timestamp_ms: int, price: float, quantity: float = 1.0, is_buyer_maker: bool | None = None) -> dict
def make_test_trades(start_id: int, count: int, base_ts: int = 1_700_000_000_000) -> list[dict]
def assert_stathera_continuity(bars: list) -> None
```

<!-- From opendeviationbar._core.PyOpenDeviationBarProcessor -->

```python
class PyOpenDeviationBarProcessor:
    def __init__(self, threshold_decimal_bps: int): ...
    def process_trades_streaming(self, trades: list[dict]) -> list[dict]: ...  # completed bars only, preserves state
    def reset_at_ouroboros(self) -> dict | None: ...  # emit orphan bar + reset state
    def get_incomplete_bar(self) -> dict | None: ...  # peek at forming bar without resetting
```

<!-- Bar dict keys (from helpers.rs opendeviationbar_to_dict) -->

```
open_time_us, close_time_us, open, high, low, close, volume, turnover, agg_record_count,
first_agg_trade_id, last_agg_trade_id, is_orphan, threshold_decimal_bps, vwap,
has_gap, gap_trade_count, max_gap_duration_us, is_exchange_gap,
duration_us, ofi, vwap_close_deviation, price_impact, kyle_lambda_proxy,
trade_intensity, volume_per_trade, aggression_ratio, aggregation_density, turnover_imbalance
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Oracle test file -- trade generators, Day-vs-Aion comparison, gap lifecycle</name>
  <files>tests/test_aion_oracle.py</files>
  <read_first>
    - tests/conftest.py (shared fixtures: make_test_trade, assert_stathera_continuity)
    - tests/test_checkpoint_oracle.py (existing oracle patterns: _generate_breach_trades sawtooth)
    - src/helpers.rs lines 100-170 (bar dict keys including gap fields)
    - src/core_bindings.rs lines 340-370 (process_trades_streaming implementation)
    - src/core_bindings.rs lines 580-665 (get_incomplete_bar, reset_at_ouroboros)
    - .planning/phases/15-oracle-validation/15-RESEARCH.md (pitfalls section especially)
  </read_first>
  <behavior>
    VAL-01 -- Single-day equivalence:
    - Feed 200 sawtooth trades (all within 2024-01-01 UTC) to Day-mode and Aion-mode processors
    - Day-mode: process_trades_streaming + reset_at_ouroboros at end
    - Aion-mode: process_trades_streaming only (no reset)
    - Assert: len(day_completed_bars) == len(aion_completed_bars)
    - Assert: every completed bar is field-equal (skip is_orphan key)
    - Assert: Day orphan bar (if any) has is_orphan=True; Aion has no orphan

    VAL-02 -- Multi-day midnight-spanning continuity:
    - Feed trades spanning 2024-01-01 23:59:xx to 2024-01-02 00:00:xx
    - Use small price movement (within threshold) so no bar completes during the span
    - Then feed breach-triggering trades after midnight so bar completes
    - Day-mode: reset_at_ouroboros at midnight boundary -> orphan bar + fresh processor -> new bars
    - Aion-mode: continuous processing -> single bar spans midnight
    - Assert: Aion spanning bar has open_time_us before midnight and close_time_us after midnight
    - Assert: Stathera continuity across all Aion bars (no TID gaps)
    - Assert: No is_orphan=True bars in Aion output
    - Assert: Aion bar VWAP matches independently computed sum(p*q)/sum(q) from all trades in that bar

    VAL-03 -- Gap flag lifecycle:
    - Feed trades 1000-1004, then skip to 1010 (gap of 5 TIDs), continue to 1020
    - Ensure at least 2 trades before the gap (pitfall 4: gap detection requires last_agg_trade_id > 0)
    - Assert: bar containing the gap has has_gap=True, gap_trade_count > 0, max_gap_duration_us > 0
    - Assert: is_exchange_gap=False (Python sets this, not Rust)
    - Fresh processor with complete trades 1000-1020 (no gap): has_gap=False, gap_trade_count=0, max_gap_duration_us=0

  </behavior>
  <action>
    Create `tests/test_aion_oracle.py` with:

    1. **Module docstring** following oracle methodology: "NEVER use the system under test to generate expected values."

    2. **Imports:**
       ```python
       from opendeviationbar._core import PyOpenDeviationBarProcessor
       from conftest import make_test_trade, assert_stathera_continuity
       ```

    3. **Constants:**
       - `BASE_TS_MS = 1704067200000`  (2024-01-01T00:00:00Z in ms)
       - `MIDNIGHT_JAN2_MS = 1704153600000`  (2024-01-02T00:00:00Z in ms)
       - `THRESHOLD_DBPS = 250`
       - `BASE_PRICE = 50000.0`

    4. **Helper: `_generate_breach_trades()`** -- Copy the sawtooth pattern from test_checkpoint_oracle.py. Oscillating price that guarantees multiple threshold breaches. Parameters: n, threshold_dbps, base_price, base_ts_ms, start_tid.

    5. **Helper: `_assert_bars_equal(bar_a, bar_b, skip_keys=None)`** -- Field-by-field comparison. Float fields use relative tolerance 1e-10. Default skip_keys={"is_orphan"}.

    6. **Helper: `_compute_vwap(trades)`** -- Independent VWAP: `sum(p*q)/sum(q)`. Oracle methodology.

    7. **test_single_day_equivalence()** (VAL-01):
       - Generate 200 breach trades within 2024-01-01 using `_generate_breach_trades(n=200, base_ts_ms=BASE_TS_MS, start_tid=1000)`
       - Day processor: `process_trades_streaming(trades)` then `reset_at_ouroboros()`
       - Aion processor: `process_trades_streaming(trades)` (no reset)
       - Assert completed bar counts match
       - Assert each completed bar pair is field-equal via `_assert_bars_equal` (skip is_orphan)
       - If Day orphan exists, assert `orphan["is_orphan"] is True`
       - Assert Aion incomplete bar (if any) is NOT orphan (no reset occurred)

    8. **test_multi_day_continuity()** (VAL-02):
       - Generate 30 trades before midnight (small price jitter, within threshold -- no breach):
         timestamps from `MIDNIGHT_JAN2_MS - 30000` to `MIDNIGHT_JAN2_MS - 1000`, step 1000ms
         prices: `BASE_PRICE + (i % 3 - 1) * micro_step` where `micro_step = BASE_PRICE * THRESHOLD_DBPS / 100_000 / 20`
         TIDs starting at 1000
       - Generate 30 trades after midnight (same small jitter):
         timestamps from `MIDNIGHT_JAN2_MS + 1000` to `MIDNIGHT_JAN2_MS + 30000`, step 1000ms
         TIDs continuing sequentially (1030-1059)
       - Then generate 100 breach trades after midnight to force bar completions:
         `_generate_breach_trades(n=100, base_ts_ms=MIDNIGHT_JAN2_MS + 31000, start_tid=1060)`

       **Day-mode path:**
       - Process pre-midnight trades: `day_proc.process_trades_streaming(pre_midnight)`
       - Reset at midnight: `day_orphan = day_proc.reset_at_ouroboros()`
       - Process post-midnight + breach trades: `day_proc.process_trades_streaming(post_midnight + breach_trades)`

       **Aion-mode path:**
       - Process ALL trades in one call: `aion_proc.process_trades_streaming(all_trades)`

       **Assertions:**
       - No `is_orphan=True` bars in Aion output (check all completed + incomplete)
       - Stathera continuity: `assert_stathera_continuity(aion_all_bars)` where aion_all_bars includes completed bars + incomplete bar if present
       - For the Aion bar that spans midnight: verify its `first_agg_trade_id` is from pre-midnight trades and `last_agg_trade_id` is from post-midnight trades (or that the incomplete bar spans midnight by checking open_time_us < MIDNIGHT_JAN2_MS * 1000 and close_time_us >= MIDNIGHT_JAN2_MS * 1000)
       - Independent VWAP: compute `_compute_vwap()` from the trades whose TIDs fall within the spanning bar's [first_agg_trade_id, last_agg_trade_id] range; assert matches bar's vwap within 1e-10 relative tolerance

    9. **test_gap_flag_lifecycle()** (VAL-03):
       - **Phase 1 (gapped):** Create trades with TID gap. Use small threshold (10 dbps) so a few trades complete a bar:
         - Trades at TIDs 1000, 1001, 1002, 1003, 1004 (continuous) -- base_price, then rising
         - Skip TIDs 1005-1009
         - Trades at TIDs 1010, 1011, 1012, ..., 1020 (continuous) -- continuing the price rise
         - Timestamps: start at BASE_TS_MS, increment 1000ms each
         - Prices: sawtooth to guarantee at least 1 breach that captures the gap
         - Process through `process_trades_streaming()` + `get_incomplete_bar()`
         - Find bar(s) where has_gap is True (could be completed or incomplete)
         - Assert: `has_gap is True`, `gap_trade_count > 0`, `max_gap_duration_us > 0`, `is_exchange_gap is False`

       - **Phase 2 (repaired):** Fresh processor, same TID range but ALL TIDs present (1000-1020):
         - Same prices/timestamps pattern but with gap filled (TIDs 1005-1009 present)
         - Process through `process_trades_streaming()` + `get_incomplete_bar()`
         - Assert ALL bars: `has_gap is False`, `gap_trade_count == 0`, `max_gap_duration_us == 0`

    10. **test_gap_detection_requires_prior_trade()** (bonus, documents pitfall 4):
        - Feed trades starting at TID 1000 with a jump to TID 2000 as the SECOND trade
        - Assert gap IS detected (has_gap=True) because first trade set last_agg_trade_id > 0
        - Feed trades where the FIRST trade is TID 1000 and immediately jumps to TID 2000
        - Wait -- that IS a valid gap after trade 1. The guard is `last_agg_trade_id > 0` which is true after trade 1 is processed.
        - Actually the pitfall says the first trade always passes without gap check. So: first trade TID=1000 (no gap check). Second trade TID=2000 (gap detected, has_gap=True).
        - Assert: `has_gap is True` after processing just [TID 1000, TID 2000]

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_aion_oracle.py -v -x</automated>
  </verify>
  <acceptance_criteria>
    - tests/test_aion_oracle.py exists with 4 test functions: test_single_day_equivalence, test_multi_day_continuity, test_gap_flag_lifecycle, test_gap_detection_requires_prior_trade
    - `uv run --python 3.13 pytest tests/test_aion_oracle.py -v -x` exits 0 with 4 passed
    - test_single_day_equivalence asserts len(day_bars) == len(aion_bars) and calls _assert_bars_equal
    - test_multi_day_continuity calls assert_stathera_continuity and _compute_vwap
    - test_gap_flag_lifecycle asserts has_gap is True for gapped trades and has_gap is False for complete trades
    - File contains `from opendeviationbar._core import PyOpenDeviationBarProcessor`
    - File contains no ClickHouse imports, no network I/O, no sleep calls
    - `uv run --python 3.13 pytest tests/ -x` (full suite) exits 0
  </acceptance_criteria>
  <done>All 4 oracle tests pass: Aion==Day single-day, Aion spans midnight correctly, gap flags set/cleared, gap detection requires prior trade</done>
</task>

</tasks>

<verification>
1. `uv run --python 3.13 pytest tests/test_aion_oracle.py -v` -- all 4 tests green
2. `uv run --python 3.13 pytest tests/ -x` -- full suite green (no regressions)
3. grep confirms: `has_gap`, `gap_trade_count`, `max_gap_duration_us`, `is_exchange_gap` all tested
4. grep confirms: `reset_at_ouroboros` called in Day-mode tests but NOT in Aion-mode tests
5. grep confirms: `assert_stathera_continuity` called for multi-day continuity
</verification>

<success_criteria>

- 4 oracle tests pass covering VAL-01, VAL-02, VAL-03
- No test uses ClickHouse, network I/O, or external dependencies
- Full test suite (`tests/`) remains green
- Test file follows existing oracle methodology (independent VWAP computation, sawtooth trade generators)
  </success_criteria>

<output>
After completion, create `.planning/phases/15-oracle-validation/15-01-SUMMARY.md`
</output>
