---
phase: 16-sidecar-aion-startup
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - crates/opendeviationbar-streaming/src/live_engine/types.rs
  - crates/opendeviationbar-streaming/src/live_engine/mod.rs
  - crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs
  - crates/opendeviationbar-streaming/src/live_engine/puck.rs
  - crates/opendeviationbar-streaming/src/stream_manager.rs
  - src/stream_manager_bindings.rs
autonomous: true
requirements: [SIDE-03]

must_haves:
  truths:
    - "OuroborosMode is threaded per-symbol from Python through PyO3 to every Rust call site that currently hardcodes OuroborosMode::Day"
    - "Puck gap fill uses per-symbol mode so Aion symbols never produce orphan bars during gap recovery"
    - "fill_from_rest uses per-symbol mode so REST backfill processes trades with correct midnight behavior"
  artifacts:
    - path: "crates/opendeviationbar-streaming/src/live_engine/types.rs"
      provides: "symbol_modes field on LiveEngineConfig"
      contains: "symbol_modes"
    - path: "crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs"
      provides: "Per-symbol OuroborosMode parameter threaded to all call sites"
      contains: "ouroboros_mode"
    - path: "src/stream_manager_bindings.rs"
      provides: "PyO3 constructor accepts symbol_modes dict"
      contains: "symbol_modes"
  key_links:
    - from: "src/stream_manager_bindings.rs"
      to: "crates/opendeviationbar-streaming/src/live_engine/types.rs"
      via: "symbol_modes HashMap<String, OuroborosMode> on LiveEngineConfig"
      pattern: "symbol_modes"
    - from: "crates/opendeviationbar-streaming/src/live_engine/mod.rs"
      to: "crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs"
      via: "ouroboros_mode parameter passed to symbol_task spawn"
      pattern: "ouroboros_mode"
---

<objective>
Wire per-symbol OuroborosMode from Python through PyO3 into every Rust call site that currently hardcodes `OuroborosMode::Day`.

Purpose: The Rust streaming engine must know each symbol's mode so Aion symbols skip midnight reset during live trade processing, REST backfill, and Puck gap fill. This is the foundation for SIDE-03 (Puck operational for Aion).
Output: All 7 hardcoded `OuroborosMode::Day` call sites replaced with per-symbol lookups; PyO3 StreamManager accepts `symbol_modes` dict.
</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-02-SUMMARY.md

<interfaces>
<!-- From types.rs: OuroborosMode enum (Phase 14) -->
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OuroborosMode {
    #[default]
    Day,
    Aion,
}
```

<!-- From types.rs: LiveEngineConfig -->

```rust
pub struct LiveEngineConfig {
    pub symbols: Vec<String>,
    pub thresholds: Vec<u32>,
    pub include_microstructure: bool,
    pub bar_channel_capacity: usize,
    pub reconnection_policy: ReconnectionPolicy,
    pub initial_checkpoints: HashMap<(String, u32), Checkpoint>,
    pub rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
    pub compute_tier2: bool,
    pub compute_tier3: bool,
    pub compute_hurst: Option<bool>,
    pub compute_permutation_entropy: Option<bool>,
    pub gap_detector_seeds: HashMap<String, i64>,
    pub ws_mode: WsMode,
}
```

<!-- From symbol_task.rs: current signature -->

```rust
pub(crate) async fn symbol_task(
    symbol: String,
    mut processors: Vec<(u32, OpenDeviationBarProcessor)>,
    bar_buffer: ConcurrentRingBuffer<CompletedBar>,
    checkpoint_tx: mpsc::Sender<(String, u32, Checkpoint)>,
    _policy: ReconnectionPolicy,
    shutdown: CancellationToken,
    metrics: Arc<LiveEngineMetrics>,
    rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
    forming_txs: Vec<(u32, watch::Sender<Option<FormingBar>>)>,
    gap_event_tx: mpsc::Sender<GapEvent>,
    mut gap_fill_rx: Option<crate::gap::GapFillReceiver>,
    ch_seed_tid: Option<i64>,
    pre_buffered_rx: Option<mpsc::Receiver<AggTrade>>,
)
```

<!-- From puck.rs: current signature -->

```rust
pub(crate) async fn puck_fill(
    symbol: &str,
    from_id: i64,
    to_id: i64,
    processors: &mut Vec<(u32, OpenDeviationBarProcessor)>,
    bar_buffer: &ConcurrentRingBuffer<CompletedBar>,
    metrics: &Arc<LiveEngineMetrics>,
    shutdown: &CancellationToken,
    rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
    forming_tx_map: &HashMap<u32, watch::Sender<Option<FormingBar>>>,
    context: &str,
    committed_floors: &mut HashMap<u32, i64>,
) -> u64
```

<!-- From stream_manager_bindings.rs: PyStreamManager constructor -->

```rust
#[pyo3(signature = (symbols, thresholds, include_microstructure=true, compute_tier2=true, compute_tier3=false, compute_hurst=None, compute_permutation_entropy=None, max_weight_per_minute=6000, safety_margin=0.20, shadow_validation=false, shadow_interval_secs=5, ws_mode="combined"))]
pub fn new(
    symbols: Vec<String>,
    thresholds: Vec<u32>,
    include_microstructure: bool,
    // ... existing params
)
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add symbol_modes to LiveEngineConfig and thread through start() + fill_from_rest()</name>
  <files>
    crates/opendeviationbar-streaming/src/live_engine/types.rs,
    crates/opendeviationbar-streaming/src/live_engine/mod.rs
  </files>
  <read_first>
    crates/opendeviationbar-streaming/src/live_engine/types.rs,
    crates/opendeviationbar-streaming/src/live_engine/mod.rs
  </read_first>
  <action>
