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", ]