---
phase: 02-seeder-day-boundary-backfill
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - scripts/tick_cache_seeder.py
  - tests/test_seeder_day_boundary.py
autonomous: true
requirements: [SEED-01, SEED-02]

must_haves:
  truths:
    - "After midnight, seeder detects yesterday's Parquet max TID < crossover_tid - 1 and backfills the gap"
    - "A complete yesterday file (max TID == crossover_tid - 1) is not re-fetched or modified"
    - "Backfill runs at most once per day transition, not on every 5-min invocation"
  artifacts:
    - path: "scripts/tick_cache_seeder.py"
      provides: "_backfill_yesterday_boundary() function + integration into _seed_symbol()"
      contains: "_backfill_yesterday_boundary"
    - path: "tests/test_seeder_day_boundary.py"
      provides: "Unit tests for day-boundary backfill logic"
      min_lines: 40
  key_links:
    - from: "scripts/tick_cache_seeder.py"
      to: "python/opendeviationbar/crossover.py"
      via: "get_crossover_tid(symbol, date) import"
      pattern: "from opendeviationbar\\.crossover import get_crossover_tid"
    - from: "scripts/tick_cache_seeder.py"
      to: "python/opendeviationbar/storage/parquet.py"
      via: "TickStorage.write_ticks() for appending missing trades"
      pattern: "storage\\.write_ticks"
---

<objective>
Add post-midnight day-boundary backfill to the Binance tick cache seeder so yesterday's Parquet file contains every trade through crossover_tid - 1.

Purpose: The seeder's 5-min timer runs REST fallback for today/yesterday, but the REST window covers "day start to now" -- it misses the ~5 min gap between when the sidecar stops writing ticks and when the seeder's next run catches up. After midnight, yesterday's Parquet file may be missing trailing trades near the boundary. This backfill ensures completeness using the shared crossover TID from Phase 1.

Output: Modified seeder with `_backfill_yesterday_boundary()` function + 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
@.planning/phases/01-crossover-tid-utility/01-01-SUMMARY.md

@scripts/tick_cache_seeder.py
@python/opendeviationbar/crossover.py
@python/opendeviationbar/storage/parquet.py

<interfaces>
<!-- Phase 1 crossover utility (already shipped) -->

From python/opendeviationbar/crossover.py:

```python
def get_crossover_tid(symbol: str, date: datetime.date | None = None) -> int | None:
    """First aggregated trade ID at or after UTC midnight for a date.
    Returns None on failure. 2-tier lookup: CH-seed + API-max."""
```

From python/opendeviationbar/storage/parquet.py:

```python
class TickStorage:
    def _get_parquet_path(self, symbol: str, day_key: str) -> Path: ...
    def write_ticks(self, symbol: str, ticks: pl.DataFrame, *, timestamp_col: str = "timestamp") -> int:
        """Write with dedup on agg_trade_id, merges with existing data."""
```

From python/opendeviationbar/\_core (Rust bindings):

