from __future__ import annotations import sys import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from src.quant_engine.exit_decisions import compute_timing_decision class TestScoreParityV1(unittest.TestCase): def test_pullback_wait_is_selected_for_borderline_entry_scores(self): res = compute_timing_decision( { "priceStatus": "PRICE_OK", "atr20": 100.0, "entryModeGate": "PASS", "entryMode": "PULLBACK", "leaderTotal": 3, "leaderGate": "PASS", "flowCredit": 0.4, "acGate": "CAUTION", "ma20Slope": 1.0, "disparity": 4.5, "rsi14": 68.0, "avgTradeValue5D": 100.0, "spreadPct": 0.5, } ) self.assertEqual(res["action"], "BUY_PULLBACK_WAIT") self.assertGreaterEqual(res["entry_score"], 60) self.assertLess(res["entry_score"], 100) def test_entry_score_boosts_with_leader_flow_and_clear_gate(self): res = compute_timing_decision( { "priceStatus": "PRICE_OK", "atr20": 100.0, "entryModeGate": "PASS", "entryMode": "BREAKOUT", "leaderTotal": 4, "leaderGate": "PASS", "flowCredit": 0.8, "acGate": "CLEAR", "ma20Slope": 1.0, "disparity": 2.0, "rsi14": 55.0, "avgTradeValue5D": 100.0, "spreadPct": 0.5, } ) self.assertEqual(res["action"], "BUY_BREAKOUT_PILOT_ONLY") self.assertGreaterEqual(res["entry_score"], 75) self.assertLessEqual(res["exit_score"], 20) def test_exit_review_is_selected_before_forced_exit_threshold(self): res = compute_timing_decision( { "priceStatus": "PRICE_OK", "atr20": 100.0, "entryModeGate": "PASS", "entryMode": "PULLBACK", "leaderTotal": 3, "leaderGate": "PASS", "flowCredit": 0.6, "acGate": "CAUTION", "ma20Slope": -1.0, "disparity": 8.5, "rsi14": 70.0, "avgTradeValue5D": 100.0, "spreadPct": 0.5, "rwPartial": 2, } ) self.assertEqual(res["action"], "EXIT_REVIEW") self.assertGreaterEqual(res["exit_score"], 50) self.assertLess(res["exit_score"], 75) def test_data_missing_short_circuits_timing_action(self): res = compute_timing_decision( { "priceStatus": "PRICE_OK", "atr20": None, "entryModeGate": "PASS", "entryMode": "BREAKOUT", "leaderTotal": 4, "leaderGate": "PASS", "flowCredit": 0.8, "acGate": "CLEAR", "ma20Slope": 1.0, "disparity": 2.0, "rsi14": 55.0, "avgTradeValue5D": 100.0, "spreadPct": 0.5, } ) self.assertEqual(res["action"], "OBSERVE_DATA_MISSING") def test_exit_score_dominates_with_risk_off_and_time_stop(self): res = compute_timing_decision( { "priceStatus": "PRICE_OK", "atr20": 100.0, "entryModeGate": "PASS", "entryMode": "PULLBACK", "leaderTotal": 3, "leaderGate": "PASS", "flowCredit": 0.6, "acGate": "BLOCK", "ma20Slope": -1.0, "disparity": 13.0, "rsi14": 80.0, "avgTradeValue5D": 100.0, "spreadPct": 0.5, "rwPartial": 4, "daysToTimeStop": 3, "profitPct": 12.0, } ) self.assertEqual(res["action"], "STOP_OR_TIME_EXIT_READY") self.assertGreaterEqual(res["exit_score"], 75) self.assertGreaterEqual(res["entry_score"], 0) if __name__ == "__main__": unittest.main()