initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree

Includes core prod + GREEN/BLUE subsystems:
- prod/ (BLUE harness, configs, scripts, docs)
- nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved)
- adaptive_exit/ (AEM engine + models/bucket_assignments.pkl)
- Observability/ (EsoF advisor, TUI, dashboards)
- external_factors/ (EsoF producer)
- mc_forewarning_qlabs_fork/ (MC regime/envelope)

Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
hjnormey
2026-04-21 16:58:38 +02:00
commit 01c19662cb
643 changed files with 260241 additions and 0 deletions

View File

@@ -0,0 +1,727 @@
"""
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)