Skip to content

Handling delistings, mergers, and ticker changes

A live trading universe is more turbulent than it looks. Every year, ~5% of listed US issuers undergo a name change, merger, spinoff, or delisting. Your application has to handle each case without crashing or, worse, silently treating one company as another.

This recipe covers the failure modes and the right defensive patterns.

Failure modes

EventAPI behaviorWhat you usually want to do
Active issuer renamed (FBMETA)Old ticker returns 404 not_found after the effective date. New ticker works.Update your symbol map. Migrate historical data under the old ticker.
Merger (ATVIMSFT)Old ticker 404s. The merger is in corporate_actions.Replace the holding with cash on the effective date.
Spinoff (Kellogg → Kellanova + WK Kellogg)New ticker(s) appear. Old ticker continues if surviving entity keeps it.Pro-rate basis across the resulting tickers.
Going private404 not_found permanently.Mark the holding as cash-out at the take-private price.
Delisting from one exchange but still active elsewhereSpecific listing 404s. Other listings still work.Switch to a different listing.
Ticker rename and re-use (rare; happens after a few years)Same ticker may resolve to a different ISIN.Always cross-check ISIN before treating “same ticker” as “same instrument”.

Defensive pattern 1: Use ISIN as the primary key

Ticker is unstable. ISIN is stable. If you are persisting a watchlist or a portfolio, store the ISIN alongside the ticker:

CREATE TABLE holdings (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
ticker TEXT NOT NULL, -- last-known ticker, may go stale
isin TEXT, -- stable across renames
quantity NUMERIC NOT NULL,
cost_basis NUMERIC NOT NULL,
added_at TIMESTAMPTZ DEFAULT now()
);

When a ticker 404s, look up the ISIN via /v1/symbol_search?query=<ISIN> and update the row if you find a new ticker for the same ISIN.

def resolve_or_replace(holding):
try:
r = httpx.get(
"https://api.oneapi.finance/v1/quote",
params={"symbol": holding["ticker"]},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10.0,
)
if r.status_code == 200:
return holding["ticker"], r.json()["quote"]
except httpx.HTTPError:
pass
if holding.get("isin"):
s = httpx.get(
"https://api.oneapi.finance/v1/symbol_search",
params={"query": holding["isin"]},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10.0,
)
s.raise_for_status()
candidates = [c for c in s.json()["symbols"] if c["isin"] == holding["isin"]]
if candidates:
new_ticker = candidates[0]["symbol"]
update_holding_ticker(holding["id"], new_ticker)
return resolve_or_replace({**holding, "ticker": new_ticker})
mark_holding_inactive(holding["id"])
return None, None

Defensive pattern 2: Distinguish “no data” from “no instrument”

A 404 not_found on /v1/quote could mean either:

  • The instrument is unknown (delisted, never existed, typo).
  • The instrument is known but the upstream has no recent quote (rare; can happen for very illiquid names).

Check /v1/profile to disambiguate:

def status_of(symbol: str) -> str:
r = httpx.get(
f"https://api.oneapi.finance/v1/profile",
params={"symbol": symbol},
headers=HEADERS,
timeout=10.0,
)
if r.status_code == 404:
return "delisted_or_unknown"
r.raise_for_status()
return "active"

For a fully built-out solution, future endpoints will return a status field on /v1/stocks rows (active, delisted, unknown) so you can detect this without an extra call.

Defensive pattern 3: Snapshot historical data before it disappears

When a symbol is renamed or merged, the old ticker’s historical /v1/time_series data continues to work for a while, but vendors rotate older symbols out of their public universes after months or years. If you care about historical performance, snapshot the time-series locally when you first add the holding, and resume from where you left off on each update.

This also has the happy side effect of cutting your API calls.

Defensive pattern 4: Handle 502 separately from 404

A 502 upstream_failure is transient — every upstream we tried just happened to be down at this exact moment. Retry with backoff (see errors). Do not delete or mark-inactive a holding based on a 502.

A 404 not_found is structural — we genuinely could not resolve the symbol. Investigate.

See also