cockpit_forward reads purely from the DB (forward_summary) and its deps are
ordering-only, so a skipped upstream stalls the whole gate/promotion/allocation
pipeline. Close the two pre-tracker offenders so no *_nav asset raises on a
data-provider failure:
- multistrat_nav: the snapshot_yahoo_closes loop runs OUTSIDE _run_paper_tracker;
a Yahoo outage now logs ERROR and degrades to a no-op {forward_days:0,...}
return (record unchanged) instead of raising and skipping the gate.
- _run_paper_tracker: broaden the caught set to also isolate the databento
provider error base (BentoError/BentoClientError, imported lazily since
databento is an optional extra). Bare Exception is NOT caught — genuine
strategy-math bugs (KeyError/ValueError) still propagate loudly.
With every expected data-provider failure mode isolated, cockpit_forward always
runs (deps kept for ordering). See report for the decouple-approach rationale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
3.4 KiB
Python
74 lines
3.4 KiB
Python
"""C2: no `*_nav` asset may raise on a data-provider/pre-tracker failure — a broken track degrades to a
|
|
stale (unchanged) row so the Dagster step always succeeds and the downstream `cockpit_forward` gate is never
|
|
skipped. These tests pin the two known offenders: multistrat_nav's pre-tracker Yahoo snapshot loop, and
|
|
`_run_paper_tracker`'s isolation of the databento provider error base."""
|
|
from __future__ import annotations
|
|
|
|
|
|
def test_multistrat_nav_survives_yahoo_snapshot_failure(tmp_path, monkeypatch):
|
|
"""multistrat_nav's `snapshot_yahoo_closes` loop runs OUTSIDE `_run_paper_tracker`. A Yahoo fetch failure
|
|
there must DEGRADE to a no-op return (record unchanged), not raise out of the asset and skip the gate."""
|
|
from dagster import build_op_context
|
|
|
|
import fxhnt.adapters.orchestration.assets as assets
|
|
from fxhnt.config import Settings
|
|
|
|
monkeypatch.setattr("fxhnt.config.get_settings",
|
|
lambda: Settings(operational_dsn=f"sqlite:///{tmp_path / 'op.db'}"))
|
|
|
|
class _BoomYahoo:
|
|
def adj_closes(self, sym): # noqa: ANN001, ANN202
|
|
raise ConnectionError("yahoo unreachable")
|
|
monkeypatch.setattr("fxhnt.adapters.data.yahoo_daily.YahooDailyClient", _BoomYahoo)
|
|
|
|
ran = {"tracker": False}
|
|
|
|
def _fake_tracker(*a, **k):
|
|
ran["tracker"] = True
|
|
return {"forward_days": 5, "last_date": "2026-07-13", "reinception": False}
|
|
monkeypatch.setattr(assets, "_run_paper_tracker", _fake_tracker)
|
|
|
|
out = assets.multistrat_nav(build_op_context())
|
|
assert out == {"forward_days": 0, "last_date": None, "reinception": False} # degraded, record unchanged
|
|
assert not ran["tracker"] # pre-tracker snapshot failed → do not recompute; leave the record as-is
|
|
|
|
|
|
def test_run_paper_tracker_isolates_databento_provider_error(tmp_path, monkeypatch):
|
|
"""A databento provider error (BentoError/BentoClientError) raised while building/recomputing a track must
|
|
be isolated (warn + no-op dict), not propagate and skip the gate — the same way transient net errors are."""
|
|
from dagster import build_op_context
|
|
from databento.common.error import BentoClientError
|
|
|
|
import fxhnt.adapters.orchestration.assets as assets
|
|
from fxhnt.config import Settings
|
|
|
|
monkeypatch.setattr("fxhnt.config.get_settings",
|
|
lambda: Settings(operational_dsn=f"sqlite:///{tmp_path / 'op.db'}"))
|
|
|
|
def _boom():
|
|
raise BentoClientError(http_status=422, message="data_end_after_available_end")
|
|
|
|
out = assets._run_paper_tracker(build_op_context(), "vrp_nav", _boom)
|
|
assert out == {"forward_days": 0, "last_date": None, "reinception": False}
|
|
|
|
|
|
def test_run_paper_tracker_lets_real_logic_bugs_stay_loud(tmp_path, monkeypatch):
|
|
"""The isolation is for data-provider/network errors ONLY — a genuine strategy-math bug (KeyError/
|
|
ValueError) must still propagate loudly, never be swallowed by a bare `except Exception`."""
|
|
from dagster import build_op_context
|
|
|
|
import fxhnt.adapters.orchestration.assets as assets
|
|
from fxhnt.config import Settings
|
|
|
|
monkeypatch.setattr("fxhnt.config.get_settings",
|
|
lambda: Settings(operational_dsn=f"sqlite:///{tmp_path / 'op.db'}"))
|
|
|
|
def _bug():
|
|
raise ValueError("genuine strategy math bug")
|
|
|
|
try:
|
|
assets._run_paper_tracker(build_op_context(), "vrp_nav", _bug)
|
|
raise AssertionError("expected the ValueError to propagate loudly")
|
|
except ValueError:
|
|
pass
|