---
phase: 21-universal-write-path-ouroboros-mode
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - python/opendeviationbar/backfill.py
  - python/opendeviationbar/kintsugi/dead_letter.py
  - python/opendeviationbar/orchestration/count_bounded.py
autonomous: true
requirements:
  - WRITE-01
  - WRITE-02
  - WRITE-03
  - WRITE-04

must_haves:
  truths:
    - "backfill_gap() passes per-symbol ouroboros_mode from registry to all 3 internal store_bars_batch calls"
    - "replay_dead_letters() resolves per-symbol mode from registry before writing"
    - "count_bounded.py passes per-symbol ouroboros_mode to both store_bars_batch calls"
    - "Zero store_bars_batch call sites in backfill.py, dead_letter.py, or count_bounded.py rely on get_operational_ouroboros_mode() global fallback"
  artifacts:
    - path: "python/opendeviationbar/backfill.py"
      provides: "Per-symbol ouroboros_mode in all store_bars_batch calls"
      contains: "_get_mode_for_symbol"
    - path: "python/opendeviationbar/kintsugi/dead_letter.py"
      provides: "Per-symbol mode lookup before store_bars_batch"
      contains: "_get_mode_for_symbol"
    - path: "python/opendeviationbar/orchestration/count_bounded.py"
      provides: "Per-symbol ouroboros_mode in store_bars_batch calls"
      contains: "ouroboros_mode"
  key_links:
    - from: "python/opendeviationbar/backfill.py"
      to: "python/opendeviationbar/symbol_registry.py"
      via: "get_symbol_entries() per-symbol lookup"
      pattern: "get_symbol_entries.*ouroboros_mode"
    - from: "python/opendeviationbar/kintsugi/dead_letter.py"
      to: "python/opendeviationbar/kintsugi/_helpers.py"
      via: "_get_mode_for_symbol(symbol)"
      pattern: "_get_mode_for_symbol"
---

<objective>
Wire per-symbol ouroboros_mode from the symbol registry into every store_bars_batch/store_bars_bulk call site in backfill.py, dead_letter.py, and count_bounded.py -- eliminating global get_operational_ouroboros_mode() fallback for crypto symbols.

Purpose: Crypto symbols (ouroboros_mode=aion) currently get the wrong mode when the global env var is set to "day" (or vice versa). Per-symbol lookup from the registry ensures each symbol's bars are written with the correct mode regardless of the global setting.
Output: Three modified files, zero remaining global-fallback call sites for crypto write paths.
</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

@python/opendeviationbar/backfill.py
@python/opendeviationbar/kintsugi/dead_letter.py
@python/opendeviationbar/orchestration/count_bounded.py
@python/opendeviationbar/kintsugi/\_helpers.py

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

From python/opendeviationbar/kintsugi/\_helpers.py (THE PATTERN TO FOLLOW):

```python
def _get_mode_for_symbol(symbol: str) -> str:
    """Get ouroboros_mode for a symbol from the registry.
    Returns "day" as safe default for unknown symbols.
    """
    from opendeviationbar.symbol_registry import get_symbol_entries
    entries = get_symbol_entries()
    entry = entries.get(symbol)
    if entry is None:
        return "day"
    return entry.ouroboros_mode.value
```

From python/opendeviationbar/clickhouse/bulk_operations.py:

```python
def store_bars_batch(
    self,
    symbol: str,
    threshold_decimal_bps: int,
    bars: pl.DataFrame,
    version: str | None = None,
    ouroboros_mode: str | None = None,  # <-- this is the parameter to populate
    *,
    skip_dedup: bool = False,
    overlap_strategy: OverlapStrategy = "warn",
    _skip_lock: bool = False,
) -> int:
```

From python/opendeviationbar/symbol_registry.py:

