""" Parity test for execution_decision_v1.py against GAS source. F05: Exit/sell action decision logic. Tests calc_exit_sell_action() with core priorities and edge cases. Source: src/gas_adapter_parts/gdf_01_price_metrics.gs:calcExitSellAction_ """ import pytest from formulas.execution_decision_v1 import calc_exit_sell_action class TestExitSellActionPriorities: """Test exit/sell action priority hierarchy.""" def test_hold_default(self): """Default HOLD when no signals trigger.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 5, "rwPartial": 0, }) assert result["action"] == "HOLD" def test_priority_1_stop_action(self): """Priority 1: STOP_OR_TIME_EXIT_READY → EXIT_100.""" result = calc_exit_sell_action({ "close": 100, "stopPrice": 95, "timingAction": "STOP_OR_TIME_EXIT_READY", }) assert result["action"] == "EXIT_100" assert result["ratio_pct"] == 100 def test_priority_1_strong_rw(self): """Priority 1: rwPartial >= 4 → EXIT_100.""" result = calc_exit_sell_action({ "close": 100, "stopPrice": 95, "rwPartial": 4, }) assert result["action"] == "EXIT_100" def test_priority_5_profit_50(self): """Priority 5: profitPct >= 50 → PROFIT_TRIM_50.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 55, "tp2Price": 155, "atr20": 2, }) assert result["action"] == "PROFIT_TRIM_50" assert result["ratio_pct"] == 50 def test_priority_5_take_profit_tier1(self): """Priority 5: profitPct >= 10 → TAKE_PROFIT_TIER1.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 15, "tp1Price": 115, "atr20": 2, }) assert result["action"] == "TAKE_PROFIT_TIER1" assert result["ratio_pct"] == 25 def test_priority_4_trailing_stop_breach(self): """Priority 4: close <= trailingStop → TRAILING_STOP_BREACH.""" result = calc_exit_sell_action({ "close": 95, "trailingStop": 98, "atr20": 2, }) assert result["action"] == "TRAILING_STOP_BREACH" assert result["ratio_pct"] == 70 def test_priority_4_rw_medium(self): """Priority 4: rwPartial >= 2 → TRIM_50.""" result = calc_exit_sell_action({ "close": 100, "atr20": 2, "rwPartial": 2, }) assert result["action"] == "TRIM_50" assert result["ratio_pct"] == 50 def test_priority_6_time_stop_near(self): """Priority 6: daysToTimeStop <= 7 → TIME_TRIM_50.""" result = calc_exit_sell_action({ "close": 100, "atr20": 2, "daysToTimeStop": 5, "profitPct": 0, "rwPartial": 0, }) assert result["action"] == "TIME_TRIM_50" assert result["ratio_pct"] == 50 def test_price_source_tier2(self): """When tp2Price available, use it for PROFIT_TRIM_50.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 55, "tp2Price": 155, "atr20": 2, }) assert result["price_source"] == "TP2_PRICE" assert result["price_basis"] == "TAKE_PROFIT_TIER2_PRICE" def test_price_fallback_to_close_protect(self): """When tp2Price absent, use closeProtectLimit.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 55, "atr20": 2, }) assert result["price_source"] == "CLOSE_PROFIT_PROTECT" assert result["price_basis"] == "PRIOR_CLOSE_X_0.998" def test_validation_confirmed(self): """Validation = SIGNAL_CONFIRMED when price valid.""" result = calc_exit_sell_action({ "close": 100, "stopPrice": 95, "timingAction": "STOP_OR_TIME_EXIT_READY", }) assert result["validation"] == "SIGNAL_CONFIRMED" def test_validation_hold_no_action(self): """Validation = NO_SELL_ACTION when HOLD.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 5, }) assert result["validation"] == "NO_SELL_ACTION" def test_rw_early_warning_trim_33(self): """Priority 4b: rwPartial >= 1 + timingExitScore >= 30 → TRIM_33.""" result = calc_exit_sell_action({ "close": 100, "atr20": 2, "rwPartial": 1, "timingExitScore": 35, }) assert result["action"] == "TRIM_33" assert result["ratio_pct"] == 33 def test_rw_signal_only_trim_25(self): """Priority 4c: rwPartial >= 1 only → TRIM_25.""" result = calc_exit_sell_action({ "close": 100, "atr20": 2, "rwPartial": 1, "timingExitScore": 0, }) assert result["action"] == "TRIM_25" assert result["ratio_pct"] == 25 def test_profit_trim_35(self): """Priority 5: profitPct >= 30 → PROFIT_TRIM_35.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 35, "tp2Price": 135, "atr20": 2, }) assert result["action"] == "PROFIT_TRIM_35" assert result["ratio_pct"] == 35 def test_profit_trim_25(self): """Priority 5: profitPct >= 20 → PROFIT_TRIM_25.""" result = calc_exit_sell_action({ "close": 100, "profitPct": 25, "tp1Price": 125, "atr20": 2, }) assert result["action"] == "PROFIT_TRIM_25" assert result["ratio_pct"] == 25 def test_time_stop_approaching(self): """Priority 6b: daysToTimeStop <= 14 → TIME_TRIM_25.""" result = calc_exit_sell_action({ "close": 100, "atr20": 2, "daysToTimeStop": 10, "profitPct": 0, "rwPartial": 0, }) assert result["action"] == "TIME_TRIM_25" assert result["ratio_pct"] == 25