# symbol-onboarding: Add or Enable a Symbol in the Registry

ultrathink

Unified checklist from "I want to add XXXUSDT" to "XXXUSDT is live on bigblack with historical backfill running". Covers all 9 registry-gated call sites, the sidecar.env second touch point, known data anomalies, and iterative post-deploy validation.

## When to Use

- `/symbol-onboarding` — Enable a pre-registered (disabled) symbol or add a brand-new one
- After enabling: validate streaming, kintsugi repairs, seeder, and heartbeat in sequence

## Architecture: Why a Skill

Enabling a symbol looks like one edit (`enabled = true`) but gates **10 call sites simultaneously** across sidecar, kintsugi, seeder, heartbeat, and Stathera. Additionally, `deploy/sidecar.env` maintains a **second, independent symbol list** that must stay in sync. Getting either wrong means silent streaming gaps or missed historical backfill.

---

## Symbol States

| State                              | Description                                  | How to Reach               |
| ---------------------------------- | -------------------------------------------- | -------------------------- |
| `enabled = true` in registry       | All 10 call sites active                     | Edit symbols.toml → deploy |
| `enabled = false` in registry      | Gate blocks all processing                   | Default for new symbols    |
| Not in registry at all             | Raises `SymbolNotRegisteredError`            | Add full entry first       |
| In registry but not in sidecar.env | Backfill/kintsugi work; **streaming silent** | Edit sidecar.env too       |

---

## Phase 0: Pre-Flight — Classify the Symbol

```bash
# Check current registry state
python3 -c "
from opendeviationbar.symbol_registry import get_symbol_entry
e = get_symbol_entry('XXXUSDT')
print(f'enabled={e.enabled}  min_threshold={e.min_threshold}  thresholds={e.thresholds}')
print(f'effective_start={e.effective_start}  listing_date={e.listing_date}')
" 2>/dev/null || echo "NOT IN REGISTRY — Phase 1 required"
```

| Result                                   | Path                               |
| ---------------------------------------- | ---------------------------------- |
| `enabled=False` with full metadata       | Phase 2 (single edit)              |
| `enabled=False` with incomplete metadata | Phase 1 (fill gaps), then Phase 2  |
| NOT IN REGISTRY                          | Phase 1 (full entry), then Phase 2 |

---

## Phase 1: New Symbol Entry (skip if already registered)

**File**: `python/opendeviationbar/data/symbols.toml`

### 1.1 Research Checklist

