Checkpoint BLUE V7 long overlay work

This commit is contained in:
Codex
2026-05-08 19:54:13 +02:00
parent 351ce2044d
commit 83f007caa8
6 changed files with 5850 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
from datetime import datetime, timedelta, timezone
from adaptive_exit.post_win_long_overlay import (
PostWinExecutionFSM,
PostWinExecutionFSMConfig,
PostWinFlipTrigger,
)
def _ts(seconds: int = 0) -> datetime:
return datetime(2026, 5, 8, 12, 0, tzinfo=timezone.utc) + timedelta(seconds=seconds)
def test_big_win_arms_one_slot_and_resets_after_consumption():
overlay = PostWinExecutionFSM()
armed = overlay.observe_closed_trade(
trade_id="t1",
asset="ALGOUSDT",
side="SHORT",
pnl=398.0,
pnl_pct=0.004,
leverage=2.0,
closed_ts=_ts(),
)
assert armed.action == "ARMED"
assert armed.reason == "big_win"
assert overlay.pending_slots == 1
tag = overlay.tag_next_entry(asset="DASHUSDT", entry_ts=_ts(30))
assert tag.action == "TAG"
assert tag.side == "LONG"
assert tag.consumed_slot == 1
assert tag.reset is True
assert overlay.pending_slots == 0
after = overlay.tag_next_entry(asset="TRXUSDT", entry_ts=_ts(60))
assert after.action == "PASS"
assert after.side == "SHORT"
def test_big_win_high_lev_arms_two_slots_then_resets():
overlay = PostWinExecutionFSM()
armed = overlay.observe_closed_trade(
trade_id="t2",
asset="VETUSDT",
side="SHORT",
pnl=573.0,
pnl_pct=0.0148,
leverage=9.0,
closed_ts=_ts(),
)
assert armed.action == "ARMED"
assert armed.reason == "big_win_high_lev"
assert overlay.pending_slots == 2
first = overlay.tag_next_entry(asset="STXUSDT", entry_ts=_ts(10))
assert first.side == "LONG"
assert first.consumed_slot == 1
assert first.reset is False
assert overlay.pending_slots == 1
second = overlay.tag_next_entry(asset="TRXUSDT", entry_ts=_ts(20))
assert second.side == "LONG"
assert second.consumed_slot == 2
assert second.reset is True
assert overlay.pending_slots == 0
third = overlay.tag_next_entry(asset="ATOMUSDT", entry_ts=_ts(30))
assert third.side == "SHORT"
def test_small_dollar_high_return_arms_one_slot():
overlay = PostWinExecutionFSM()
armed = overlay.observe_closed_trade(
trade_id="t3",
asset="ETCUSDT",
side="SHORT",
pnl=149.0,
pnl_pct=0.0075,
leverage=0.8,
closed_ts=_ts(),
)
assert armed.action == "ARMED"
assert armed.reason == "small_dollar_high_return"
assert overlay.tag_next_entry(asset="LTCUSDT", entry_ts=_ts(10)).side == "LONG"
assert overlay.tag_next_entry(asset="BNBUSDT", entry_ts=_ts(20)).side == "SHORT"
def test_rearm_attempt_while_slots_active_is_ignored_and_does_not_extend_counter():
overlay = PostWinExecutionFSM()
overlay.observe_closed_trade(
trade_id="first",
asset="ALGOUSDT",
side="SHORT",
pnl=500.0,
pnl_pct=0.010,
leverage=9.0,
closed_ts=_ts(),
)
ignored = overlay.observe_closed_trade(
trade_id="second",
asset="VETUSDT",
side="SHORT",
pnl=900.0,
pnl_pct=0.020,
leverage=9.0,
closed_ts=_ts(5),
)
assert ignored.action == "IGNORED"
assert ignored.reason == "active_arm_no_rearm"
assert overlay.ignored_rearm_attempts == 1
assert overlay.pending_slots == 2
assert overlay.tag_next_entry(asset="A", entry_ts=_ts(10)).side == "LONG"
assert overlay.tag_next_entry(asset="B", entry_ts=_ts(20)).side == "LONG"
assert overlay.tag_next_entry(asset="C", entry_ts=_ts(30)).side == "SHORT"
def test_overlay_flipped_trade_outcome_cannot_rearm():
overlay = PostWinExecutionFSM()
ignored = overlay.observe_closed_trade(
trade_id="long-flip",
asset="DASHUSDT",
side="LONG",
pnl=1000.0,
pnl_pct=0.03,
leverage=9.0,
closed_ts=_ts(),
was_overlay_flip=True,
)
assert ignored.action == "IGNORED"
assert ignored.reason == "overlay_flip_outcome"
assert overlay.pending_slots == 0
def test_arm_expires_by_optional_ttl_without_consuming_slot():
overlay = PostWinExecutionFSM(PostWinExecutionFSMConfig(max_arm_age_sec=60.0))
overlay.observe_closed_trade(
trade_id="ttl",
asset="VETUSDT",
side="SHORT",
pnl=500.0,
pnl_pct=0.01,
leverage=9.0,
closed_ts=_ts(),
)
tag = overlay.tag_next_entry(asset="LATEUSDT", entry_ts=_ts(61))
assert tag.action == "PASS"
assert tag.side == "SHORT"
assert overlay.pending_slots == 0
assert overlay.expired_arms == 1
def test_future_expansion_supports_more_than_two_slots():
overlay = PostWinExecutionFSM(
PostWinExecutionFSMConfig(
rules=(
PostWinFlipTrigger(
name="future_three_slot_rule",
slots=3,
min_pnl_abs=100.0,
strict_min_pnl_abs=True,
),
)
)
)
overlay.observe_closed_trade(
trade_id="three",
asset="XRPUSDT",
side="SHORT",
pnl=101.0,
pnl_pct=0.001,
leverage=1.0,
closed_ts=_ts(),
)
assert [overlay.tag_next_entry(asset=str(i), entry_ts=_ts(i)).side for i in range(1, 5)] == [
"LONG",
"LONG",
"LONG",
"SHORT",
]

View 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)