```python
def fetch_aggtrades_by_id(symbol: str, from_id: int, limit: int) -> list[dict]: ...
def fetch_aggtrades_parallel(symbol: str, from_id: int, to_id: int) -> list[dict]: ...
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Day-boundary backfill with TDD</name>
  <files>scripts/tick_cache_seeder.py, tests/test_seeder_day_boundary.py</files>
  <behavior>
    - Test 1: _backfill_yesterday_boundary() detects incomplete file (max TID < crossover_tid - 1) and returns True (backfill needed)
    - Test 2: _backfill_yesterday_boundary() skips complete file (max TID == crossover_tid - 1) and returns False
    - Test 3: _backfill_yesterday_boundary() skips when crossover lookup fails (returns None) and returns False
    - Test 4: _backfill_yesterday_boundary() skips when yesterday Parquet file does not exist (no file to complete) and returns False
    - Test 5: _backfill_yesterday_boundary() writes a state marker after successful backfill so it does not re-run on subsequent 5-min invocations
  </behavior>
  <action>
RED phase:
Create tests/test_seeder_day_boundary.py with 5 unit tests (listed above). Mock `get_crossover_tid`, `fetch_aggtrades_parallel`, `pl.read_parquet`, and `TickStorage.write_ticks`. Use `tmp_path` for state file tests. Tests MUST fail initially.

GREEN phase:
In scripts/tick_cache_seeder.py, add function `_backfill_yesterday_boundary()`:

```python
def _backfill_yesterday_boundary(
    symbol: str,
    storage: TickStorage,
    cache_sym: str,
    state_dir: Path,
) -> bool:
```

Logic:

1. Compute `yesterday = datetime.now(UTC).date() - timedelta(days=1)`
2. Check state marker: `state_dir / f"{cache_sym}_boundary_{yesterday.isoformat()}.done"`. If exists, return False (already ran today).
3. Check yesterday's Parquet file exists via `storage._get_parquet_path(cache_sym, yesterday.isoformat())`. If not exists, return False (nothing to complete -- seeder hasn't cached yesterday yet, will be handled by normal Vision/REST flow).
4. Read yesterday's max agg_trade_id: `pl.read_parquet(parquet_path, columns=["agg_trade_id"])["agg_trade_id"].max()`
5. Call `get_crossover_tid(symbol, today)` where `today = datetime.now(UTC).date()`. If None, log warning and return False.
6. Compute `expected_max = crossover_tid - 1`. If `current_max >= expected_max`, log info "already complete" and write state marker, return False.
7. Fetch missing trades: `fetch_aggtrades_parallel(symbol, current_max + 1, expected_max)`. Convert to polars DataFrame with canonical 7 columns (same pattern as `_rest_fallback_day`).
8. Call `storage.write_ticks(cache_sym, df)` -- existing merge/dedup handles appending.
9. Verify: re-read the Parquet file's max TID. If it equals expected_max, write state marker file and log success. Return True.
10. State marker location: `~/.cache/opendeviationbar/seeder-state/` directory (create if needed).

Then wire into `_seed_symbol()`: call `_backfill_yesterday_boundary()` at the TOP of the function (before `_find_missing_days`), only when `market == "spot"`. This ensures it runs on every 5-min invocation but the state marker prevents repeated work.

Import at function level: `from opendeviationbar.crossover import get_crossover_tid`

REFACTOR: None expected -- the function is straightforward.
</action>
<verify>
<automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_seeder_day_boundary.py -x -v 2>&1 | tail -30</automated>
</verify>
<done> - 5 tests pass covering: incomplete backfill, complete skip, crossover failure skip, missing file skip, state marker dedup
-\_backfill_yesterday_boundary() exists in tick_cache_seeder.py - Function is called from \_seed_symbol() for spot market only - State marker prevents redundant runs within the same UTC day - Yesterday's Parquet max(agg_trade_id) == crossover_tid - 1 after successful backfill (SEED-02)
</done>
</task>

</tasks>

<verification>
```bash
# Unit tests pass
cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_seeder_day_boundary.py -x -v

# Existing seeder tests still pass (no regression)

cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/ -k "seeder" -x -v 2>&1 | tail -20

# Import check -- crossover integration

cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 python -c "from scripts.tick_cache_seeder import \_backfill_yesterday_boundary; print('OK')" 2>&1 || echo "Import test N/A for scripts (expected -- scripts are not packages)"

# Grep confirms key links

grep -n "get_crossover_tid" scripts/tick_cache_seeder.py
grep -n "\_backfill_yesterday_boundary" scripts/tick_cache_seeder.py

```
</verification>

<success_criteria>
1. _backfill_yesterday_boundary() detects incomplete yesterday Parquet and fetches missing trailing trades via REST
2. Complete files are not re-fetched (idempotent)
3. State marker ensures backfill runs at most once per day transition
4. 5 unit tests pass with mocked Binance REST and storage
5. Existing seeder behavior unchanged (no regression)
</success_criteria>

<output>
After completion, create `.planning/phases/02-seeder-day-boundary-backfill/02-01-SUMMARY.md`
</output>
```
