Checkpoint BLUE V7 long overlay work
This commit is contained in:
195
prod/tests/test_post_win_long_overlay.py
Normal file
195
prod/tests/test_post_win_long_overlay.py
Normal 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",
|
||||
]
|
||||
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