Checkpoint BLUE V7 long overlay work
This commit is contained in:
295
prod/tests/test_v7_live_exit_wiring.py
Normal file
295
prod/tests/test_v7_live_exit_wiring.py
Normal file
@@ -0,0 +1,295 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user