Retiring a symbol meant delete_ticker or bootstrap_universe(prune_missing), both of which cascade through OHLCV, setups and scores. That destroys exactly the history four research documents already apologise for: today's tracked universe projected backward is survivorship-biased, and hard-deleting every delisted name is what causes it. Keeping the rows preserves the option to fix that — it does not fix it, which needs the replay to model a delisting as an exit event. tickers gains delisted_on / delisted_reason (migration 032). NULL means actively traded. The filter is opt-in via ticker_service.active_only rather than folded into a shared getter: the registry and admin views deliberately keep delisted rows so the delisting is visible, and a silent default would undo that. Applied to the live path only — scanner, momentum ranking, scoring, breadth, fundamentals candidates, SEC universe, earnings import, ingestion loops. run_backtest keeps them on purpose. Detection runs off OHLCV staleness, not off the SEC fundamentals import: that importer stalls for days on unrelated Company-Facts gaps and would take detection down with it. On a stale symbol the scheduler asks SEC for a Form 25/25-NSE/15 and retires it only on a hit, so a halt or a rename (SATS->ECHO) keeps the existing warning. The probe waits 3 stale days so a market-data outage cannot turn into one SEC request per symbol per run. Safe to automate because it is reversible: clear_delisted un-retires a false positive, where a delete had already taken the history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46 lines
2.8 KiB
Python
46 lines
2.8 KiB
Python
from datetime import date, datetime
|
|
|
|
from sqlalchemy import Date, String, DateTime
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class Ticker(Base):
|
|
__tablename__ = "tickers"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
symbol: Mapped[str] = mapped_column(String(10), unique=True, nullable=False)
|
|
# Company name (e.g. "Biogen Inc."); backfilled from Alpaca, nullable for
|
|
# symbols Alpaca doesn't know.
|
|
name: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
|
# SEC issuer identity, refreshed by the SEC fundamentals import from
|
|
# company_tickers.json / submissions. The only ticker<->issuer join point;
|
|
# multi-class tickers (GOOG/GOOGL) share these values. Nullable: not every
|
|
# symbol resolves to a CIK (e.g. ADRs, foreign issuers not in SEC data).
|
|
cik: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
|
sic: Mapped[str | None] = mapped_column(String(4), nullable=True)
|
|
sic_description: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
|
# Delisting is recorded, never deleted: the rows carry the price history that
|
|
# makes a backtest less survivorship-biased, and a delete cascades it away.
|
|
# NULL == actively traded. The live signal path filters on this (see
|
|
# ticker_service.active_only); list/admin views keep the row and show it.
|
|
delisted_on: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
|
|
# How we learned: "form_25" (SEC confirmed), "manual" (operator).
|
|
delisted_reason: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.utcnow, nullable=False
|
|
)
|
|
|
|
# Relationships (cascade deletes)
|
|
ohlcv_records = relationship("OHLCVRecord", back_populates="ticker", cascade="all, delete-orphan")
|
|
sentiment_scores = relationship("SentimentScore", back_populates="ticker", cascade="all, delete-orphan")
|
|
fundamental_data = relationship("FundamentalData", back_populates="ticker", cascade="all, delete-orphan")
|
|
sr_levels = relationship("SRLevel", back_populates="ticker", cascade="all, delete-orphan")
|
|
dimension_scores = relationship("DimensionScore", back_populates="ticker", cascade="all, delete-orphan")
|
|
composite_scores = relationship("CompositeScore", back_populates="ticker", cascade="all, delete-orphan")
|
|
trade_setups = relationship("TradeSetup", back_populates="ticker", cascade="all, delete-orphan")
|
|
watchlist_entries = relationship("WatchlistEntry", back_populates="ticker", cascade="all, delete-orphan")
|
|
ingestion_progress = relationship("IngestionProgress", back_populates="ticker", cascade="all, delete-orphan", uselist=False)
|
|
earnings_events = relationship("EarningsEvent", back_populates="ticker", cascade="all, delete-orphan")
|