"""test_dolphin_actor.py — DolphinActor lifecycle and correctness tests. Tests cover: - Champion parameter invariants (frozen champion config → correct defaults) - ACB pending-flag thread safety (no lost updates, no race on clear) - HIBERNATE posture guard (on_bar returns immediately, engine not called) - Date change handling (begin_day / end_day transition) - Replay mode data loading (bar_idx increments, engine step_bar called) - HZ-unavailable graceful degradation (posture defaults APEX, no crash) - Stale-state snapshot guard (detects mid-eval state changes) - on_stop cleanup (processed_dates cleared, stale events reset) All tests use unittest.mock to avoid requiring live HZ or parquet files. Run with: source /home/dolphin/siloqy_env/bin/activate cd /mnt/dolphinng5_predict python -m pytest nautilus_dolphin/tests/test_dolphin_actor.py -v """ import sys import json import threading import time import unittest from datetime import datetime, timezone from pathlib import Path from unittest.mock import MagicMock, patch, PropertyMock HCM_DIR = Path(__file__).parent.parent.parent sys.path.insert(0, str(HCM_DIR / "nautilus_dolphin")) try: from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor _HAS_ACTOR = True except ImportError as _e: _HAS_ACTOR = False _IMPORT_ERR = str(_e) import pytest # ── Helpers ─────────────────────────────────────────────────────────────────────── def _make_blue_config(**overrides) -> dict: """Minimal champion-frozen config dict (matches blue.yml).""" cfg = { "strategy_name": "blue", "direction": "short_only", "live_mode": False, "engine": { "boost_mode": "baseline", "vel_div_threshold": -0.02, "vel_div_extreme": -0.05, "fixed_tp_pct": 0.0095, "max_hold_bars": 120, "fraction": 0.20, "min_leverage": 0.5, "max_leverage": 5.0, "abs_max_leverage": 6.0, "leverage_convexity": 3.0, "dc_lookback_bars": 7, "dc_min_magnitude_bps": 0.75, "min_irp_alignment": 0.45, "sp_maker_entry_rate": 0.62, "sp_maker_exit_rate": 0.50, "seed": 42, # required but non-champion "stop_pct": 1.0, "use_direction_confirm": True, "dc_skip_contradicts": True, "dc_leverage_boost": 1.0, "dc_leverage_reduce": 0.5, "use_asset_selection": True, "use_sp_fees": True, "use_sp_slippage": True, "use_ob_edge": True, "ob_edge_bps": 5.0, "ob_confirm_rate": 0.40, "lookback": 100, "use_alpha_layers": True, "use_dynamic_leverage": True, }, "paper_trade": {"initial_capital": 25000.0}, "hazelcast": {"imap_pnl": "DOLPHIN_PNL_BLUE"}, } cfg.update(overrides) return cfg def _make_actor_no_hz() -> "DolphinActor": """Return a DolphinActor with HZ patched to None and Strategy.__init__ mocked.""" actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) return actor def _make_synthetic_bar(date_str: str = "2026-01-15", bar_seconds: int = 5): """Build a minimal Bar-like object with ts_event in nanoseconds.""" dt = datetime.strptime(date_str, "%Y-%m-%d").replace( hour=0, minute=0, second=bar_seconds, tzinfo=timezone.utc ) bar = MagicMock() bar.ts_event = int(dt.timestamp() * 1e9) return bar # ── Test: Champion parameter invariants ────────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason=f"DolphinActor import failed: {locals().get('_IMPORT_ERR','')}") class TestChampionParamInvariants(unittest.TestCase): """Champion params must survive round-trip through DolphinActor.__init__.""" CHAMPION = { "vel_div_threshold": -0.02, "vel_div_extreme": -0.05, "fixed_tp_pct": 0.0095, "max_hold_bars": 120, "fraction": 0.20, "min_leverage": 0.5, "max_leverage": 5.0, "abs_max_leverage": 6.0, "leverage_convexity": 3.0, "dc_lookback_bars": 7, "dc_min_magnitude_bps": 0.75, "min_irp_alignment": 0.45, "sp_maker_entry_rate": 0.62, "sp_maker_exit_rate": 0.50, "seed": 42, } def test_champion_config_round_trip(self): """Actor init stores config dict without mutation.""" actor = DolphinActor.__new__(DolphinActor) cfg = _make_blue_config() DolphinActor.__init__(actor, config=cfg) eng_cfg = actor.dolphin_config["engine"] for k, v in self.CHAMPION.items(): self.assertAlmostEqual(eng_cfg[k], v, places=9, msg=f"Champion param {k} mismatch: {eng_cfg[k]} != {v}") def test_initial_posture_is_apex(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) self.assertEqual(actor.posture, "APEX") def test_initial_engine_is_none(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) self.assertIsNone(actor.engine) def test_initial_hz_client_is_none(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) self.assertIsNone(actor.hz_client) def test_pending_acb_is_none(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) self.assertIsNone(actor._pending_acb) def test_acb_lock_is_rlock_compatible(self): """_acb_lock must be a threading.Lock (or RLock) — acquire/release must work.""" actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) acquired = actor._acb_lock.acquire(timeout=0.1) self.assertTrue(acquired) actor._acb_lock.release() # ── Test: ACB pending-flag thread safety ───────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestACBPendingFlagThreadSafety(unittest.TestCase): """Verify the pending-flag pattern prevents lost ACB updates.""" def _make_actor(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) # Attach a minimal mock engine that tracks update_acb_boost calls actor.engine = MagicMock() actor.engine._day_base_boost = 1.0 actor.engine._day_beta = 0.0 actor.engine._mc_gate_open = True return actor def test_on_acb_event_stores_pending(self): """_on_acb_event must set _pending_acb under the lock.""" actor = self._make_actor() event = MagicMock() event.value = json.dumps({"boost": 1.5, "beta": 0.3}) actor._on_acb_event(event) with actor._acb_lock: pending = actor._pending_acb self.assertIsNotNone(pending) self.assertAlmostEqual(pending["boost"], 1.5) self.assertAlmostEqual(pending["beta"], 0.3) def test_on_acb_event_empty_value_ignored(self): """Empty/None event value must not set pending.""" actor = self._make_actor() event = MagicMock() event.value = None actor._on_acb_event(event) self.assertIsNone(actor._pending_acb) def test_on_acb_event_malformed_json_ignored(self): """Malformed JSON must be caught; pending stays None.""" actor = self._make_actor() event = MagicMock() event.value = "{not-json" actor._on_acb_event(event) self.assertIsNone(actor._pending_acb) def test_concurrent_write_then_on_bar_apply(self): """Simulate concurrent HZ listener write + on_bar read — no lost update. Thread 1 writes boost=2.0 via _on_acb_event(). Thread 2 (main) calls the on_bar ACB-apply section and asserts engine updated. """ actor = self._make_actor() # Simulate _on_acb_event from a background thread def writer(): event = MagicMock() event.value = json.dumps({"boost": 2.0, "beta": 0.5}) actor._on_acb_event(event) t = threading.Thread(target=writer) t.start() t.join(timeout=1.0) # Simulate the ACB-apply section of on_bar() with actor._acb_lock: pending = actor._pending_acb actor._pending_acb = None self.assertIsNotNone(pending, "ACB update was lost") actor.engine.update_acb_boost(float(pending["boost"]), float(pending["beta"])) actor.engine.update_acb_boost.assert_called_once_with(2.0, 0.5) # After consumption, pending must be None with actor._acb_lock: self.assertIsNone(actor._pending_acb) def test_multiple_rapid_events_last_wins(self): """Multiple rapid HZ events — last write wins (expected behaviour).""" actor = self._make_actor() for boost in [1.1, 1.2, 1.3, 1.4, 1.5]: event = MagicMock() event.value = json.dumps({"boost": boost, "beta": 0.0}) actor._on_acb_event(event) with actor._acb_lock: pending = actor._pending_acb self.assertAlmostEqual(pending["boost"], 1.5) # ── Test: HIBERNATE posture guard ──────────────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestHibernatePostureGuard(unittest.TestCase): """When posture=='HIBERNATE', on_bar must return immediately.""" def _make_actor_with_engine(self, posture: str = "HIBERNATE"): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.posture = posture actor.current_date = "2026-01-15" # pre-set so no begin_day triggers actor.engine = MagicMock() actor.engine._day_base_boost = 1.0 actor.engine._day_beta = 0.0 actor.engine._mc_gate_open = True actor.hz_client = None actor._day_data = None return actor def test_hibernate_skips_step_bar(self): """step_bar must not be called when posture==HIBERNATE.""" actor = self._make_actor_with_engine("HIBERNATE") bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) actor.engine.step_bar.assert_not_called() def test_hibernate_does_not_increment_bar_idx(self): """_bar_idx_today must not change on HIBERNATE.""" actor = self._make_actor_with_engine("HIBERNATE") actor._bar_idx_today = 42 bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) self.assertEqual(actor._bar_idx_today, 42) def test_apex_does_attempt_step_bar(self): """APEX posture must reach the step_bar call (given valid data).""" actor = self._make_actor_with_engine("APEX") # Provide fake day data so on_bar can pull a row import pandas as pd import numpy as np rows = { "vel_div": np.full(200, -0.03), "BTCUSDT": np.full(200, 95000.0), "v50_lambda_max_velocity": np.zeros(200), "v750_lambda_max_velocity": np.zeros(200), "instability_50": np.zeros(200), } df = pd.DataFrame(rows) actor._day_data = (df, ["BTCUSDT"]) actor._bar_idx_today = 0 actor.engine.step_bar = MagicMock(return_value={}) actor._write_result_to_hz = MagicMock() bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) actor.engine.step_bar.assert_called_once() # ── Test: Date change handling ──────────────────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestDateChangeHandling(unittest.TestCase): """Verify begin_day / end_day lifecycle across date boundaries.""" def _make_actor(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.hz_client = None actor.posture = "APEX" actor.engine = MagicMock() actor.engine._day_base_boost = 1.0 actor.engine._day_beta = 0.0 actor.engine._mc_gate_open = True actor._day_data = None actor._write_result_to_hz = MagicMock() return actor def test_first_bar_calls_begin_day_not_end_day(self): """First bar of the session must call begin_day but not end_day.""" actor = self._make_actor() actor.current_date = None bar = _make_synthetic_bar("2026-01-15") # Stub _load_parquet_data to return empty (HIBERNATE-safe; no step_bar needed here) with patch.object(actor, "_load_parquet_data", return_value=(MagicMock(empty=True), [], None)): actor.on_bar(bar) actor.engine.begin_day.assert_called_once_with("2026-01-15", posture="APEX", direction=-1) actor.engine.end_day.assert_not_called() def test_date_change_calls_end_day_then_begin_day(self): """Date rollover must call end_day() on old date then begin_day() on new.""" actor = self._make_actor() actor.current_date = "2026-01-14" actor.engine.end_day = MagicMock(return_value={}) bar_new = _make_synthetic_bar("2026-01-15") with patch.object(actor, "_load_parquet_data", return_value=(MagicMock(empty=True), [], None)): actor.on_bar(bar_new) actor.engine.end_day.assert_called_once() actor.engine.begin_day.assert_called_once_with("2026-01-15", posture="APEX", direction=-1) def test_same_date_no_redundant_begin_day(self): """Bars arriving on same date must not re-call begin_day.""" actor = self._make_actor() actor.current_date = "2026-01-15" import pandas as pd import numpy as np rows = { "vel_div": np.full(200, -0.03), "BTCUSDT": np.full(200, 95000.0), "v50_lambda_max_velocity": np.zeros(200), "v750_lambda_max_velocity": np.zeros(200), "instability_50": np.zeros(200), } actor._day_data = (pd.DataFrame(rows), ["BTCUSDT"]) actor._bar_idx_today = 0 actor.engine.step_bar = MagicMock(return_value={}) bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) bar2 = _make_synthetic_bar("2026-01-15", bar_seconds=10) actor.on_bar(bar2) actor.engine.begin_day.assert_not_called() def test_direction_short_only_maps_to_minus_one(self): """direction='short_only' must pass direction=-1 to begin_day.""" actor = self._make_actor() actor.current_date = None actor.dolphin_config["direction"] = "short_only" bar = _make_synthetic_bar("2026-01-15") with patch.object(actor, "_load_parquet_data", return_value=(MagicMock(empty=True), [], None)): actor.on_bar(bar) _, kwargs = actor.engine.begin_day.call_args self.assertEqual(kwargs["direction"], -1) def test_direction_long_maps_to_plus_one(self): """direction='long' must pass direction=+1 to begin_day.""" actor = self._make_actor() actor.current_date = None actor.dolphin_config["direction"] = "long" bar = _make_synthetic_bar("2026-01-15") with patch.object(actor, "_load_parquet_data", return_value=(MagicMock(empty=True), [], None)): actor.on_bar(bar) _, kwargs = actor.engine.begin_day.call_args self.assertEqual(kwargs["direction"], 1) # ── Test: HZ-unavailable graceful degradation ──────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestHZUnavailableDegradation(unittest.TestCase): """Actor must function (APEX posture, no crash) when HZ is unreachable.""" def test_connect_hz_returns_none_on_failure(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) with patch("hazelcast.HazelcastClient", side_effect=Exception("refused")): result = actor._connect_hz() self.assertIsNone(result) def test_read_posture_returns_apex_when_hz_none(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.hz_client = None posture = actor._read_posture() self.assertEqual(posture, "APEX") def test_write_result_no_hz_is_noop(self): """_write_result_to_hz with hz_client=None must silently return.""" actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.hz_client = None # Must not raise actor._write_result_to_hz("2026-01-15", {"pnl": 1.0}) def test_get_latest_hz_scan_returns_none_when_no_client(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.hz_client = None result = actor._get_latest_hz_scan() self.assertIsNone(result) # ── Test: Replay mode bar index tracking ───────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestReplayModeBarTracking(unittest.TestCase): """Verify _bar_idx_today increments correctly in replay mode.""" def _make_actor_with_data(self, n_rows: int = 10): import pandas as pd import numpy as np actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.hz_client = None actor.posture = "APEX" actor.current_date = "2026-01-15" actor.engine = MagicMock() actor.engine._day_base_boost = 1.0 actor.engine._day_beta = 0.0 actor.engine._mc_gate_open = True actor.engine.step_bar = MagicMock(return_value={}) actor._write_result_to_hz = MagicMock() rows = { "vel_div": np.full(n_rows, -0.03), "BTCUSDT": np.full(n_rows, 95000.0), "v50_lambda_max_velocity": np.zeros(n_rows), "v750_lambda_max_velocity": np.zeros(n_rows), "instability_50": np.zeros(n_rows), } actor._day_data = (pd.DataFrame(rows), ["BTCUSDT"]) actor._bar_idx_today = 0 return actor def test_bar_idx_increments_per_bar(self): actor = self._make_actor_with_data(10) bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) self.assertEqual(actor._bar_idx_today, 1) actor.on_bar(bar) self.assertEqual(actor._bar_idx_today, 2) def test_step_bar_called_with_correct_bar_idx(self): """step_bar's bar_idx kwarg must match the pre-increment counter.""" actor = self._make_actor_with_data(10) bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) call_kwargs = actor.engine.step_bar.call_args[1] self.assertEqual(call_kwargs["bar_idx"], 0) def test_past_end_of_data_returns_silently(self): """When _bar_idx_today >= len(df), on_bar must return without stepping.""" actor = self._make_actor_with_data(3) actor._bar_idx_today = 3 # already past end bar = _make_synthetic_bar("2026-01-15") actor.on_bar(bar) actor.engine.step_bar.assert_not_called() # ── Test: on_stop cleanup ──────────────────────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestOnStopCleanup(unittest.TestCase): """on_stop must clear state and shut down HZ client.""" def test_on_stop_clears_processed_dates(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor._processed_dates = {"2026-01-13", "2026-01-14"} actor.hz_client = None actor.on_stop() self.assertEqual(len(actor._processed_dates), 0) def test_on_stop_resets_stale_state_events(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor._stale_state_events = 7 actor.hz_client = None actor.on_stop() self.assertEqual(actor._stale_state_events, 0) def test_on_stop_shuts_down_hz_client(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) mock_hz = MagicMock() actor.hz_client = mock_hz actor.on_stop() mock_hz.shutdown.assert_called_once() def test_on_stop_no_hz_no_crash(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.hz_client = None actor.on_stop() # must not raise # ── Test: Stale-state snapshot ──────────────────────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestStaleStateGuard(unittest.TestCase): """_GateSnap before/after comparison must detect mid-eval state changes.""" def test_gate_snap_equality_no_change(self): """Identical pre/post snapshots → no stale event logged.""" from nautilus_dolphin.nautilus.dolphin_actor import _GateSnap before = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="APEX", mc_gate_open=True) after = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="APEX", mc_gate_open=True) self.assertEqual(before, after) def test_gate_snap_detects_boost_change(self): from nautilus_dolphin.nautilus.dolphin_actor import _GateSnap before = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="APEX", mc_gate_open=True) after = _GateSnap(acb_boost=1.5, acb_beta=0.0, posture="APEX", mc_gate_open=True) self.assertNotEqual(before, after) def test_gate_snap_detects_posture_change(self): from nautilus_dolphin.nautilus.dolphin_actor import _GateSnap before = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="APEX", mc_gate_open=True) after = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="STALKER", mc_gate_open=True) self.assertNotEqual(before, after) def test_gate_snap_detects_gate_close(self): from nautilus_dolphin.nautilus.dolphin_actor import _GateSnap before = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="APEX", mc_gate_open=True) after = _GateSnap(acb_boost=1.0, acb_beta=0.0, posture="APEX", mc_gate_open=False) self.assertNotEqual(before, after) def test_gate_snap_fields_list(self): """GateSnap must have exactly 4 named fields.""" from nautilus_dolphin.nautilus.dolphin_actor import _GateSnap self.assertEqual(set(_GateSnap._fields), {"acb_boost", "acb_beta", "posture", "mc_gate_open"}) # ── Test: Capital persistence (_save_capital / _restore_capital) ───────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestCapitalPersistence(unittest.TestCase): """DolphinActor._save_capital / _restore_capital — HZ checkpoint roundtrip.""" def _make_actor_with_state_map(self, capital: float = 31_500.0): """Actor with a mock state_map backed by a real dict.""" actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.engine = MagicMock() actor.engine.capital = capital saved = {} mock_map = MagicMock() mock_map.blocking.return_value.put = lambda k, v: saved.update({k: v}) mock_map.blocking.return_value.get = lambda k: saved.get(k) actor.state_map = mock_map return actor, saved def test_save_writes_checkpoint_key(self): actor, saved = self._make_actor_with_state_map(31_500.0) DolphinActor._save_capital(actor) self.assertIn('capital_checkpoint', saved, "_save_capital must write 'capital_checkpoint' to state_map") def test_save_persists_correct_value(self): actor, saved = self._make_actor_with_state_map(99_123.45) DolphinActor._save_capital(actor) data = json.loads(saved['capital_checkpoint']) self.assertAlmostEqual(data['capital'], 99_123.45, places=2) def test_save_nan_not_written(self): actor, saved = self._make_actor_with_state_map(float('nan')) DolphinActor._save_capital(actor) self.assertNotIn('capital_checkpoint', saved, "NaN capital must not be persisted") def test_save_zero_not_written(self): actor, saved = self._make_actor_with_state_map(0.0) DolphinActor._save_capital(actor) self.assertNotIn('capital_checkpoint', saved, "Zero capital must not be persisted") def test_save_sub_dollar_not_written(self): actor, saved = self._make_actor_with_state_map(0.50) DolphinActor._save_capital(actor) self.assertNotIn('capital_checkpoint', saved, "Sub-$1 capital must not be persisted") def test_save_no_state_map_no_crash(self): actor, _ = self._make_actor_with_state_map(25_000.0) actor.state_map = None DolphinActor._save_capital(actor) # must not raise def test_restore_recovers_saved_capital(self): actor, _ = self._make_actor_with_state_map(77_777.77) DolphinActor._save_capital(actor) # Fresh actor, same mock map actor2 = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor2, config=_make_blue_config()) actor2.engine = MagicMock() actor2.engine.capital = 25_000.0 actor2.state_map = actor.state_map DolphinActor._restore_capital(actor2) self.assertAlmostEqual(actor2.engine.capital, 77_777.77, places=2, msg="Restored capital must match saved value") def test_restore_stale_72h_ignored(self): """Checkpoint older than 72 h must not be restored.""" import time as _time saved = {} mock_map = MagicMock() old_ts = _time.time() - (73 * 3600) saved['capital_checkpoint'] = json.dumps({'capital': 55_000.0, 'ts': old_ts}) mock_map.blocking.return_value.get = lambda k: saved.get(k) actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.engine = MagicMock() actor.engine.capital = 25_000.0 actor.state_map = mock_map DolphinActor._restore_capital(actor) # Capital must remain at initial value — stale checkpoint must not be applied self.assertAlmostEqual(actor.engine.capital, 25_000.0, places=2, msg="Stale (73h) checkpoint must not restore capital") def test_restore_sub_dollar_ignored(self): """Checkpoint with capital < $1 must not be restored.""" import time as _time saved = {} mock_map = MagicMock() saved['capital_checkpoint'] = json.dumps({'capital': 0.001, 'ts': _time.time()}) mock_map.blocking.return_value.get = lambda k: saved.get(k) actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.engine = MagicMock() actor.engine.capital = 25_000.0 actor.state_map = mock_map DolphinActor._restore_capital(actor) # Sub-$1 checkpoint must not be applied self.assertAlmostEqual(actor.engine.capital, 25_000.0, places=2, msg="Sub-$1 checkpoint must not restore capital") def test_restore_no_state_map_no_crash(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config()) actor.engine = MagicMock() actor.state_map = None DolphinActor._restore_capital(actor) # must not raise def test_connect_hz_reads_host_from_config(self): """_connect_hz must use config hazelcast.host, not hardcoded localhost.""" actor = DolphinActor.__new__(DolphinActor) cfg = _make_blue_config( live_mode=True, hazelcast={"host": "10.0.0.99:5701", "cluster": "testnet", "imap_pnl": "DOLPHIN_PNL_BLUE", "state_map": "DOLPHIN_STATE_BLUE"} ) DolphinActor.__init__(actor, config=cfg) with patch("hazelcast.HazelcastClient") as mock_hz: mock_client = MagicMock() mock_hz.return_value = mock_client actor._connect_hz() _, kwargs = mock_hz.call_args self.assertIn("10.0.0.99:5701", kwargs.get("cluster_members", []), "_connect_hz must pass config host to HazelcastClient") self.assertEqual(kwargs.get("cluster_name"), "testnet") def test_connect_hz_sets_state_map(self): """_connect_hz must assign self.state_map from DOLPHIN_STATE_BLUE.""" actor = DolphinActor.__new__(DolphinActor) cfg = _make_blue_config(live_mode=True, hazelcast={"host": "localhost:5701", "cluster": "dolphin", "imap_pnl": "DOLPHIN_PNL_BLUE"}) DolphinActor.__init__(actor, config=cfg) with patch("hazelcast.HazelcastClient") as mock_hz: mock_client = MagicMock() mock_hz.return_value = mock_client actor._connect_hz() self.assertIsNotNone(actor.state_map, "state_map must be set after successful _connect_hz()") # ── Test: Stablecoin filter + NaN vel_div guard ────────────────────────────────── @pytest.mark.skipif(not _HAS_ACTOR, reason="DolphinActor not available") class TestLiveBarGuards(unittest.TestCase): """Stablecoin hard-block and NaN/spike vel_div clamp in on_bar() live path.""" def _make_live_actor(self): actor = DolphinActor.__new__(DolphinActor) DolphinActor.__init__(actor, config=_make_blue_config(live_mode=True)) actor.hz_client = MagicMock() actor.posture = "APEX" actor.current_date = "2026-01-15" actor.engine = MagicMock() actor.engine._day_base_boost = 1.0 actor.engine._day_beta = 0.0 actor.engine._mc_gate_open = True actor.engine.step_bar = MagicMock(return_value={}) actor._write_result_to_hz = MagicMock() return actor def _inject_scan(self, actor, scan: dict): """Push scan directly into the push cache (bypasses HZ reactor thread).""" with actor._scan_cache_lock: actor._latest_scan_cache = scan actor.last_scan_number = -1 def test_stablecoin_absent_from_prices_passed_to_engine(self): """USDCUSDT in scan must be absent from prices dict passed to step_bar.""" actor = self._make_live_actor() scan = { 'scan_number': 1, 'vel_div': -0.03, 'w50_velocity': -0.03, 'w750_velocity': -0.005, 'instability_50': 0.1, 'assets': ['BTCUSDT', 'ETHUSDT', 'USDCUSDT'], 'asset_prices': [95000.0, 2500.0, 1.0001], } self._inject_scan(actor, scan) bar = _make_synthetic_bar("2026-01-15", bar_seconds=5) actor.on_bar(bar) call_kwargs = actor.engine.step_bar.call_args[1] prices_passed = call_kwargs.get('prices', {}) self.assertNotIn('USDCUSDT', prices_passed, "USDCUSDT must be hard-blocked before step_bar") self.assertIn('BTCUSDT', prices_passed) self.assertIn('ETHUSDT', prices_passed) def test_nan_vel_div_clamped_to_zero(self): """NaN vel_div in scan must be clamped to 0.0 before step_bar.""" actor = self._make_live_actor() scan = { 'scan_number': 2, 'vel_div': float('nan'), 'w50_velocity': 0.0, 'w750_velocity': 0.0, 'instability_50': 0.0, 'assets': ['BTCUSDT'], 'asset_prices': [95000.0], } self._inject_scan(actor, scan) bar = _make_synthetic_bar("2026-01-15", bar_seconds=5) actor.on_bar(bar) call_kwargs = actor.engine.step_bar.call_args[1] self.assertEqual(call_kwargs['vel_div'], 0.0, "NaN vel_div must be clamped to 0.0") def test_spike_vel_div_clamped_to_zero(self): """|vel_div| > 0.20 (NG7 restart spike) must be clamped to 0.0.""" actor = self._make_live_actor() for spike in [-12.76, +12.98, -0.21, +0.21]: scan = { 'scan_number': 10 + int(abs(spike)), 'vel_div': spike, 'w50_velocity': spike, 'w750_velocity': 0.0, 'instability_50': 0.0, 'assets': ['BTCUSDT'], 'asset_prices': [95000.0], } self._inject_scan(actor, scan) actor.last_scan_number = -1 bar = _make_synthetic_bar("2026-01-15", bar_seconds=5) actor.on_bar(bar) call_kwargs = actor.engine.step_bar.call_args[1] self.assertEqual(call_kwargs['vel_div'], 0.0, f"vel_div spike={spike} must be clamped to 0.0") def test_duplicate_scan_number_skipped(self): """Same scan_number arriving twice must call step_bar only once.""" actor = self._make_live_actor() scan = { 'scan_number': 99, 'vel_div': -0.03, 'w50_velocity': -0.03, 'w750_velocity': -0.005, 'instability_50': 0.0, 'assets': ['BTCUSDT'], 'asset_prices': [95000.0], } self._inject_scan(actor, scan) bar = _make_synthetic_bar("2026-01-15", bar_seconds=5) actor.on_bar(bar) actor.on_bar(bar) # second call with same scan_number — must be deduped self.assertEqual(actor.engine.step_bar.call_count, 1, "Duplicate scan_number must be deduplicated") # ── Standalone runner ───────────────────────────────────────────────────────────── if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])