Files
siloqy/prod/tests/test_v7_live_exit_wiring.py

296 lines
11 KiB
Python
Raw Normal View History

2026-05-08 19:54:13 +02:00
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path("/mnt/dolphinng5_predict")
sys.path.insert(0, str(ROOT / "nautilus_dolphin"))
sys.path.insert(1, str(ROOT))
if "nautilus_dolphin" in sys.modules:
pkg = sys.modules["nautilus_dolphin"]
pkg_file = str(getattr(pkg, "__file__", "") or "")
if not pkg_file.endswith("nautilus_dolphin/nautilus_dolphin/__init__.py"):
del sys.modules["nautilus_dolphin"]
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine, NDPosition
from nautilus_dolphin.nautilus.alpha_exit_v7_engine import AlphaExitEngineV7, AlphaExitV7Config
from prod.nautilus_event_trader import DolphinLiveTrader
class _DummyCtx:
def __init__(self, entry_price: float, entry_bar: int, side: int) -> None:
self.entry_price = entry_price
self.entry_bar = entry_bar
self.side = side
self.exf = None
def set_exf(self, funding: float = 0.0, dvol: float = 0.0, fear_greed: float = 0.0, taker: float = 0.0) -> None:
self.exf = {
"funding": funding,
"dvol": dvol,
"fear_greed": fear_greed,
"taker": taker,
}
class _DummyV7Engine:
def __init__(self) -> None:
self.make_calls = []
self.evaluate_calls = []
def make_context(self, entry_price: float, entry_bar: int, side: int) -> _DummyCtx:
self.make_calls.append((entry_price, entry_bar, side))
return _DummyCtx(entry_price=entry_price, entry_bar=entry_bar, side=side)
def evaluate(self, ctx, current_price: float, current_bar: int, ob_imbalance: float, asset: str = "default") -> dict:
self.evaluate_calls.append(
{
"ctx": ctx,
"current_price": current_price,
"current_bar": current_bar,
"ob_imbalance": ob_imbalance,
"asset": asset,
}
)
return {
"action": "EXIT",
"reason": "V7_COMPOSITE_PRESSURE",
"pnl_pct": 1.25,
"bars_held": current_bar - ctx.entry_bar,
"mfe": 0.02,
"mae": 0.01,
"mfe_risk": 0.0,
"mae_risk": 0.0,
"exit_pressure": 2.81,
"rv_comp": 0.001,
"mae_thresh1": 0.002,
"bounce_score": 0.1,
"bounce_risk": 0.2,
}
class _DummyOBSignal:
def __init__(self, imbalance_ma5: float) -> None:
self.imbalance_ma5 = imbalance_ma5
class _DummyOBEngine:
def __init__(self) -> None:
self.calls = []
def get_signal(self, asset: str, bar_idx: float):
self.calls.append((asset, bar_idx))
return _DummyOBSignal(0.42)
def test_ndalphaengine_prefers_exit_decision_provider_before_base_manager():
engine = NDAlphaEngine(
initial_capital=1000.0,
use_sp_fees=False,
use_sp_slippage=False,
use_ob_edge=False,
use_asset_selection=False,
use_direction_confirm=False,
use_alpha_layers=False,
use_dynamic_leverage=False,
)
pos = NDPosition(
trade_id="tid-1",
asset="DASHUSDT",
direction=-1,
entry_price=100.0,
entry_bar=0,
notional=100.0,
leverage=1.0,
fraction=0.2,
entry_vel_div=-0.03,
bucket_idx=4,
current_price=90.0,
)
engine.position = pos
engine._day_posture = "APEX"
engine.regime_dd_halt = False
provider_called = {}
def provider(**kwargs):
provider_called.update(kwargs)
return {
"action": "EXIT",
"reason": "V7_COMPOSITE_PRESSURE",
"pnl_pct": 1.25,
"bars_held": 7,
}
engine.exit_decision_provider = provider
def _should_not_run(*args, **kwargs):
raise AssertionError("base exit_manager should not be consulted when provider returns a decision")
engine.exit_manager.evaluate = _should_not_run
executed = {}
def _fake_execute_exit(reason: str, bar_idx: int, pnl_pct_raw: float = 0.0, bars_held: int = 0):
executed.update(
{
"reason": reason,
"bar_idx": bar_idx,
"pnl_pct_raw": pnl_pct_raw,
"bars_held": bars_held,
}
)
engine.position = None
return executed
engine._execute_exit = _fake_execute_exit
out = engine._manage_position(
bar_idx=17,
prices={"DASHUSDT": 89.0},
vel_div=-0.12,
v50_vel=0.03,
v750_vel=0.01,
)
assert provider_called["pos"] is pos
assert provider_called["bar_idx"] == 17
assert out["reason"] == "V7_COMPOSITE_PRESSURE"
assert executed["reason"] == "V7_COMPOSITE_PRESSURE"
assert executed["bar_idx"] == 17
def test_blue_live_v7_provider_records_journal_and_uses_ob_signal():
trader = DolphinLiveTrader.__new__(DolphinLiveTrader)
trader._v7_exit_engine = _DummyV7Engine()
trader._pending_entries = {
"tid-2": {
"entry_price": 100.0,
"entry_bar": 4,
"side": "SHORT",
"quantity": 2.0,
"leverage": 3.0,
"notional": 200.0,
}
}
trader._v7_contexts = {}
trader._v7_decision_seq = {}
trader._v7_decisions = {}
trader._last_exf = {
"funding": 1.0,
"dvol": 2.0,
"fear_greed": 3.0,
"taker": 4.0,
}
trader.ob_eng = _DummyOBEngine()
captured = {}
def _capture_record(**kwargs):
captured.update(kwargs)
trader._record_v7_decision = _capture_record
pos = SimpleNamespace(trade_id="tid-2", asset="DASHUSDT", current_price=97.0)
decision = trader._v7_live_exit_decision(
pos=pos,
bar_idx=10,
prices={"DASHUSDT": 97.5},
vel_div=-0.3,
v50_vel=0.1,
v750_vel=0.2,
)
assert decision["action"] == "EXIT"
assert trader._v7_contexts["tid-2"].exf == {
"funding": 1.0,
"dvol": 2.0,
"fear_greed": 3.0,
"taker": 4.0,
}
assert trader.ob_eng.calls == [("DASHUSDT", 9.0)]
assert trader._v7_exit_engine.evaluate_calls[0]["current_bar"] == 9
assert trader._v7_exit_engine.evaluate_calls[0]["ob_imbalance"] == pytest.approx(0.42)
assert captured["source"] == "live_exit"
assert captured["bar_idx"] == 9
assert captured["trade_id"] == "tid-2"
assert captured["asset"] == "DASHUSDT"
def test_alpha_exit_v7_is_mechanically_side_aware_for_long_and_short():
engine = AlphaExitEngineV7(bar_duration_sec=11.0, bounce_model_path="/tmp/nonexistent-bounce-model.pkl")
long_ctx = engine.make_context(entry_price=100.0, entry_bar=0, side=0)
long_favorable = engine.evaluate(long_ctx, current_price=101.0, current_bar=1, ob_imbalance=0.0)
assert long_favorable["pnl_pct"] == pytest.approx(1.0)
assert long_favorable["mfe"] == pytest.approx(0.01)
assert long_favorable["mae"] == pytest.approx(0.0)
long_adverse = engine.make_context(entry_price=100.0, entry_bar=0, side=0)
long_adverse_out = engine.evaluate(long_adverse, current_price=99.0, current_bar=1, ob_imbalance=0.0)
assert long_adverse_out["pnl_pct"] == pytest.approx(-1.0)
assert long_adverse_out["mfe"] == pytest.approx(0.0)
assert long_adverse_out["mae"] == pytest.approx(0.01)
short_ctx = engine.make_context(entry_price=100.0, entry_bar=0, side=1)
short_favorable = engine.evaluate(short_ctx, current_price=99.0, current_bar=1, ob_imbalance=0.0)
assert short_favorable["pnl_pct"] == pytest.approx(1.0)
assert short_favorable["mfe"] == pytest.approx(0.01)
assert short_favorable["mae"] == pytest.approx(0.0)
short_adverse = engine.make_context(entry_price=100.0, entry_bar=0, side=1)
short_adverse_out = engine.evaluate(short_adverse, current_price=101.0, current_bar=1, ob_imbalance=0.0)
assert short_adverse_out["pnl_pct"] == pytest.approx(-1.0)
assert short_adverse_out["mfe"] == pytest.approx(0.0)
assert short_adverse_out["mae"] == pytest.approx(0.01)
def test_alpha_exit_v7_default_config_matches_legacy_threshold_surface():
engine = AlphaExitEngineV7(bar_duration_sec=11.0, bounce_model_path="/tmp/nonexistent-bounce-model.pkl")
cfg = engine.config
assert cfg.rvol_w15 == pytest.approx(0.50)
assert cfg.rvol_w30 == pytest.approx(0.30)
assert cfg.rvol_w50 == pytest.approx(0.20)
assert cfg.mae_tier1_k == pytest.approx(3.5)
assert cfg.mae_tier2_k == pytest.approx(7.0)
assert cfg.mae_tier3_k == pytest.approx(12.0)
assert cfg.mae_tier1_floor == pytest.approx(0.005)
assert cfg.mae_tier2_floor == pytest.approx(0.012)
assert cfg.mae_tier3_floor == pytest.approx(0.025)
assert cfg.mae_tier1_risk == pytest.approx(0.5)
assert cfg.mae_tier2_risk == pytest.approx(0.8)
assert cfg.mae_tier3_risk == pytest.approx(1.2)
assert cfg.mae_accel_min_bars == 3
assert cfg.mae_accel_peak_floor == pytest.approx(0.003)
assert cfg.mae_recovery_peak_floor == pytest.approx(0.004)
assert cfg.mae_recovery_prev_min == pytest.approx(0.25)
assert cfg.mae_recovery_snapback_max == pytest.approx(0.10)
assert cfg.mae_late_floor == pytest.approx(0.003)
assert cfg.mae_late_start_frac == pytest.approx(0.60)
assert cfg.mae_late_risk_max == pytest.approx(0.4)
assert cfg.mfe_convexity_decay_exit == pytest.approx(0.35)
assert cfg.mfe_convexity_decay_soft == pytest.approx(0.20)
assert cfg.bounce_dir_w == pytest.approx(0.15)
assert cfg.bounce_risk_w == pytest.approx(0.35)
assert cfg.exit_pressure_threshold == pytest.approx(2.69)
assert cfg.retract_pressure_threshold == pytest.approx(1.0)
assert cfg.extend_pressure_threshold == pytest.approx(-0.5)
def test_alpha_exit_v7_custom_threshold_config_is_per_instance():
strict = AlphaExitEngineV7(
bar_duration_sec=11.0,
bounce_model_path="/tmp/nonexistent-bounce-model.pkl",
config=AlphaExitV7Config(exit_pressure_threshold=0.1, retract_pressure_threshold=0.05),
)
default = AlphaExitEngineV7(bar_duration_sec=11.0, bounce_model_path="/tmp/nonexistent-bounce-model.pkl")
strict_ctx = strict.make_context(entry_price=100.0, entry_bar=0, side=0)
default_ctx = default.make_context(entry_price=100.0, entry_bar=0, side=0)
strict_decision = strict.evaluate(strict_ctx, current_price=100.0, current_bar=1, ob_imbalance=0.0)
default_decision = default.evaluate(default_ctx, current_price=100.0, current_bar=1, ob_imbalance=0.0)
assert strict_decision["action"] == "EXIT"
assert default_decision["action"] == "HOLD"
assert strict.config.exit_pressure_threshold == pytest.approx(0.1)
assert default.config.exit_pressure_threshold == pytest.approx(2.69)