---
phase: 05-deploy-gap-repair
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - python/opendeviationbar/backfill.py
  - tests/test_committed_fixes.py
autonomous: true
requirements:
  - BFILL-01
  - BFILL-02

must_haves:
  truths:
    - "backfill_gap() computes now_us in microseconds and compares against close_time_us without unit mismatch"
    - "gap_seconds is correct (positive, realistic) when trailing edge exists"
    - "All variable names in backfill_gap() use _us suffix consistently (no _ms remnants)"
    - "Existing tests pass with microsecond mock values"
  artifacts:
    - path: "python/opendeviationbar/backfill.py"
      provides: "Fixed backfill_gap() with microsecond-aligned trailing edge calculation"
      contains: "now_us"
    - path: "tests/test_committed_fixes.py"
      provides: "Updated test mocks using microsecond timestamps"
      contains: "now_us"
  key_links:
    - from: "python/opendeviationbar/backfill.py"
      to: "clickhouse/query_operations.py:get_latest_bar_with_trade_id"
      via: "close_time_us return value compared to now_us"
      pattern: "now_us.*latest_ts"
---

<objective>
Fix the unit mismatch in backfill_gap() where now_ms (milliseconds) is compared against close_time_us (microseconds) from ClickHouse, causing negative gap calculations and all trailing-edge repairs to fail. Also update tests to use microsecond mock values.

Purpose: After Phase 4 renamed CH columns from \_ms to_us, backfill_gap() still computes `now_ms = timestamp * 1000` but the CH query returns microsecond values. The subtraction `now_ms - latest_ts` produces a massive negative number, making gap_seconds negative, which fails the trailing edge guard.

Output: Working backfill_gap() with correct microsecond arithmetic, passing tests.
</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
@tests/test_committed_fixes.py
@python/opendeviationbar/clickhouse/query_operations.py

<interfaces>
<!-- Key return types the executor needs -->

From python/opendeviationbar/clickhouse/query_operations.py:

```python
def get_latest_bar_with_trade_id(self, symbol, threshold_decimal_bps, ouroboros_mode=None) -> tuple[int | None, int | None]:
    """Returns (latest_close_time_us, latest_last_agg_trade_id)"""
    # SQL: SELECT max(close_time_us), maxIf(last_agg_trade_id, ...)

def get_latest_bar_timestamp(self, symbol, threshold_decimal_bps, ouroboros_mode=None) -> int | None:
    """Returns latest close_time_us value (microseconds)"""
    # SQL: SELECT max(close_time_us) as latest_ts
```

Both methods return **microsecond** values after Phase 3-4 column rename.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Fix backfill_gap() ms-to-us unit mismatch and update tests</name>
  <files>python/opendeviationbar/backfill.py, tests/test_committed_fixes.py</files>
  <read_first>
    - python/opendeviationbar/backfill.py (lines 150-320 — backfill_gap function)
    - tests/test_committed_fixes.py (full file — trailing edge tests)
    - python/opendeviationbar/clickhouse/query_operations.py (lines 348-390 — get_latest_bar_with_trade_id return type confirmation)
  </read_first>
  <behavior>
    - Test: backfill_gap() with fresh timestamp (10 min ago) skips correctly — mock returns microsecond close_time_us, now_us computed as microseconds, gap_seconds is ~600 (not negative)
    - Test: backfill_gap() with stale timestamp (2 hours ago) proceeds to fetch — mock returns microsecond value, gap_seconds is ~7200
    - Test: gap_ms variable no longer exists in backfill_gap() (renamed to gap_us)
  </behavior>
  <action>
    **In python/opendeviationbar/backfill.py**, backfill_gap() function:

    1. Line 203: Change `now_ms = int(datetime.now(UTC).timestamp() * 1000)` to `now_us = int(datetime.now(UTC).timestamp() * 1_000_000)` — aligns with close_time_us from CH.

    2. Line 230: Change `gap_ms = now_ms - latest_ts` to `gap_us = now_us - latest_ts` — both sides now microseconds.

    3. Line 231: Change `gap_seconds = gap_ms / 1000.0` to `gap_seconds = gap_us / 1_000_000.0` — correct conversion from microseconds to seconds.

    4. Line 271: Change `start_ms=latest_ts + 1` — this passes to _fetch_and_process_gap which expects milliseconds for REST API calls. Convert: `start_ms=latest_ts // 1000 + 1` (latest_ts is us, REST API needs ms).

    5. Line 272: Change `end_ms=now_ms` to `end_ms=now_us // 1000` — REST API needs milliseconds.

    6. Verify all other references to `now_ms` in this function are updated to `now_us` (there should be none after the above changes).

    **In tests/test_committed_fixes.py**, update the two trailing-edge tests:

    1. `test_trailing_edge_skip_fresh_gap`: Change `now_ms = int(datetime.now(UTC).timestamp() * 1000)` to `now_us = int(datetime.now(UTC).timestamp() * 1_000_000)`. Change `fresh_ts = now_ms - (10 * 60 * 1000)` to `fresh_ts = now_us - (10 * 60 * 1_000_000)` (10 minutes in microseconds).

    2. `test_trailing_edge_proceeds_stale_gap`: Change `now_ms = int(datetime.now(UTC).timestamp() * 1000)` to `now_us = int(datetime.now(UTC).timestamp() * 1_000_000)`. Change `stale_ts = now_ms - (2 * 60 * 60 * 1000)` to `stale_ts = now_us - (2 * 60 * 60 * 1_000_000)` (2 hours in microseconds).

    Both test mocks for `get_latest_bar_with_trade_id.return_value` already receive (ts, tid) — the ts values must now be microseconds to match what CH actually returns.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_committed_fixes.py -x -v 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - backfill_gap() uses `now_us = int(datetime.now(UTC).timestamp() * 1_000_000)` — no `now_ms` variable remains
    - `gap_us = now_us - latest_ts` — both microseconds, subtraction yields positive value
    - `gap_seconds = gap_us / 1_000_000.0` — correct us-to-seconds conversion
    - REST API calls receive millisecond values via `// 1000` conversion
    - Both test_trailing_edge_* tests pass with microsecond mock values
    - `grep -n 'now_ms' python/opendeviationbar/backfill.py` returns zero matches in backfill_gap()
  </acceptance_criteria>
  <done>backfill_gap() trailing edge calculation correctly uses microseconds throughout, gap_seconds is positive and realistic, all tests pass</done>
</task>

</tasks>

<verification>
1. `uv run --python 3.13 pytest tests/test_committed_fixes.py -x -v` — both trailing edge tests pass
2. `grep -n 'now_ms' python/opendeviationbar/backfill.py` — zero matches (all renamed to now_us or removed)
3. `grep -n 'gap_ms' python/opendeviationbar/backfill.py` — zero matches in backfill_gap() (renamed to gap_us)
4. `mise run check-full` — full quality gate passes
</verification>

<success_criteria>

- backfill_gap() computes now_us in microseconds and compares against close_time_us correctly
- gap_seconds yields positive, realistic values (not negative from unit mismatch)
- REST API calls still receive millisecond timestamps (converted from us via // 1000)
- All variable names use \_us consistently within backfill_gap()
- All existing tests pass after mock value updates
  </success_criteria>

<output>
After completion, create `.planning/phases/05-deploy-gap-repair/05-01-SUMMARY.md`
</output>