1. In `types.rs`, add a new field to `LiveEngineConfig`:
   ```rust
   /// Per-symbol ouroboros mode. Symbols not in this map default to Day.
   pub symbol_modes: HashMap<String, OuroborosMode>,
   ```
   Initialize it to `HashMap::new()` in `LiveEngineConfig::new()`.

1. In `mod.rs` `start()` method (~line 822 where `symbol_task` is spawned):
   - Before the spawn, look up the mode: `let ouroboros_mode = self.config.symbol_modes.get(&symbol).copied().unwrap_or_default();`
   - Pass `ouroboros_mode` as a new parameter to `symbol_task()`.

2. In `mod.rs` `fill_from_rest()` method (~line 346 loop over symbols):
   - Clone `symbol_modes` from config before the loop: `let symbol_modes = self.config.symbol_modes.clone();`
   - Inside the spawned task, look up the mode: `let ouroboros_mode = symbol_modes.get(&symbol).copied().unwrap_or_default();`
   - At line 561 where `OuroborosMode::Day` is hardcoded in the REST chunk processing loop, replace with `ouroboros_mode`.

3. In `mod.rs` test functions (~lines 1341, 1394) that use `OuroborosMode::Day` -- these are existing tests. Leave them as `OuroborosMode::Day` since tests test Day mode.
   </action>

  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && cargo check -p opendeviationbar-streaming 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - `symbol_modes: HashMap<String, OuroborosMode>` field exists on LiveEngineConfig
    - `start()` passes `ouroboros_mode` to `symbol_task()` (grep confirms)
    - `fill_from_rest()` uses per-symbol mode lookup instead of hardcoded Day (line 561 TODO removed)
  </acceptance_criteria>
  <done>LiveEngineConfig carries per-symbol modes; start() and fill_from_rest() use them instead of hardcoded Day</done>
</task>

<task type="auto">
  <name>Task 2: Thread ouroboros_mode through symbol_task and puck_fill</name>
  <files>
    crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs,
    crates/opendeviationbar-streaming/src/live_engine/puck.rs
  </files>
  <read_first>
    crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs,
    crates/opendeviationbar-streaming/src/live_engine/puck.rs
  </read_first>
  <action>
1. In `symbol_task.rs`, add `ouroboros_mode: OuroborosMode` parameter to the `symbol_task` function signature (after `pre_buffered_rx`).

1. Replace all 3 `OuroborosMode::Day` call sites in `symbol_task.rs` with the `ouroboros_mode` parameter:
   - Line 219: junction fill `process_trade_through_processors` call
   - Line 278: WS buffer drain `process_trade_through_processors` call
   - Line 434: main trade loop `process_trade_through_processors` call
     Remove the `// TODO(v7.0 Phase 16)` comments from all 3 sites.

2. In `puck.rs`, add `ouroboros_mode: OuroborosMode` parameter to the `puck_fill` function signature (after `committed_floors`).

3. Replace the hardcoded `OuroborosMode::Day` at line 84 in `puck_fill` with the `ouroboros_mode` parameter. Remove the TODO comment.

4. Update all call sites of `puck_fill` in `symbol_task.rs` (there are 2: junction fill ~line 250 and normal gap fill ~line 400) to pass `ouroboros_mode`.

