fix(tickers): honor the effective date instead of retiring on the mark
active_only tested delisted_on IS NULL, so a symbol dropped out of signals the moment a Form 25 was detected — ten days before Rule 12d2-2 makes the removal effective, while it was demonstrably still trading. A manual future-dated mark behaved the same way. It now compares against the database's own date, so a pending delisting stays live until the day it takes effect. That exposes a second problem the fix would otherwise create. Trading typically stops before the ten-day delay expires, so across that window the symbol is correctly active yet produces no bars — and confirm_delisting returned None for an already-marked row, which would have fired the staleness warning daily for ten days, the exact noise this flow exists to remove. It now reports the known effective date on every path where the delisting is established, so the caller warns only about gaps that are still unexplained. CURRENT_DATE renders identically on postgres and sqlite, and the OR is parenthesized when callers chain further where clauses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,9 +236,11 @@ async def test_a_confirmed_row_is_never_reprobed(session: AsyncSession, monkeypa
|
||||
raise AssertionError("a SEC-confirmed row must not cost another request")
|
||||
|
||||
monkeypatch.setattr(ticker_service, "_sec_client_factory", _explode, raising=False)
|
||||
# Costs no request, and still reports the date so the caller knows this gap
|
||||
# is explained and must not warn about it again.
|
||||
assert await ticker_service.confirm_delisting(
|
||||
session, "EA", last_bar=date(2026, 8, 4), today=date(2026, 9, 1)
|
||||
) is None
|
||||
) == date(2026, 8, 4)
|
||||
|
||||
|
||||
async def test_ohlcv_priority_ordering_skips_delisted(session: AsyncSession):
|
||||
@@ -365,3 +367,50 @@ async def test_prune_keeps_delisted_rows(session: AsyncSession, monkeypatch):
|
||||
assert remaining == ["AAPL", "EA"] # GONE pruned, EA protected
|
||||
assert summary["deleted"] == 1
|
||||
assert summary["kept_delisted"] == ["EA"]
|
||||
|
||||
|
||||
async def test_a_future_effective_date_keeps_the_symbol_live(session: AsyncSession):
|
||||
"""Form 25 is known ten days before removal takes effect. The symbol is still
|
||||
trading in that window and must keep being scanned and ingested."""
|
||||
session.add_all([Ticker(symbol="AAPL"), Ticker(symbol="EA")])
|
||||
await session.commit()
|
||||
await ticker_service.mark_delisted(session, "EA", delisted_on=date(2026, 8, 14))
|
||||
|
||||
def active(as_of: date) -> list[str]:
|
||||
return ticker_service.active_only(select(Ticker.symbol), as_of=as_of)
|
||||
|
||||
before = (await session.execute(active(date(2026, 8, 11)))).scalars().all()
|
||||
on_the_day = (await session.execute(active(date(2026, 8, 14)))).scalars().all()
|
||||
after = (await session.execute(active(date(2026, 8, 15)))).scalars().all()
|
||||
|
||||
assert sorted(before) == ["AAPL", "EA"] # still trading
|
||||
assert sorted(on_the_day) == ["AAPL"] # removal effective
|
||||
assert sorted(after) == ["AAPL"]
|
||||
|
||||
|
||||
async def test_the_pending_window_does_not_re_warn(session: AsyncSession, monkeypatch):
|
||||
"""Between filing and effect the symbol is active but produces no bars. That
|
||||
must not resurrect the daily staleness warning this flow exists to end."""
|
||||
session.add(Ticker(symbol="EA", cik="0000712515"))
|
||||
await session.commit()
|
||||
monkeypatch.setattr(
|
||||
ticker_service,
|
||||
"_sec_client_factory",
|
||||
lambda: _sec_client(_submissions(["25-NSE"], ["2026-08-04"])),
|
||||
raising=False,
|
||||
)
|
||||
first = await ticker_service.confirm_delisting(
|
||||
session, "EA", last_bar=date(2026, 8, 4), today=date(2026, 8, 11)
|
||||
)
|
||||
assert first == date(2026, 8, 14)
|
||||
|
||||
def _explode():
|
||||
raise AssertionError("must not re-probe a confirmed row")
|
||||
|
||||
monkeypatch.setattr(ticker_service, "_sec_client_factory", _explode, raising=False)
|
||||
# Every later run inside the window still reports the delisting, so the
|
||||
# caller keeps emitting "delisted" rather than "no new bars".
|
||||
for day in (date(2026, 8, 12), date(2026, 8, 13)):
|
||||
assert await ticker_service.confirm_delisting(
|
||||
session, "EA", last_bar=date(2026, 8, 4), today=day
|
||||
) == date(2026, 8, 14)
|
||||
|
||||
Reference in New Issue
Block a user