Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/marketdata/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,27 @@ def main(argv=None) -> int:
p.error(f"{flag} modifies the databento ingest. Pass it with "
f"--ingest-databento.")

# Same rule once more, for --symbols. A symbol the registry does not carry
# cannot be fetched by anything, and the providers disagreed about what to do
# with one: databento refuses it (KeyError, "not in the marketdata registry"),
# while yfinance and norgate filtered it out and returned ok=True with wrote=0
# -- exit 0, a wrapper's `if not errorlevel 1` reading it as a good run. So
# `--symbols VIX` against a registry that no longer carries VIX reported
# SUCCESS while fetching nothing, and a symbol retired out of the registry
# took its consumers' fetches down silently, months from whoever did it. The
# check belongs here rather than in each provider: an unscoped `--bars` runs
# every domain, so a futures symbol legitimately resolves to nothing in
# yfinance and vice versa, and a per-provider refusal would fail a run the
# other half handled fine.
if args.symbols:
from .registry import REGISTRY
unknown = [s for s in args.symbols if s not in REGISTRY]
if unknown:
p.error("not in the marketdata registry: " + ", ".join(unknown)
+ ". Nothing would be fetched for these, so this is refused "
"rather than reported as a run that wrote 0 rows. Check the "
"spelling, or add them to registry.yaml.")

if args.final_cutoff:
print(f"note: --final-cutoff {args.final_cutoff} is deprecated and ignored. "
f"The finals gate is data-driven (a newer settled bar than the store "
Expand Down
62 changes: 62 additions & 0 deletions tests/test_cli_symbols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""`marketdata-update --symbols`: a symbol the registry does not carry must be
refused, not filtered out.

The providers used to disagree. databento raised KeyError on an unregistered
symbol; yfinance and norgate dropped it from their target list and returned
ok=True with wrote=0, which `main` turns into exit 0. So `--symbols VIX` against a
registry that no longer carried VIX printed a line nobody reads and reported a
successful run, and a wrapper testing `if not errorlevel 1` went on to sync a
store nothing had been written to. That is the failure mode a retired symbol
hands to its consumers, months after the retirement, looking like a typo.
"""
import pytest

from marketdata import update
from marketdata.providers import norgate as nprov
from marketdata.providers import yfinance as yprov

YF_OK = {"kind": "bars_yahoo", "ok": True, "wrote": 1, "failed": 0}
NG_OK = {"kind": "bars_futures_norgate", "ok": True, "wrote": 1, "failed": 0,
"errors": [], "rows": 10, "newest": "2026-08-20"}


def test_an_unregistered_symbol_is_refused(capsys):
with pytest.raises(SystemExit):
update.main(["--bars", "--symbols", "NOTASYMBOL"])
err = capsys.readouterr().err
assert "not in the marketdata registry" in err
assert "NOTASYMBOL" in err


def test_refused_even_when_other_requested_symbols_are_valid(capsys):
"""A partially satisfiable request is still not the request that was made. If
one of four symbols has been retired out of the registry, the run must say so
rather than quietly fetch three and exit 0."""
with pytest.raises(SystemExit):
update.main(["--bars", "--symbols", "SPY", "NOTASYMBOL"])
err = capsys.readouterr().err
assert "NOTASYMBOL" in err
assert "SPY" not in err # names what is wrong, not what is fine


def test_a_registered_symbol_is_not_refused(monkeypatch):
"""The guard must not over-fire on the case it exists to protect."""
monkeypatch.setattr(yprov, "update", lambda *a, **k: YF_OK)
monkeypatch.setattr(nprov, "update", lambda *a, **k: NG_OK)
assert update.main(["--bars", "--domain", "equities", "--symbols", "VIX"]) == 0


def test_an_equities_symbol_does_not_fail_the_futures_half(monkeypatch):
"""An unscoped --bars runs every domain, so an equities symbol resolves to
nothing in norgate and a futures symbol resolves to nothing in yfinance. That
is why the check lives in main() and not in each provider: a per-provider
refusal would fail a run the other half handled perfectly well."""
monkeypatch.setattr(yprov, "update", lambda *a, **k: YF_OK)
monkeypatch.setattr(nprov, "update", lambda *a, **k: NG_OK)
assert update.main(["--bars", "--symbols", "VIX"]) == 0


def test_no_symbols_means_no_check(monkeypatch):
monkeypatch.setattr(yprov, "update", lambda *a, **k: YF_OK)
monkeypatch.setattr(nprov, "update", lambda *a, **k: NG_OK)
assert update.main(["--bars", "--domain", "equities"]) == 0
Loading