---
phase: 14-rust-core-gap-fields-conditional-reset
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs
  - crates/opendeviationbar-streaming/src/live_engine/types.rs
autonomous: true
requirements: [CORE-01, CORE-04]

must_haves:
  truths:
    - "When processor is in Aion mode, maybe_reset_at_midnight returns None for all trades (no midnight reset, no orphan bars)"
    - "When processor is in Day mode, maybe_reset_at_midnight behaves identically to current production (backward compatible)"
    - "process_trade_through_processors receives ouroboros mode and passes it to maybe_reset_at_midnight"
  artifacts:
    - path: "crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs"
      provides: "Conditional midnight reset based on ouroboros_mode"
      contains: "ouroboros_mode"
  key_links:
    - from: "crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs"
      to: "crates/opendeviationbar-core/src/processor.rs"
      via: "maybe_reset_at_midnight conditionally calls reset_at_ouroboros"
      pattern: "Aion|aion"
---

<objective>
Make the midnight reset in trade_dispatch.rs conditional on ouroboros_mode. Aion-mode symbols skip reset_at_ouroboros() entirely, allowing bars to span UTC midnight boundaries. Day-mode behavior is unchanged.

Purpose: Aion (continuous) mode for 24/7 crypto needs bars that cross midnight without being split. The reset decision must happen at the trade dispatch level where the day boundary check occurs.
Output: Conditional midnight reset in trade_dispatch.rs, mode passed through from process_trade_through_processors.
</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/14-rust-core-gap-fields-conditional-reset/14-CONTEXT.md
@crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs
@crates/opendeviationbar-streaming/src/live_engine/types.rs

<interfaces>
<!-- Key types and contracts the executor needs -->

From crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs:

```rust
// maybe_reset_at_midnight (line ~135-149):
pub(crate) fn maybe_reset_at_midnight(
    processor: &mut OpenDeviationBarProcessor,
    trade_timestamp_us: i64,
    last_day: &mut i64,
) -> Option<OpenDeviationBar> {
    let trade_day = trade_timestamp_us / DAY_US;
    if *last_day >= 0 && trade_day != *last_day {
        *last_day = trade_day;
        processor.reset_at_ouroboros()
    } else {
        *last_day = trade_day;
        None
    }
}

// process_trade_through_processors (line ~17-28):
pub(crate) fn process_trade_through_processors(
    trade: &AggTrade,
    processors: &mut [(u32, OpenDeviationBarProcessor)],
    last_days: &mut [i64],
    bar_buffer: &ConcurrentRingBuffer<CompletedBar>,
    metrics: &LiveEngineMetrics,
    symbol_arc: &Arc<str>,
    symbol: &str,
    forming_tx_map: &HashMap<u32, watch::Sender<Option<FormingBar>>>,
    committed_floors: &mut HashMap<u32, i64>,
    last_forming_update: &mut HashMap<u32, i64>,
)
```

From crates/opendeviationbar-streaming/src/live_engine/types.rs:

```rust
pub struct CompletedBar {
    pub symbol: Arc<str>,
    pub threshold_decimal_bps: u32,
    pub bar: OpenDeviationBar,
}
```

