ADR-009: tracked_pairs Table as the Canonical Symbol Registry

Status: Accepted

Context

Before this change, the set of tracked market symbols was defined entirely in code. Two separate constant arrays — TRACKED_SYMBOLS (the main app) and YAHOO_FINANCE_SYMBOLS (ohlc-service) — were hardcoded and duplicated across 6+ files. This created several compounding problems:

  • Scatter. Adding or removing a symbol required coordinated changes in multiple files across two independent services.
  • Drift. The two lists were not guaranteed to stay in sync. ETH/USD appeared twice in the backend list — a silent duplicate that went undetected because nothing validated the arrays.
  • No metadata. The arrays held symbols as plain strings. Per-symbol metadata (price format, enabled/disabled state) had to be inferred from the symbol string or held in separate parallel structures.
  • Redeploys required. Any change to the tracked set meant a code change, PR, and redeploy of one or both services.

Options considered:

OptionProsCons
Keep hardcoded arrays (status quo)Zero infrastructure overheadScatter, drift, duplicates; redeploy to change anything
Environment variable listNo extra schema; easy to override per-environmentNot UI-manageable; still requires a redeploy to change; no per-symbol metadata
Shared config file on diskSimple read pathCouples both services at the filesystem level; not viable across containers; no UI
tracked_pairs DB table (chosen)Single source of truth; UI-manageable; rich metadata; no redeploysohlc-service gains a soft startup dependency on mi-casa’s HTTP API

Decision

Replace all hardcoded symbol arrays with a single tracked_pairs PostgreSQL table in the main app database. This table is the sole source of truth for which symbols are tracked and how each one behaves.

Schema

tracked_pairs (
  symbol       TEXT PRIMARY KEY,      -- e.g. "EUR/USD"
  label        TEXT NOT NULL,         -- display name, e.g. "Euro / US Dollar"
  price_format TEXT NOT NULL,         -- "forex5" | "forex4" | "forex3" | "price2" | "price0"
  enabled      BOOLEAN NOT NULL DEFAULT true,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
)

source was removed later once cTrader became the sole supported market data provider.

All 13 original pairs are seeded idempotently by backend/db/trackedPairsSeed.ts.

Admin CRUD API (mi-casa backend)

MethodRouteDescription
GET/api/admin/tracked-pairsList all pairs
POST/api/admin/tracked-pairsCreate a new pair
PATCH/api/admin/tracked-pairs/:symbolUpdate label, format, or enabled state
DELETE/api/admin/tracked-pairs/:symbolRemove a pair

All routes require admin authentication.

Admin UI

A new AdminTrackedPairsPanel React component lets admin users create, edit, enable/disable, and delete pairs from the web UI with no code changes or redeploys.

getSymbols() endpoint

The public GET /api/symbols endpoint now calls trackedPairsRepository.getAllEnabled() instead of returning a hardcoded array.

Internal API for ohlc-service

A new protected endpoint GET /api/internal/tracked-pairs is added to mi-casa. It is guarded by the existing OHLC_SERVICE_API_KEY shared secret (passed as X-API-Key). ohlc-service calls this endpoint at startup via trackedSymbolsCache.ts and caches the result in memory.

ohlc-service graceful degradation

If mi-casa is unreachable at startup, trackedSymbolsCache stays empty and ohlc-service starts anyway. Symbol-dependent operations degrade gracefully until the cache is populated on a successful retry.

Consequences

Positive

  • Adding, removing, or reconfiguring a tracked pair is a single DB row change, performable from the admin UI — no code change or redeploy required in either service
  • Per-symbol metadata (price_format, enabled) is now first-class, validated by the schema, and impossible to desynchronise between services
  • Silent bugs like the duplicate ETH/USD entry are structurally prevented by the primary key constraint
  • The TRACKED_SYMBOLS and YAHOO_FINANCE_SYMBOLS constants are removed from all codebases

Negative / Tradeoffs

  • ohlc-service now has a soft startup dependency on mi-casa’s HTTP API; if mi-casa is down when ohlc-service starts, symbol-driven operations are unavailable until the cache warms
  • The tracked_pairs table must be kept in sync with whatever ohlc-service actually supports — unsupported symbols will still fail until cTrader exposes them and the downstream services can consume them
  • GET /api/internal/tracked-pairs is a new internal surface area guarded only by the static pre-shared key (same threat model as the existing ohlc-service auth; see ADR-008)
  • #68 — Replace hardcoded symbol lists with tracked_pairs DB table
  • ADR-008 — introduced the service-to-service API key pattern reused here