---
phase: 04-heartbeat-coverage
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - scripts/heartbeat/config.py
  - scripts/heartbeat/checks/yesterday.py
  - tests/test_heartbeat_yesterday_junction.py
autonomous: true
requirements: [HB-01]

must_haves:
  truths:
    - "Yesterday completion check runs for all 17+ Binance spot symbols, not just BTCUSDT and ETHUSDT"
    - "An incomplete yesterday for any Binance spot symbol triggers the existing LOUD Telegram alert path"
    - "Exness forex symbols are excluded (they have no agg_trade_id)"
  artifacts:
    - path: "scripts/heartbeat/config.py"
      provides: "get_heartbeat_symbols() function replacing HEARTBEAT_PRIORITY_SYMBOLS tuple"
      contains: "get_heartbeat_symbols"
    - path: "scripts/heartbeat/checks/yesterday.py"
      provides: "Dynamic symbol iteration via get_heartbeat_symbols()"
      contains: "get_heartbeat_symbols"
    - path: "tests/test_heartbeat_yesterday_junction.py"
      provides: "Tests covering dynamic symbol list and multi-symbol completion"
  key_links:
    - from: "scripts/heartbeat/config.py"
      to: "opendeviationbar.symbol_registry"
      via: "get_registered_symbols + get_symbol_entries filter"
      pattern: "get_registered_symbols|get_symbol_entries"
    - from: "scripts/heartbeat/checks/yesterday.py"
      to: "scripts/heartbeat/config.py"
      via: "import get_heartbeat_symbols"
      pattern: "get_heartbeat_symbols"
---

<objective>
Expand yesterday completion monitoring from 2 hardcoded symbols (BTCUSDT, ETHUSDT) to all streaming Binance spot symbols.

Purpose: Detect day-boundary incompleteness for any monitored symbol, not just the two most liquid ones. Currently 15+ symbols have zero yesterday-completion monitoring.
Output: Dynamic symbol list from registry, filtered to Binance spot only. Existing alert severity (DEGRADED tier) unchanged.
</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

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

From python/opendeviationbar/symbol_registry.py:

