196 lines
5.2 KiB
Python
196 lines
5.2 KiB
Python
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",
|
|
]
|