- [ ] **listing_date**: First day on Binance spot (check `data.binance.vision/data/spot/daily/aggTrades/XXXUSDT/`)
- [ ] **effective_start**: First clean day — check for ghost trades on 2018-01-14/15 (BTC/ETH/BNB/LTC all affected)
- [ ] **data_anomalies**: HTTP 404 windows (ATOM: 7 days, XLM: 22 days, BCH: ~2 years), Polars schema crashes on first-day data
- [ ] **asset_class**: `"crypto"` for Binance spot
- [ ] **min_threshold**: Crypto class default is 250 dbps. Raise for meme coins / high-volume pairs (see [Known Gotchas](#known-gotchas))
- [ ] **thresholds**: Which thresholds to compute bars at

### 1.2 Entry Template

```toml
[symbols.XXXUSDT]
enabled = false          # start disabled; flip to true in Phase 2
asset_class = "crypto"
exchange = "binance"
market = "spot"
tier = 1
listing_date = YYYY-MM-DD
effective_start = YYYY-MM-DD    # first clean aggTrade day
reason = "Why effective_start differs from listing_date"
first_clean_date = YYYY-MM-DD
data_anomalies = [
    "YYYY-MM-DD: description of anomaly",
]
min_threshold = 250
thresholds = [250, 500, 750, 1000]
processing_notes = "Bar density, volatility notes, any known issues"
keywords = ["token name", "ticker"]
reference = "https://github.com/terrylica/opendeviationbar-py/issues/79"
```

---

## Phase 2: The Registry Edit

**One line** in `python/opendeviationbar/data/symbols.toml`:

```toml
[symbols.XXXUSDT]
enabled = true    # ← flip this
```

### 2.1 Verify the entry is complete before flipping

| Field             | Required | Notes                               |
| ----------------- | -------- | ----------------------------------- |
| `enabled`         | yes      | true                                |
| `asset_class`     | yes      | `"crypto"` for Binance              |
| `exchange`        | yes      | `"binance"`                         |
| `market`          | yes      | `"spot"` (never futures here)       |
| `listing_date`    | yes      | Binance spot listing                |
| `effective_start` | yes      | First clean day — ghost trade fence |
| `min_threshold`   | yes      | Floor for threshold validation      |
| `thresholds`      | yes      | List of thresholds to compute       |

---

## Phase 3: sidecar.env — Second Touch Point (CRITICAL)

`deploy/sidecar.env` has a **hardcoded** `OPENDEVIATIONBAR_STREAMING_SYMBOLS` list that overrides registry-based symbol discovery for streaming. A symbol not in this list will NOT stream even if `enabled = true`.

```bash
# Check if symbol is already in sidecar.env
grep STREAMING_SYMBOLS deploy/sidecar.env | grep -o XXXUSDT
```

**If NOT present**: Add the symbol to the comma-separated list in `deploy/sidecar.env`:

```ini
OPENDEVIATIONBAR_STREAMING_SYMBOLS=...,XXXUSDT,...
```

Keep the list alphabetically sorted.

**If present**: No action needed (ETHUSDT and 17 others are pre-listed).

> **Design note**: The registry should be the sole SSoT for streaming. The hardcoded sidecar.env list is technical debt — it was pre-populated as a desired-future-state list. A future cleanup would remove `OPENDEVIATIONBAR_STREAMING_SYMBOLS` from sidecar.env entirely and let `config/sidecar.py:27` (`get_registered_symbols(enabled_only=True)`) drive everything. Until then, both touch points must stay in sync.

---

## Phase 4: What Auto-Wires (9 Call Sites)

Setting `enabled = true` in symbols.toml atomically gates all of these:

| Call Site                | File:Line                             | What It Does                                    | Notes                                                        |
| ------------------------ | ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------ |
| Binance tick seeder      | `scripts/tick_cache_seeder.py:442`    | Downloads Parquet ticks from Binance Vision     | Starts on next 5-min timer                                   |
| Exness tick seeder       | `scripts/exness_tick_cache_seeder.py` | Downloads Parquet ticks from Exness archives    | Starts on next 1-hour timer (exchange="exness" symbols only) |
| Heartbeat                | `scripts/health_heartbeat.py`         | Stathera audit includes symbol                  | Next 5-min heartbeat                                         |
| Sidecar config defaults  | `config/sidecar.py:27`                | Adds to streaming defaults (when env var unset) | Overridden by sidecar.env today                              |
| Kintsugi gap filter      | `kintsugi.py:277`                     | Repairs symbol's gaps; skips disabled           | Next kintsugi pass (~60s)                                    |
| Stathera SQL scope       | `clickhouse/stathera_query.py:93`     | ClickHouse queries include symbol               | Next Stathera audit                                          |
| Sidecar startup gap-fill | `sidecar.py:213`                      | Gap-fills from last bar on restart              | Service restart                                              |
| Sidecar live streaming   | `sidecar.py:1547`                     | Produces real-time bars via WebSocket           | Service restart                                              |
| Backfill pair filter     | `backfill.py:244`                     | Gap-fill backfill respects enabled state        | On backfill invocation                                       |
| Symbol gate              | `symbol_registry.py`                  | Blocks API calls for unregistered symbols       | On any API call                                              |

**No code changes needed. Registry + sidecar.env are the complete configuration surface.**

---

## Phase 5: Threshold Resolution Verification

```bash
python3 -c "
from opendeviationbar.symbol_registry import get_thresholds_for_symbol
from opendeviationbar.threshold import get_min_threshold_for_symbol
sym = 'XXXUSDT'
print(f'min_threshold : {get_min_threshold_for_symbol(sym)} dbps')
print(f'thresholds    : {get_thresholds_for_symbol(sym)}')
"
```

Resolution chain (first match wins):

1. Per-symbol env var: `OPENDEVIATIONBAR_MIN_THRESHOLD_XXXUSDT`
2. `symbols.toml` `min_threshold` field ← normal path
3. Asset-class env: `OPENDEVIATIONBAR_CRYPTO_MIN_THRESHOLD` (=250)
4. Hardcoded fallback: 250 dbps

---

## Phase 6: Deploy

`symbols.toml` is bundled as package data in the wheel — bigblack's `.venv` serves the embedded copy. Deploy via full release:

```bash
/odb-ship   # full pipeline: version bump → build → publish → deploy → verify
```

Hotfix path (rsync only) also works for pure data-file changes, skipping Rust rebuild:

```bash
/odb-ship hotfix
```

---

## Phase 7: Post-Deploy Monitoring

Run these in order after services restart. Each has an expected outcome.

### 7.1 Sidecar — symbol appears in active streams (~30s)

```bash
ssh bigblack 'curl -sf http://localhost:8081/health | python3 -m json.tool | grep -A2 XXXUSDT'
```

### 7.2 Kintsugi — begins XXXUSDT gap repairs (~60s)

```bash
ssh bigblack 'journalctl --user -u opendeviationbar-kintsugi -f | grep XXXUSDT'
# Expected: "kintsugi: discovered N shards for XXXUSDT" then repair log lines
```

### 7.3 Seeder — downloads XXXUSDT Parquet ticks (~5 min timer)

```bash
ssh bigblack 'journalctl --user -u opendeviationbar-seeder -n 50 --no-pager | grep XXXUSDT'
```

### 7.4 Heartbeat — XXXUSDT in Stathera audit (~5 min)

Watch the next Telegram heartbeat message. Expect:

- `XXXUSDT` listed in the ClickHouse pairs table
- Stathera gaps: non-zero initially (kintsugi is repairing) — OK
- Freshness: will be stale until first sidecar bar lands — OK

### 7.5 ClickHouse bar coverage (after kintsugi runs for a few hours)

```bash
ssh bigblack 'clickhouse-client --query "
SELECT
    symbol, threshold_decimal_bps,
    min(toDate(open_time_ms/1000, '"'"'UTC'"'"')) AS first_bar,
    max(toDate(close_time_ms/1000, '"'"'UTC'"'"')) AS last_bar,
    count() AS total_bars
FROM opendeviationbar_cache.open_deviation_bars FINAL
WHERE symbol = '"'"'XXXUSDT'"'"'
GROUP BY symbol, threshold_decimal_bps
ORDER BY threshold_decimal_bps"'
```

---

## Phase 8: Historical Coverage Gap Check

### 8.1 Seeder start date vs. listing date

The seeder's `OPENDEVIATIONBAR_SEEDER_HISTORICAL_START=2018-01-01` means any symbol listed before 2018-01-01 has a pre-seeder window.

| Symbol  | Listing    | Effective Start | Pre-2018 Window                                                  | Action |
| ------- | ---------- | --------------- | ---------------------------------------------------------------- | ------ |
| ETHUSDT | 2017-08-17 | 2018-01-16      | 2018-01-16 to 2018-01-01 = none (effective_start > seeder start) | None   |
| BTCUSDT | 2017-08-17 | 2018-01-16      | Same                                                             | None   |
| LTCUSDT | 2017-12-13 | 2018-01-16      | None                                                             | None   |
| BNBUSDT | 2017-11-06 | 2018-01-16      | None                                                             | None   |

> For any new symbol with `effective_start < 2018-01-01`: kintsugi will still repair that window — it just won't have Parquet pre-cached, so repairs use Vision downloads (slower, still correct). No special action required; just expect slower initial repair for that date range.

### 8.2 Clear kintsugi rate-limit entries for new symbol

If kintsugi rate-limiting blocks immediate repairs for a new symbol:

> **WARNING (v7.3+):** `TRUNCATE TABLE kintsugi_log` is **BANNED** -- destroys `result='exchange_gap'` permanent dismiss entries, causing kintsugi to re-attempt 96K+ unhealable gaps.

**Safe cleanup** (preserves exchange gap classifications):

```bash
ssh bigblack 'curl -s "http://localhost:8123/" --data-binary "ALTER TABLE opendeviationbar_cache.kintsugi_log DELETE WHERE result NOT IN ('"'"'exchange_gap'"'"') AND timestamp < now() - INTERVAL 7 DAY SETTINGS mutations_sync=1"'
```

---

## Known Gotchas

| Gotcha                                           | Impact                                                                                                    | Fix                                                                                                       |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `sidecar.env` hardcoded symbol list              | New symbol not in list = silent no streaming                                                              | Add to `OPENDEVIATIONBAR_STREAMING_SYMBOLS` in `deploy/sidecar.env`                                       |
| Ghost trades 2018-01-14/15                       | BTC/ETH/BNB/LTC all have price=0 sentinel records                                                         | `effective_start = 2018-01-16` for all these symbols                                                      |
| Polars schema crash on first-day data            | SHIB (2021-05-10), AVAX (2020-09-22) had mixed int/float                                                  | Fixed in v12.10.4 by `infer_schema_length=None`                                                           |
| Binance Vision 404 windows                       | ATOM (7 days), XLM (22 days), BCH (~2 years)                                                              | Set `effective_start` to first available date; document in `data_anomalies`                               |
| Meme coin bar density                            | SHIB/WIF/PEPE produce 100K+ bars on pump days at low thresholds                                           | min_threshold ≥ 750 (SHIB) or 1000 (WIF/PEPE)                                                             |
| Seeder is symbol-count-aware                     | Seeder iterates `get_registered_symbols(enabled_only=True)` — each added symbol increases seeder run time | Monitor seeder timer stays within 5-min window; `SEEDER_MAX_CONCURRENT_DOWNLOADS=2` caps Vision bandwidth |
| `constants.py TIER1_SYMBOLS`                     | Hardcoded fallback; registry always takes precedence                                                      | No action needed; document only                                                                           |
| Kintsugi log dismissal window                    | 30-day dismissal window if repairs attempted before effective_start data existed                          | Safe DELETE (preserve exchange_gap entries) -- see Phase 8.2. **NEVER TRUNCATE** (v7.3+ ban)              |
| `filter_pairs_by_registry()` in sidecar gap-fill | Disabled symbols silently skipped in startup gap-fill (intended)                                          | Verify symbol is `enabled=true` before debugging gap-fill                                                 |

---

## Validation Checklist (Post-Deploy)

Run through after each new symbol enablement. Mark PASS/FAIL/PENDING.

```
Phase A: Immediate (within 5 min of deploy)
  [ ] sidecar health shows XXXUSDT in streaming pairs
  [ ] journalctl shows kintsugi discovering XXXUSDT shards
  [ ] sidecar.env contains XXXUSDT in STREAMING_SYMBOLS
  [ ] symbols.toml: enabled=true, thresholds complete, effective_start set

Phase B: Short-term (1–2 hours)
  [ ] ClickHouse has XXXUSDT bars for recent dates (today - 7d)
  [ ] Heartbeat Telegram shows XXXUSDT in ClickHouse pairs table
  [ ] Kintsugi repairing in reverse-chronological order (newest shards first)
  [ ] Seeder has XXXUSDT Parquet files in ~/.cache/opendeviationbar/ticks/

Phase C: Long-term (days to weeks)
  [ ] Stathera: tid_delta = 0 across all XXXUSDT day boundaries
  [ ] Full historical coverage from effective_start to today
  [ ] Freshness: XXXUSDT@250 within 1h in heartbeat
  [ ] No persistent Stathera anomalies in heartbeat
```

---

## Refinement Log

Document what worked, what didn't, and changes made after each symbol enablement cycle.

| Date       | Symbol  | Change                                    | Reason                                  |
| ---------- | ------- | ----------------------------------------- | --------------------------------------- |
| 2026-03-13 | ETHUSDT | Skill created                             | First symbol added beyond BTCUSDT       |
|            |         | sidecar.env second touch point documented | blast radius audit found hardcoded list |
|            |         | seeder start date gap analysis            | 2018-01-01 floor vs listing dates       |

---

## Related Skills

| Skill              | Use When                                               |
| ------------------ | ------------------------------------------------------ |
| `/odb-ship`        | Full release + deploy after registry edit              |
| `/odb-ship hotfix` | Deploy registry-only change without Rust rebuild       |
| `/odb-telemetry`   | Read kintsugi repair logs, ClickHouse coverage queries |
| `/odb-validation`  | Validate bar correctness after backfill completes      |