```python
def get_registered_symbols(
    *,
    asset_class: str | None = None,
    tier: int | None = None,
    enabled_only: bool = True,
) -> tuple[str, ...]:
    """Get filtered list of registered symbols."""

def get_symbol_entries() -> types.MappingProxyType[str, SymbolEntry]:
    """Get all symbol entries from registry."""

@dataclass(frozen=True)
class SymbolEntry:
    symbol: str
    enabled: bool
    asset_class: str
    exchange: str       # "binance" or "exness"
    market: str         # "spot", "futures-um", "raw_spread"
    ...
```

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."""
```

From scripts/heartbeat/config.py:

```python
HEARTBEAT_PRIORITY_SYMBOLS = ("BTCUSDT", "ETHUSDT")  # TO BE REPLACED
```

From scripts/heartbeat/checks/yesterday.py:

```python
def collect_yesterday_completion(
    client: object,  # clickhouse_connect Client
    results: dict[str, bool | str],
) -> None:
    """Iterates HEARTBEAT_PRIORITY_SYMBOLS, queries crossover TID + CH orphan bar."""
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Replace hardcoded HEARTBEAT_PRIORITY_SYMBOLS with dynamic registry lookup</name>
  <files>scripts/heartbeat/config.py, scripts/heartbeat/checks/yesterday.py, tests/test_heartbeat_yesterday_junction.py</files>
  <read_first>
    - scripts/heartbeat/config.py
    - scripts/heartbeat/checks/yesterday.py
    - tests/test_heartbeat_yesterday_junction.py
    - python/opendeviationbar/symbol_registry.py (lines 162-200 for SymbolEntry, lines 551-588 for get_registered_symbols)
  </read_first>
  <behavior>
    - Test: get_heartbeat_symbols() returns only Binance spot symbols (not Exness forex)
    - Test: get_heartbeat_symbols() returns all enabled Binance spot symbols (17+, not just 2)
    - Test: collect_yesterday_completion with 3+ symbols sets per-symbol result keys and correct aggregate
    - Test: A single symbol mismatch among many makes yesterday_completion=False
    - Test: Registry unavailable (import error) falls back to ("BTCUSDT", "ETHUSDT")
  </behavior>
  <action>
    **In `scripts/heartbeat/config.py`** (per HB-01):

    1. Add a `get_heartbeat_symbols() -> tuple[str, ...]` function that:
       - Imports `get_symbol_entries` from `opendeviationbar.symbol_registry`
       - Filters to `entry.exchange == "binance" and entry.market == "spot" and entry.enabled`
       - Returns sorted tuple of matching symbol names
       - Wraps in try/except: on ImportError or any exception, falls back to `("BTCUSDT", "ETHUSDT")` with a `logger.warning`
       - Uses `@lru_cache(maxsize=1)` since registry is static within a heartbeat run (5 min cycle)
    2. Keep `HEARTBEAT_PRIORITY_SYMBOLS` as a module-level constant for backward compat but add a deprecation comment pointing to `get_heartbeat_symbols()`

    **In `scripts/heartbeat/checks/yesterday.py`**:

    3. Replace `from heartbeat.config import HEARTBEAT_PRIORITY_SYMBOLS` with `from heartbeat.config import get_heartbeat_symbols`
    4. Change the loop from `for symbol in HEARTBEAT_PRIORITY_SYMBOLS:` to `for symbol in get_heartbeat_symbols():`
    5. Update docstring to reflect dynamic symbol list

    **In `tests/test_heartbeat_yesterday_junction.py`**:

    6. Add tests for `get_heartbeat_symbols()`:
       - Mock `get_symbol_entries` to return a mix of Binance spot + Exness forex entries, assert only Binance spot returned
       - Mock import failure, assert fallback to BTCUSDT/ETHUSDT
    7. Update existing `TestYesterdayCompletion` tests:
       - Patch `heartbeat.checks.yesterday.get_heartbeat_symbols` instead of relying on the hardcoded tuple
       - Add a test with 3 symbols where one has a mismatch, asserting `yesterday_completion=False`

    **Important**: Do NOT add a `logger` to `config.py` at module level -- use `logging.getLogger("health-heartbeat")` inline in the function to match existing heartbeat logging pattern.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_heartbeat_yesterday_junction.py -x -v</automated>
  </verify>
  <acceptance_criteria>
    - get_heartbeat_symbols() returns all enabled Binance spot symbols from registry (17+)
    - get_heartbeat_symbols() excludes Exness forex symbols
    - collect_yesterday_completion iterates all Binance spot symbols
    - Fallback to BTCUSDT/ETHUSDT on registry import failure
    - All existing tests still pass (no regressions)
    - yesterday_completion aggregate is False if ANY symbol has a mismatch
  </acceptance_criteria>
  <done>Yesterday completion check dynamically covers all Binance spot symbols from registry. Tests prove filtering, fallback, and multi-symbol aggregation.</done>
</task>

</tasks>

<verification>
1. `uv run --python 3.13 pytest tests/test_heartbeat_yesterday_junction.py -x -v` -- all tests pass
2. `uv run --python 3.13 python -c "from heartbeat.config import get_heartbeat_symbols; print(get_heartbeat_symbols())"` -- prints 17+ symbols (requires PYTHONPATH=scripts)
3. `grep -r "HEARTBEAT_PRIORITY_SYMBOLS" scripts/heartbeat/checks/` -- should return zero matches (no more hardcoded usage in checks/)
</verification>

<success_criteria>

- HB-01 satisfied: yesterday completion check runs for all streaming Binance symbols
- No hardcoded symbol list in the check path (checks/yesterday.py uses get_heartbeat_symbols())
- Fallback ensures heartbeat never crashes if registry is unavailable
- Existing alert severity tier (DEGRADED) unchanged
  </success_criteria>

<output>
After completion, create `.planning/phases/04-heartbeat-coverage/04-01-SUMMARY.md`
</output>