```python
@dataclass(frozen=True)
class SymbolEntry:
    ...
    ouroboros_mode: OuroborosMode = OuroborosMode.DAY
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Wire per-symbol ouroboros_mode in backfill.py and dead_letter.py</name>
  <files>python/opendeviationbar/backfill.py, python/opendeviationbar/kintsugi/dead_letter.py</files>
  <action>
**backfill.py** (per WRITE-01, WRITE-04):

1. Add a helper function `_get_mode_for_symbol(symbol)` at module level (same pattern as `kintsugi/_helpers.py:_get_mode_for_symbol`):

   ```python
   def _get_mode_for_symbol(symbol: str) -> str:
       from opendeviationbar.symbol_registry import get_symbol_entries
       entries = get_symbol_entries()
       entry = entries.get(symbol)
       if entry is None:
           return "day"
       return entry.ouroboros_mode.value
   ```

2. In `_process_gap_fallback_starttime()` (line ~450):
   - Remove the `get_operational_ouroboros_mode` import and call at lines 451, 455
   - Replace with `mode = _get_mode_for_symbol(symbol)`
   - At line 498 `store_bars_batch(...)`: add `ouroboros_mode=mode` parameter
   - Same replacement at line 631 and 692: replace `get_operational_ouroboros_mode()` calls with `_get_mode_for_symbol(symbol)`

3. In `_fetch_and_process_gap()` (line ~627):
   - Remove `get_operational_ouroboros_mode` import and call at lines 627, 631
   - Replace with `mode = _get_mode_for_symbol(symbol)`
   - At line 651 `store_bars_batch(...)`: add `ouroboros_mode=mode` parameter
   - At line 692 check: replace `get_operational_ouroboros_mode()` with `mode` (already computed)
   - At line 701 `store_bars_batch(...)`: add `ouroboros_mode=mode` parameter

4. In `_get_cached_symbol_threshold_pairs()` (line ~128):
   - This uses `get_operational_ouroboros_mode()` for a CH query filter, which is correct (it queries ALL pairs for the operational mode). Leave this one as-is -- it's a read path, not a write path.

**dead_letter.py** (per WRITE-02):

1. In `replay_dead_letters()` (line ~43):
   - Import `_get_mode_for_symbol` from `._helpers`
   - After extracting `symbol` from the filename (line 33), resolve mode: `mode = _get_mode_for_symbol(symbol)`
   - At line 43 `store_bars_batch(...)`: add `ouroboros_mode=mode` parameter
     </action>

  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && grep -n "get_operational_ouroboros_mode" python/opendeviationbar/backfill.py python/opendeviationbar/kintsugi/dead_letter.py | grep -v "^.*:#" | grep -v "_get_cached_symbol_threshold_pairs" | wc -l | xargs test 0 -eq && echo "PASS: no global fallback in write paths" || echo "FAIL"</automated>
  </verify>
  <done>
  - All 3 store_bars_batch calls in backfill.py pass explicit ouroboros_mode from per-symbol registry lookup
  - dead_letter.py store_bars_batch call passes per-symbol ouroboros_mode
  - The only remaining get_operational_ouroboros_mode() in backfill.py is in _get_cached_symbol_threshold_pairs() (read path, not write path)
  </done>
</task>

<task type="auto">
  <name>Task 2: Wire per-symbol ouroboros_mode in count_bounded.py</name>
  <files>python/opendeviationbar/orchestration/count_bounded.py</files>
  <action>
**count_bounded.py** (per WRITE-03, WRITE-04):

1. In `get_n_open_deviation_bars()` (line ~266):
   - The current code at lines 266-269 resolves ouroboros_mode from `get_operational_ouroboros_mode()` when `ouroboros_mode is None`. This is the public API entry point where the user can pass ouroboros_mode explicitly. For the public API, per-symbol resolution is the right default when user doesn't specify.
   - Replace the fallback logic at lines 266-269:

     ```python
     # Before:
     if ouroboros_mode is None:
         from opendeviationbar.ouroboros import get_operational_ouroboros_mode
         ouroboros_mode = get_operational_ouroboros_mode()

     # After:
     if ouroboros_mode is None:
         from opendeviationbar.symbol_registry import get_symbol_entries
         entries = get_symbol_entries()
         entry = entries.get(symbol)
         ouroboros_mode = entry.ouroboros_mode.value if entry else "day"
     ```

2. At line 522 `cache.store_bars_batch(symbol, threshold, new_bars_pl)`:
   - Add `ouroboros_mode=ouroboros_mode` parameter (the variable is already in scope from step 1)

3. At line 689 (lambda in `_fill_gap_exness`):
   - The lambda `cache_callback=lambda sym, thr, bars: cache.store_bars_batch(sym, thr, pl.from_pandas(bars.reset_index()))` needs ouroboros_mode.
   - Change to: `cache_callback=lambda sym, thr, bars: cache.store_bars_batch(sym, thr, pl.from_pandas(bars.reset_index()), ouroboros_mode=ouroboros_mode)`
   - Note: `ouroboros_mode` is captured in the closure from the enclosing `get_n_open_deviation_bars()` scope. For Exness (forex), this will correctly be "day".
     </action>

  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && grep -n "get_operational_ouroboros_mode" python/opendeviationbar/orchestration/count_bounded.py | wc -l | xargs test 0 -eq && echo "PASS: no global fallback in count_bounded" || echo "FAIL"</automated>
  </verify>
  <done>
  - Both store_bars_batch calls in count_bounded.py pass explicit ouroboros_mode
  - Per-symbol registry lookup replaces global get_operational_ouroboros_mode() fallback
  - ouroboros_mode flows from public API parameter through to all write calls
  </done>
</task>

</tasks>

<verification>
```bash
# Verify WRITE-04: Zero global fallback in write paths
# Expected: only _get_cached_symbol_threshold_pairs in backfill.py (read path)
grep -rn "get_operational_ouroboros_mode" python/opendeviationbar/backfill.py python/opendeviationbar/kintsugi/dead_letter.py python/opendeviationbar/orchestration/count_bounded.py

# Verify all store_bars_batch calls have ouroboros_mode

grep -n "store_bars_batch" python/opendeviationbar/backfill.py python/opendeviationbar/kintsugi/dead_letter.py python/opendeviationbar/orchestration/count_bounded.py

# Verify per-symbol lookup exists

grep -n "\_get_mode_for_symbol\|get_symbol_entries.\*ouroboros_mode" python/opendeviationbar/backfill.py python/opendeviationbar/kintsugi/dead_letter.py python/opendeviationbar/orchestration/count_bounded.py

# Build to verify no syntax errors

cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 python -c "from opendeviationbar.backfill import backfill_gap; from opendeviationbar.kintsugi.dead_letter import replay_dead_letters; from opendeviationbar.orchestration.count_bounded import get_n_open_deviation_bars; print('All imports OK')"

```
</verification>

<success_criteria>
1. `backfill.py`: All 3 `store_bars_batch` calls pass `ouroboros_mode` from per-symbol registry lookup
2. `dead_letter.py`: `store_bars_batch` call passes `ouroboros_mode` from per-symbol registry lookup
3. `count_bounded.py`: Both `store_bars_batch` calls pass `ouroboros_mode` from per-symbol resolution
4. `grep -rn "get_operational_ouroboros_mode" python/opendeviationbar/backfill.py python/opendeviationbar/kintsugi/dead_letter.py python/opendeviationbar/orchestration/count_bounded.py` shows only the read-path call in `_get_cached_symbol_threshold_pairs()`
</success_criteria>

<output>
After completion, create `.planning/phases/21-universal-write-path-ouroboros-mode/21-01-SUMMARY.md`
</output>
```