5. Verify no `OuroborosMode::Day` hardcodes remain in symbol_task.rs or puck.rs (only test files may still have them).
   </action>

  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && cargo check -p opendeviationbar-streaming 2>&1 | tail -5 && echo "---" && grep -n "OuroborosMode::Day" crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs crates/opendeviationbar-streaming/src/live_engine/puck.rs || echo "No hardcoded Day in symbol_task/puck (GOOD)"</automated>
  </verify>
  <acceptance_criteria>
    - `symbol_task` signature includes `ouroboros_mode: OuroborosMode`
    - `puck_fill` signature includes `ouroboros_mode: OuroborosMode`
    - Zero `OuroborosMode::Day` hardcodes in symbol_task.rs and puck.rs
    - All `TODO(v7.0 Phase 16)` comments removed from these files
  </acceptance_criteria>
  <done>All 4 hardcoded OuroborosMode::Day sites in symbol_task.rs and puck.rs replaced with per-symbol mode parameter</done>
</task>

<task type="auto">
  <name>Task 3: PyO3 bindings -- StreamManager accepts symbol_modes dict</name>
  <files>
    src/stream_manager_bindings.rs,
    crates/opendeviationbar-streaming/src/stream_manager.rs
  </files>
  <read_first>
    src/stream_manager_bindings.rs,
    crates/opendeviationbar-streaming/src/stream_manager.rs
  </read_first>
  <action>
1. In `stream_manager.rs`, propagate `symbol_modes` from `StreamManagerConfig::engine_config` to `LiveBarEngine`. The `StreamManager::new()` creates a `LiveBarEngine` from `config.engine_config` -- no change needed since `LiveEngineConfig` already has the field from Task 1. Verify the field flows through.

1. In `stream_manager_bindings.rs`, add `symbol_modes: Option<HashMap<String, String>>` parameter to the `PyStreamManager::new()` constructor:
   - Add to the `#[pyo3(signature = (...))]` attribute: `symbol_modes=None` at the end
   - Add to the Rust function params: `symbol_modes: Option<HashMap<String, String>>`
   - After creating `engine_config`, convert and set:

     ```rust
     if let Some(modes) = symbol_modes {
         use opendeviationbar_streaming::OuroborosMode;
         for (sym, mode_str) in modes {
             let mode = match mode_str.as_str() {
                 "aion" => OuroborosMode::Aion,
                 _ => OuroborosMode::Day,
             };
             engine_config.symbol_modes.insert(sym, mode);
         }
     }
     ```

   This uses the `OuroborosMode` that is already re-exported from `opendeviationbar_streaming`.

2. Run full compilation check including PyO3 crate to verify the entire chain compiles.
   </action>

  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && cargo check -p opendeviationbar-py 2>&1 | tail -5 && cargo nextest run -p opendeviationbar-streaming --lib 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `PyStreamManager::new()` accepts `symbol_modes: Option<HashMap<String, String>>`
    - `OuroborosMode` conversion from string ("aion"/"day") works in PyO3 binding
    - `cargo check -p opendeviationbar-py` passes (entire PyO3 chain compiles)
    - All existing streaming tests pass
  </acceptance_criteria>
  <done>Python can pass symbol_modes={"BTCUSDT": "aion", "EURUSD": "day"} to StreamManager constructor; modes flow through to Rust engine</done>
</task>

</tasks>

<verification>
1. `cargo check -p opendeviationbar-streaming` -- compiles clean
2. `cargo check -p opendeviationbar-py` -- PyO3 crate compiles clean
3. `cargo nextest run -p opendeviationbar-streaming --lib` -- all existing tests pass
4. `grep -rn "OuroborosMode::Day" crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs crates/opendeviationbar-streaming/src/live_engine/puck.rs` -- no matches (all wired)
5. `grep -n "TODO.*Phase 16" crates/opendeviationbar-streaming/src/live_engine/` -- no remaining TODOs
</verification>

<success_criteria>

- All 7 `OuroborosMode::Day` placeholders in production code replaced with per-symbol mode lookups (2 in mod.rs tests are OK to keep as Day)
- PyO3 `StreamManager` constructor accepts optional `symbol_modes` dict
- All existing tests pass unchanged (Day mode is the default when symbol not in modes map)
  </success_criteria>

<output>
After completion, create `.planning/phases/16-sidecar-aion-startup/16-01-SUMMARY.md`
</output>
