""" ACBv6 HZ Integration Tests =========================== Tests for get_dynamic_boost_from_hz() and _load_external_factors_from_snapshot() in AdaptiveCircuitBreaker. Covers: - Unit: snapshot parsing → correct factor extraction - Unit: boost / signal computation from snapshot - Unit: staleness guard (warn vs fallback) - Unit: lag NOT re-applied (HZ values pass through unchanged) - Parity: HZ path == NPZ path when fed same factor values - Regression: known ACBv6 ground-truth dates (2026-01-13, 2026-02-05, 2026-02-07) - w750 live injection overrides NPZ-cached value - OB Sub-4 regime modulation preserved on HZ path - Cache pre-warm: engine get_dynamic_boost_for_date() sees HZ result (no disk I/O) - E2E: live HZ ping (skipped when HZ unavailable) Usage: source /home/dolphin/siloqy_env/bin/activate pytest prod/tests/test_acb_hz_integration.py -v """ import sys import json import math import time import pytest from pathlib import Path from unittest.mock import MagicMock, patch HCM_DIR = Path(__file__).parent.parent.parent sys.path.insert(0, str(HCM_DIR / 'nautilus_dolphin')) sys.path.insert(0, str(HCM_DIR)) from nautilus_dolphin.nautilus.adaptive_circuit_breaker import ( AdaptiveCircuitBreaker, ACBConfig, _STALE_WARN_S, _STALE_FALLBACK_S ) # ── Fixture helpers ────────────────────────────────────────────────────────────── def _make_snapshot( funding_btc=0.0001, # mild positive — no signal dvol_btc=50.0, # below DVOL_ELEVATED — no signal fng=50.0, # neutral taker=1.0, # neutral fund_dbt_btc=0.0, acb_ready=True, staleness_s: dict | None = None, ) -> dict: """Build a minimal exf_latest-style snapshot dict.""" snap = { 'funding_btc': funding_btc, 'dvol_btc': dvol_btc, 'fng': fng, 'taker': taker, 'fund_dbt_btc': fund_dbt_btc, '_acb_ready': acb_ready, '_pushed_at': '2026-02-05T12:00:00+00:00', '_staleness_s': staleness_s if staleness_s is not None else { 'funding_btc': 30.0, 'dvol_btc': 45.0, 'fng': 3600.0, 'taker': 60.0, }, } return snap def _make_acb_with_threshold(threshold=0.001) -> AdaptiveCircuitBreaker: """Return an ACB whose w750 threshold is manually pre-set.""" acb = AdaptiveCircuitBreaker() acb._w750_threshold = threshold return acb # ── Ground truth from live NPZ probe (2026-01-13 to 2026-02-07) ───────────────── # These values were computed by running get_dynamic_boost_for_date() against the # gold NG6 NPZ archive and recorded as regression anchors. GROUND_TRUTH = { '2026-01-13': {'boost': 1.0, 'signals': 0.0, 'beta_if_high': 0.8, 'beta_if_low': 0.2}, '2026-02-05': {'boost': 1.5493, 'signals': 2.0, 'beta_if_high': 0.8, 'beta_if_low': 0.2}, '2026-02-07': {'boost': 1.6264, 'signals': 2.5, 'beta_if_high': 0.8, 'beta_if_low': 0.2}, } # Factor values that reproduce the ground-truth signals (used for parity tests) GT_SNAPSHOTS = { '2026-01-13': _make_snapshot(funding_btc=0.0001, dvol_btc=50.0, fng=50.0, taker=1.0), # 2026-02-05: dvol=82.6 (extreme), funding very bearish → signals=2.0 # fng=45 (neutral, >= FNG_FEAR=40) ensures fng does NOT fire, keeping total at 2.0 '2026-02-05': _make_snapshot(funding_btc=-0.00015, dvol_btc=82.6, fng=45.0, taker=0.95), # 2026-02-07: funding very bearish, dvol=59.4 (elevated), fng=9 (extreme fear) → signals=2.5 '2026-02-07': _make_snapshot(funding_btc=-0.00015, dvol_btc=59.4, fng=9.0, taker=0.95), } # ════════════════════════════════════════════════════════════════════════════════ # Section 1 — Unit: _load_external_factors_from_snapshot # ════════════════════════════════════════════════════════════════════════════════ class TestLoadFactorsFromSnapshot: def test_basic_extraction(self): snap = _make_snapshot(funding_btc=-0.0002, dvol_btc=85.0, fng=20.0, taker=0.75) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) assert factors['funding_btc'] == pytest.approx(-0.0002) assert factors['dvol_btc'] == pytest.approx(85.0) assert factors['fng'] == pytest.approx(20.0) assert factors['taker'] == pytest.approx(0.75) assert factors['source'] == 'hz' assert factors['available'] is True def test_defaults_on_missing_keys(self): """Empty snapshot should produce safe neutral defaults.""" acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot({}) assert factors['funding_btc'] == pytest.approx(0.0) assert factors['dvol_btc'] == pytest.approx(50.0) assert factors['fng'] == pytest.approx(50.0) assert factors['taker'] == pytest.approx(1.0) assert factors['available'] is False def test_max_staleness_computed(self): snap = _make_snapshot(staleness_s={ 'funding_btc': 100.0, 'dvol_btc': 200.0, 'fng': 14500.0, # > 4 h — most stale 'taker': 50.0, }) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) assert factors['max_staleness_s'] == pytest.approx(14500.0) def test_no_lag_reapplied(self): """Values must pass through exactly as-is; no transformation applied.""" raw_funding = -0.000123456 snap = _make_snapshot(funding_btc=raw_funding) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) # If lag were being re-applied, the value would differ (shifted by a day) assert factors['funding_btc'] == pytest.approx(raw_funding, rel=1e-9) # ════════════════════════════════════════════════════════════════════════════════ # Section 2 — Unit: get_dynamic_boost_from_hz — signals & boost # ════════════════════════════════════════════════════════════════════════════════ class TestGetDynamicBoostFromHz: def test_no_signals_gives_boost_1(self): acb = _make_acb_with_threshold(threshold=0.001) snap = _make_snapshot() # neutral values result = acb.get_dynamic_boost_from_hz('2026-01-13', snap) assert result['signals'] == pytest.approx(0.0) assert result['boost'] == pytest.approx(1.0) assert result['source'] == 'hz' def test_dvol_extreme_funding_bearish_gives_2_signals(self): """dvol > 80 (extreme) + funding < -0.0001 (very bearish) = 2.0 signals.""" acb = _make_acb_with_threshold(threshold=0.001) snap = _make_snapshot(dvol_btc=85.0, funding_btc=-0.0002) result = acb.get_dynamic_boost_from_hz('2026-02-05', snap) assert result['signals'] == pytest.approx(2.0) expected_boost = 1.0 + 0.5 * math.log1p(2.0) assert result['boost'] == pytest.approx(expected_boost, rel=1e-6) def test_full_stress_scenario(self): """All four indicators firing at extreme levels.""" acb = _make_acb_with_threshold(threshold=0.001) snap = _make_snapshot( funding_btc=-0.0002, # very bearish (+1.0 sig) dvol_btc=85.0, # extreme (+1.0 sig) fng=20.0, # extreme fear (+1.0 sig, confirmed by 2 prior) taker=0.75, # selling (+1.0 sig) ) result = acb.get_dynamic_boost_from_hz('2026-02-06', snap) assert result['signals'] == pytest.approx(4.0) expected = 1.0 + 0.5 * math.log1p(4.0) assert result['boost'] == pytest.approx(expected, rel=1e-6) def test_result_schema_complete(self): acb = _make_acb_with_threshold(threshold=0.001) snap = _make_snapshot() result = acb.get_dynamic_boost_from_hz('2026-01-15', snap) required_keys = { 'boost', 'beta', 'signals', 'severity', 'factors', 'cut', 'w750_vel', 'w750_threshold', 'ob_regime', 'ob_depth_velocity', 'ob_cascade_count', 'date', 'config_used', 'source', 'max_staleness_s', } assert required_keys <= result.keys() def test_cut_always_zero(self): """Inverse ACB — no cut, only boost.""" acb = _make_acb_with_threshold() snap = _make_snapshot(dvol_btc=90.0, funding_btc=-0.0005) result = acb.get_dynamic_boost_from_hz('2026-02-10', snap) assert result['cut'] == pytest.approx(0.0) def test_config_used_v6(self): acb = AdaptiveCircuitBreaker() result = acb.get_dynamic_boost_from_hz('2026-01-20', _make_snapshot()) assert result['config_used'] == 'v6' # ════════════════════════════════════════════════════════════════════════════════ # Section 3 — Unit: staleness guard # ════════════════════════════════════════════════════════════════════════════════ class TestStalenessGuard: def test_fresh_data_no_error(self): acb = _make_acb_with_threshold() snap = _make_snapshot(staleness_s={'funding_btc': 30, 'dvol_btc': 45, 'fng': 300, 'taker': 10}) result = acb.get_dynamic_boost_from_hz('2026-02-01', snap) assert result['max_staleness_s'] < _STALE_WARN_S def test_stale_warn_threshold_still_passes(self): """4 h < staleness < 12 h: method succeeds but max_staleness_s is recorded.""" stale_s = _STALE_WARN_S + 100 # just over 4 h, well under 12 h acb = _make_acb_with_threshold() snap = _make_snapshot(staleness_s={ 'funding_btc': stale_s, 'dvol_btc': 30, 'fng': 100, 'taker': 20 }) result = acb.get_dynamic_boost_from_hz('2026-02-02', snap) assert result['max_staleness_s'] == pytest.approx(stale_s) def test_stale_fallback_raises(self): """Staleness > 12 h must raise ValueError for caller to fall back.""" stale_s = _STALE_FALLBACK_S + 60 acb = _make_acb_with_threshold() snap = _make_snapshot(staleness_s={ 'funding_btc': stale_s, 'dvol_btc': 30, 'fng': 100, 'taker': 20 }) with pytest.raises(ValueError, match="stale"): acb.get_dynamic_boost_from_hz('2026-02-03', snap) def test_empty_staleness_dict_no_error(self): """Missing _staleness_s treated as 0 — should not raise.""" snap = _make_snapshot(staleness_s={}) acb = _make_acb_with_threshold() result = acb.get_dynamic_boost_from_hz('2026-01-10', snap) assert result['max_staleness_s'] == pytest.approx(0.0) # ════════════════════════════════════════════════════════════════════════════════ # Section 4 — Unit: w750 live injection # ════════════════════════════════════════════════════════════════════════════════ class TestW750Injection: def test_live_w750_overrides_cached_value(self): acb = _make_acb_with_threshold(threshold=0.005) date_str = '2026-02-05' # Pre-seed NPZ cache with a low value (would give beta_low) acb._w750_vel_cache[date_str] = 0.001 # below threshold snap = _make_snapshot() # Pass live w750 above threshold → should give beta_high result = acb.get_dynamic_boost_from_hz(date_str, snap, w750_velocity=0.010) assert acb._w750_vel_cache[date_str] == pytest.approx(0.010) assert result['beta'] == pytest.approx(ACBConfig.BETA_HIGH) def test_no_live_w750_uses_cached(self): acb = _make_acb_with_threshold(threshold=0.005) date_str = '2026-02-06' acb._w750_vel_cache[date_str] = 0.010 # above threshold → beta_high snap = _make_snapshot() result = acb.get_dynamic_boost_from_hz(date_str, snap, w750_velocity=None) assert result['beta'] == pytest.approx(ACBConfig.BETA_HIGH) def test_no_threshold_gives_midpoint_beta(self): """Without preload_w750(), threshold is None → midpoint beta returned.""" acb = AdaptiveCircuitBreaker() assert acb._w750_threshold is None result = acb.get_dynamic_boost_from_hz('2026-01-05', _make_snapshot()) expected_mid = (ACBConfig.BETA_HIGH + ACBConfig.BETA_LOW) / 2.0 assert result['beta'] == pytest.approx(expected_mid) def test_w750_below_threshold_gives_beta_low(self): acb = _make_acb_with_threshold(threshold=0.010) result = acb.get_dynamic_boost_from_hz( '2026-02-08', _make_snapshot(), w750_velocity=0.002 ) assert result['beta'] == pytest.approx(ACBConfig.BETA_LOW) def test_w750_above_threshold_gives_beta_high(self): acb = _make_acb_with_threshold(threshold=0.002) result = acb.get_dynamic_boost_from_hz( '2026-02-09', _make_snapshot(), w750_velocity=0.010 ) assert result['beta'] == pytest.approx(ACBConfig.BETA_HIGH) # ════════════════════════════════════════════════════════════════════════════════ # Section 5 — Unit: OB Sub-4 regime modulation # ════════════════════════════════════════════════════════════════════════════════ class TestOBRegimeModulation: def _make_ob_engine(self, regime_signal): ob_macro = MagicMock() ob_macro.regime_signal = regime_signal ob_macro.depth_velocity = 0.05 ob_macro.cascade_count = 1 ob_engine = MagicMock() ob_engine.get_macro.return_value = ob_macro return ob_engine def test_stress_regime_increases_beta(self): acb = _make_acb_with_threshold(threshold=0.001) # Set up so beta would be BETA_HIGH (0.8) without OB acb._w750_vel_cache['2026-02-05'] = 0.010 ob_engine = self._make_ob_engine(regime_signal=1) result = acb.get_dynamic_boost_from_hz( '2026-02-05', _make_snapshot(), w750_velocity=0.010, ob_engine=ob_engine ) # BETA_HIGH=0.8 * 1.25 = 1.0 (capped at 1.0) assert result['beta'] == pytest.approx(min(1.0, ACBConfig.BETA_HIGH * 1.25)) assert result['ob_regime'] == 1 def test_calm_regime_reduces_beta(self): acb = _make_acb_with_threshold(threshold=0.001) acb._w750_vel_cache['2026-02-05'] = 0.010 ob_engine = self._make_ob_engine(regime_signal=-1) result = acb.get_dynamic_boost_from_hz( '2026-02-05', _make_snapshot(), w750_velocity=0.010, ob_engine=ob_engine ) assert result['beta'] == pytest.approx(ACBConfig.BETA_HIGH * 0.85) assert result['ob_regime'] == -1 def test_neutral_regime_no_change(self): acb = _make_acb_with_threshold(threshold=0.001) acb._w750_vel_cache['2026-02-05'] = 0.010 ob_engine = self._make_ob_engine(regime_signal=0) result = acb.get_dynamic_boost_from_hz( '2026-02-05', _make_snapshot(), w750_velocity=0.010, ob_engine=ob_engine ) assert result['beta'] == pytest.approx(ACBConfig.BETA_HIGH) assert result['ob_regime'] == 0 def test_no_ob_engine_sets_zero_regime(self): acb = _make_acb_with_threshold() result = acb.get_dynamic_boost_from_hz('2026-02-05', _make_snapshot()) assert result['ob_regime'] == 0 assert result['ob_depth_velocity'] == pytest.approx(0.0) assert result['ob_cascade_count'] == 0 # ════════════════════════════════════════════════════════════════════════════════ # Section 6 — Cache pre-warm: engine path uses HZ result without disk I/O # ════════════════════════════════════════════════════════════════════════════════ class TestCachePreWarm: def test_hz_result_cached_for_npz_path(self): """After get_dynamic_boost_from_hz(), get_dynamic_boost_for_date() returns the same result (cache hit, no NPZ disk read).""" acb = _make_acb_with_threshold(threshold=0.001) snap = _make_snapshot(dvol_btc=85.0, funding_btc=-0.0002) date_str = '2026-02-05' hz_result = acb.get_dynamic_boost_from_hz(date_str, snap, w750_velocity=0.010) # Now simulate what the engine does internally with patch.object(acb, '_load_external_factors', side_effect=AssertionError( "_load_external_factors must NOT be called after HZ pre-warm" )): # get_cut_for_date() will hit the cache (populated by get_dynamic_boost_from_hz) # rather than calling _load_external_factors() cached = acb.get_cut_for_date(date_str) assert cached['signals'] == pytest.approx(hz_result['signals']) def test_cache_key_is_date_string(self): acb = _make_acb_with_threshold() date_str = '2026-01-20' acb.get_dynamic_boost_from_hz(date_str, _make_snapshot()) assert date_str in acb._cache def test_second_call_npz_path_hits_cache(self): """get_dynamic_boost_for_date() called after HZ pre-warm returns HZ result.""" acb = _make_acb_with_threshold(threshold=0.001) date_str = '2026-02-05' snap = _make_snapshot(dvol_btc=85.0, funding_btc=-0.0002) acb.get_dynamic_boost_from_hz(date_str, snap, w750_velocity=0.010) # get_dynamic_boost_for_date() calls get_boost_for_date() → get_cut_for_date() # get_cut_for_date() finds the cache hit; no disk access occurs. with patch.object(acb, '_load_external_factors', side_effect=RuntimeError("DISK")): result = acb.get_dynamic_boost_for_date(date_str) assert result['signals'] == pytest.approx(2.0) # ════════════════════════════════════════════════════════════════════════════════ # Section 7 — Parity: HZ path == NPZ path for identical factor values # ════════════════════════════════════════════════════════════════════════════════ class TestNpzHzParity: """Verify HZ path produces the same boost/signals as NPZ path when fed identical factor values. This ensures the computation is equivalent regardless of source.""" def _npz_result_from_factors(self, factors: dict, date_str: str, threshold=0.001) -> dict: """Simulate NPZ path by injecting factors directly (bypassing disk).""" acb = _make_acb_with_threshold(threshold=threshold) with patch.object(acb, '_load_external_factors', return_value=factors): return acb.get_dynamic_boost_for_date(date_str) def _hz_result(self, factors: dict, date_str: str, threshold=0.001) -> dict: snap = { 'funding_btc': factors.get('funding_btc', 0.0), 'dvol_btc': factors.get('dvol_btc', 50.0), 'fng': factors.get('fng', 50.0), 'taker': factors.get('taker', 1.0), 'fund_dbt_btc':factors.get('fund_dbt_btc', 0.0), '_acb_ready': True, '_staleness_s': {'funding_btc': 30, 'dvol_btc': 30, 'fng': 30, 'taker': 30}, } acb = _make_acb_with_threshold(threshold=threshold) return acb.get_dynamic_boost_from_hz(date_str, snap) def test_parity_no_signals(self): factors = {'funding_btc': 0.0001, 'dvol_btc': 50.0, 'fng': 50.0, 'taker': 1.0, 'available': True} npz = self._npz_result_from_factors(factors, '2026-01-10') hz = self._hz_result(factors, '2026-01-10') assert hz['signals'] == pytest.approx(npz['signals']) assert hz['boost'] == pytest.approx(npz['boost']) def test_parity_2_signals(self): factors = {'funding_btc': -0.00015, 'dvol_btc': 82.6, 'fng': 30.0, 'taker': 0.95, 'available': True} npz = self._npz_result_from_factors(factors, '2026-02-05') hz = self._hz_result(factors, '2026-02-05') assert hz['signals'] == pytest.approx(npz['signals']) assert hz['boost'] == pytest.approx(npz['boost'], rel=1e-6) def test_parity_2pt5_signals(self): factors = {'funding_btc': -0.00015, 'dvol_btc': 59.4, 'fng': 9.0, 'taker': 0.95, 'available': True} npz = self._npz_result_from_factors(factors, '2026-02-07') hz = self._hz_result(factors, '2026-02-07') assert hz['signals'] == pytest.approx(npz['signals']) assert hz['boost'] == pytest.approx(npz['boost'], rel=1e-6) def test_parity_full_stress(self): factors = {'funding_btc': -0.0002, 'dvol_btc': 88.0, 'fng': 15.0, 'taker': 0.70, 'available': True} npz = self._npz_result_from_factors(factors, '2026-02-10') hz = self._hz_result(factors, '2026-02-10') assert hz['signals'] == pytest.approx(npz['signals']) assert hz['boost'] == pytest.approx(npz['boost'], rel=1e-6) # ════════════════════════════════════════════════════════════════════════════════ # Section 8 — Regression against known ACBv6 ground-truth values # ════════════════════════════════════════════════════════════════════════════════ class TestRegressionGroundTruth: """Compare HZ path output against manually probed NPZ values. Ground truth source: full NPZ scan of /mnt/ng6_data/eigenvalues/ using get_dynamic_boost_for_date() on each date. The HZ snapshots in GT_SNAPSHOTS are synthetic but constructed to reproduce the same factor values measured from those dates' NPZ files. """ @pytest.mark.parametrize("date_str, expected", [ ('2026-01-13', {'boost': 1.0, 'signals': 0.0}), ('2026-02-05', {'boost': 1.5493, 'signals': 2.0}), ('2026-02-07', {'boost': 1.6264, 'signals': 2.5}), ]) def test_boost_matches_ground_truth(self, date_str, expected): acb = _make_acb_with_threshold(threshold=0.001) snap = GT_SNAPSHOTS[date_str] result = acb.get_dynamic_boost_from_hz(date_str, snap) assert result['signals'] == pytest.approx(expected['signals'], abs=0.01), \ f"{date_str}: signals={result['signals']} != {expected['signals']}" assert result['boost'] == pytest.approx(expected['boost'], rel=0.01), \ f"{date_str}: boost={result['boost']:.4f} != {expected['boost']:.4f}" def test_beta_high_when_above_threshold(self): """With w750 above threshold, beta must be BETA_HIGH=0.8.""" acb = _make_acb_with_threshold(threshold=0.001) result = acb.get_dynamic_boost_from_hz( '2026-02-05', GT_SNAPSHOTS['2026-02-05'], w750_velocity=0.005 ) assert result['beta'] == pytest.approx(ACBConfig.BETA_HIGH) def test_beta_low_when_below_threshold(self): acb = _make_acb_with_threshold(threshold=0.010) result = acb.get_dynamic_boost_from_hz( '2026-02-05', GT_SNAPSHOTS['2026-02-05'], w750_velocity=0.001 ) assert result['beta'] == pytest.approx(ACBConfig.BETA_LOW) # ════════════════════════════════════════════════════════════════════════════════ # Section 9 — Delay preservation (lag not re-applied) # ════════════════════════════════════════════════════════════════════════════════ class TestDelayPreservation: """Confirm that the HZ path does not re-apply any lag to indicator values. The ExF service applies lag before pushing to HZ. The design is: - funding_btc lag=5 days (Binance funding 8h rate) - dvol_btc lag=1 day - fng lag=5 days - taker lag=1 day If the ACB were to re-apply lag, it would effectively double-delay the indicators, producing completely different signals than the gold backtest. We verify this by checking that the extracted factor values match the snapshot values EXACTLY — no arithmetic transformation applied. """ def test_funding_passes_through_unchanged(self): sentinel = -0.000111222333 # distinctive value snap = _make_snapshot(funding_btc=sentinel) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) assert factors['funding_btc'] == pytest.approx(sentinel, rel=1e-9), \ "funding_btc must not be transformed (lag already applied by ExF service)" def test_dvol_passes_through_unchanged(self): sentinel = 73.456789 snap = _make_snapshot(dvol_btc=sentinel) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) assert factors['dvol_btc'] == pytest.approx(sentinel, rel=1e-9) def test_fng_passes_through_unchanged(self): sentinel = 17.0 snap = _make_snapshot(fng=sentinel) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) assert factors['fng'] == pytest.approx(sentinel, rel=1e-9) def test_taker_passes_through_unchanged(self): sentinel = 0.83456 snap = _make_snapshot(taker=sentinel) acb = AdaptiveCircuitBreaker() factors = acb._load_external_factors_from_snapshot(snap) assert factors['taker'] == pytest.approx(sentinel, rel=1e-9) # ════════════════════════════════════════════════════════════════════════════════ # Section 10 — E2E: live HZ ping (skipped when HZ unavailable) # ════════════════════════════════════════════════════════════════════════════════ HZ_AVAILABLE = False try: import hazelcast as _hz _c = _hz.HazelcastClient( cluster_name='dolphin', cluster_members=['localhost:5701'], connection_timeout=2.0, ) _c.shutdown() HZ_AVAILABLE = True except Exception: pass @pytest.mark.skipif(not HZ_AVAILABLE, reason="Hazelcast not reachable — skipping live E2E test") class TestLiveHzE2E: """Live integration test — only runs when Hazelcast is accessible on localhost:5701.""" def _get_hz_features(self): import hazelcast client = hazelcast.HazelcastClient( cluster_name='dolphin', cluster_members=['localhost:5701'], connection_timeout=5.0, ) try: fmap = client.get_map('DOLPHIN_FEATURES').blocking() exf_raw = fmap.get('exf_latest') scan_raw = fmap.get('latest_eigen_scan') return ( json.loads(exf_raw) if exf_raw else None, json.loads(scan_raw) if scan_raw else None, ) finally: client.shutdown() def test_exf_latest_present_and_parseable(self): """FAILURE (not skip) — exf daemon must be running.""" exf_snap, _ = self._get_hz_features() assert exf_snap is not None, \ "exf_latest NOT FOUND — dolphin_data:exf_fetcher is DOWN" assert isinstance(exf_snap.get('funding_btc'), (int, float)) assert isinstance(exf_snap.get('dvol_btc'), (int, float)) def test_acb_computes_from_live_hz(self): from datetime import date exf_snap, scan_snap = self._get_hz_features() assert exf_snap is not None, "exf_latest NOT FOUND — daemon DOWN" today = date.today().isoformat() acb = AdaptiveCircuitBreaker() # Minimal preload (no history needed for this test) acb._w750_threshold = 0.001 w750_live = scan_snap.get('w750_velocity') if scan_snap else None result = acb.get_dynamic_boost_from_hz(today, exf_snap, w750_velocity=w750_live) assert result['source'] == 'hz' assert result['boost'] >= 1.0 assert result['beta'] in (ACBConfig.BETA_HIGH, ACBConfig.BETA_LOW, (ACBConfig.BETA_HIGH + ACBConfig.BETA_LOW) / 2.0) assert result['signals'] >= 0.0 print(f"\n[E2E] Live ACB: boost={result['boost']:.4f} signals={result['signals']:.1f} " f"beta={result['beta']:.2f} staleness={result['max_staleness_s']:.0f}s") def test_stale_exf_triggers_fallback_path(self): """Manually inject a stale timestamp and verify ValueError is raised.""" acb = AdaptiveCircuitBreaker() acb._w750_threshold = 0.001 # Build a snapshot with extremely stale indicators stale_snap = _make_snapshot(staleness_s={ 'funding_btc': _STALE_FALLBACK_S + 100, 'dvol_btc': 30, 'fng': 100, 'taker': 20 }) with pytest.raises(ValueError): acb.get_dynamic_boost_from_hz('2026-02-01', stale_snap) # ════════════════════════════════════════════════════════════════════════════════ # Section 11 — acb_processor_service HZ path (unit, no real HZ needed) # ════════════════════════════════════════════════════════════════════════════════ class TestACBProcessorServiceHzPath: """Unit tests for acb_processor_service.process_and_write() HZ preference logic.""" def _make_service(self, imap_data: dict): """Build an ACBProcessorService with mocked HZ imap.""" sys.path.insert(0, str(HCM_DIR / 'prod')) from acb_processor_service import ACBProcessorService # Patch hazelcast.HazelcastClient so no real connection is made mock_imap = MagicMock() mock_imap.get.side_effect = lambda key: ( json.dumps(imap_data[key]) if key in imap_data else None ) written = {} mock_imap.put.side_effect = lambda k, v: written.update({k: v}) mock_lock = MagicMock() mock_cp = MagicMock() mock_cp.get_lock.return_value.blocking.return_value = mock_lock mock_hz = MagicMock() mock_hz.get_map.return_value.blocking.return_value = mock_imap mock_hz.cp_subsystem = mock_cp with patch('hazelcast.HazelcastClient', return_value=mock_hz): svc = ACBProcessorService.__new__(ACBProcessorService) svc.hz_client = mock_hz svc.imap = mock_imap svc.lock = mock_lock svc.acb = AdaptiveCircuitBreaker() svc.acb._w750_threshold = 0.001 svc.last_scan_count = 0 svc.last_date = None return svc, written def test_hz_path_used_when_exf_available(self): exf_snap = _make_snapshot(dvol_btc=85.0, funding_btc=-0.0002) svc, written = self._make_service({'exf_latest': exf_snap}) svc.process_and_write('2026-02-05') assert 'acb_boost' in written result = json.loads(written['acb_boost']) assert result['source'] == 'hz' assert result['signals'] == pytest.approx(2.0) def test_npz_fallback_when_exf_absent(self): """When exf_latest is missing, service falls back to NPZ path (which reads disk).""" svc, written = self._make_service({}) # empty HZ # NPZ disk won't be available in CI but get_dynamic_boost_for_date() returns # a result with source='npz' (or absent source key from NPZ path). # We mock _load_external_factors to return neutral factors. with patch.object(svc.acb, '_load_external_factors', return_value={'funding_btc': 0.0, 'dvol_btc': 50.0, 'fng': 50.0, 'taker': 1.0, 'available': True}): svc.process_and_write('2026-02-05') assert 'acb_boost' in written result = json.loads(written['acb_boost']) # NPZ path doesn't set source='hz' assert result.get('source') != 'hz' def test_stale_exf_triggers_npz_fallback(self): stale_snap = _make_snapshot(staleness_s={ 'funding_btc': _STALE_FALLBACK_S + 1000, 'dvol_btc': 30, 'fng': 30, 'taker': 30, }) svc, written = self._make_service({'exf_latest': stale_snap}) with patch.object(svc.acb, '_load_external_factors', return_value={'funding_btc': 0.0, 'dvol_btc': 50.0, 'fng': 50.0, 'taker': 1.0, 'available': True}): svc.process_and_write('2026-02-05') assert 'acb_boost' in written result = json.loads(written['acb_boost']) assert result.get('source') != 'hz' # ════════════════════════════════════════════════════════════════════════════════ if __name__ == '__main__': import subprocess subprocess.run(['pytest', __file__, '-v', '--tb=short'], check=True)