The call site that invokes process_trade_through_processors is in the live_engine symbol_task.
The ouroboros_mode needs to flow from the symbol configuration down to this function.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add OuroborosMode enum to streaming crate and make midnight reset conditional</name>
  <read_first>
    - crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs (full file — maybe_reset_at_midnight, process_trade_through_processors)
    - crates/opendeviationbar-streaming/src/live_engine/types.rs (CompletedBar, FormingBar)
    - crates/opendeviationbar-streaming/src/live_engine/mod.rs (to find symbol_task and how process_trade_through_processors is called)
  </read_first>
  <files>crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs, crates/opendeviationbar-streaming/src/live_engine/types.rs</files>
  <action>
    **Step 1: Add OuroborosMode to the streaming crate's types.rs:**

    Add a simple enum at the top of `types.rs` (this is a Rust-native enum for the streaming crate — separate from Python's OuroborosMode):
    ```rust
    /// Ouroboros reset mode for bar construction
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    pub enum OuroborosMode {
        /// Day mode: reset at UTC midnight (existing behavior)
        #[default]
        Day,
        /// Aion mode: continuous, no midnight reset (24/7 crypto)
        Aion,
    }
    ```

    **Step 2: Add ouroboros_mode parameter to maybe_reset_at_midnight:**

    Change the signature to:
    ```rust
    pub(crate) fn maybe_reset_at_midnight(
        processor: &mut OpenDeviationBarProcessor,
        trade_timestamp_us: i64,
        last_day: &mut i64,
        ouroboros_mode: OuroborosMode,
    ) -> Option<OpenDeviationBar> {
        // Aion mode: never reset at midnight — bars span across day boundaries
        if ouroboros_mode == OuroborosMode::Aion {
            // Still track the day for metrics/logging, but never trigger reset
            let trade_day = trade_timestamp_us / DAY_US;
            *last_day = trade_day;
            return None;
        }

        // Day mode: existing behavior unchanged
        let trade_day = trade_timestamp_us / DAY_US;
        if *last_day >= 0 && trade_day != *last_day {
            *last_day = trade_day;
            processor.reset_at_ouroboros()
        } else {
            *last_day = trade_day;
            None
        }
    }
    ```

    **Step 3: Add ouroboros_mode parameter to process_trade_through_processors:**

    Add `ouroboros_mode: OuroborosMode` as the last parameter of `process_trade_through_processors`.

    Update the `maybe_reset_at_midnight` call inside to pass the mode:
    ```rust
    if let Some(orphan) =
        maybe_reset_at_midnight(processor, trade.timestamp, &mut last_days[idx], ouroboros_mode)
    ```

    **Step 4: Update all call sites of process_trade_through_processors:**

    Search the codebase for all calls to `process_trade_through_processors`. Each call site needs the new `ouroboros_mode` parameter. For now, pass `OuroborosMode::Day` as the default to maintain backward compatibility. The sidecar integration (Phase 16) will wire the actual mode from symbol config.

    Likely call sites:
    - `crates/opendeviationbar-streaming/src/live_engine/mod.rs` or a symbol_task file
    - Search with: `grep -rn "process_trade_through_processors" crates/opendeviationbar-streaming/src/`

    At each call site, add `OuroborosMode::Day` as the last argument (or import the enum and use it). Add a `// TODO(v7.0 Phase 16): wire actual mode from symbol config` comment.

    **Step 5: Export OuroborosMode from the crate:**

    Ensure OuroborosMode is re-exported from the `types` module's public path so Phase 16 can import it when wiring the sidecar.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && cargo check -p opendeviationbar-streaming 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "OuroborosMode" crates/opendeviationbar-streaming/src/live_engine/types.rs` returns enum definition with Day and Aion variants
    - `grep -n "OuroborosMode" crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs` returns usage in maybe_reset_at_midnight and process_trade_through_processors
    - `grep -n "Aion" crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs` returns the early-return None path
    - `cargo check -p opendeviationbar-streaming` succeeds
    - `cargo check -p opendeviationbar-py` succeeds (PyO3 crate depends on streaming)
  </acceptance_criteria>
  <done>maybe_reset_at_midnight is conditional on OuroborosMode. Aion skips reset entirely (returns None). Day mode is unchanged. All call sites compile with default Day mode. Phase 16 will wire actual mode from symbol config.</done>
</task>

</tasks>

<verification>
1. `cargo check -p opendeviationbar-streaming` — streaming crate compiles with conditional reset
2. `cargo check -p opendeviationbar-py` — PyO3 crate compiles (depends on streaming)
3. `cargo nextest run -p opendeviationbar-streaming` — existing streaming tests pass (Day mode default, backward compatible)
4. `grep -c "OuroborosMode::Day" crates/opendeviationbar-streaming/src/` — all call sites use Day as default
</verification>

<success_criteria>

1. OuroborosMode enum exists with Day (default) and Aion variants in the streaming crate
2. maybe_reset_at_midnight accepts OuroborosMode and returns None for Aion (no reset)
3. Day mode behavior is identical to current production
4. All call sites compile and pass existing tests
   </success_criteria>

<output>
After completion, create `.planning/phases/14-rust-core-gap-fields-conditional-reset/14-02-SUMMARY.md`
</output>
