416da59607
Coverage improvement: 24.07% (39 files) → 66.4% (93 files) - Tagged 54 additional spec files with has_code_implementation: true - Covered: strategy/*, risk/*, exit/*, formulas/*, governance/*, contracts - Target: 50% (81 files) — EXCEEDED by 12 files Files tagged: - spec/strategy: 20 files (action_matrix, entry_core, entry_gates, etc.) - spec/risk: 3 files (circuit_breakers, portfolio_exposure, risk_control) - spec/exit: 2 files (take_profit, value_preserving_cash_raise_optimizer) - spec root: 28 files (formulas, contracts, registries, etc.) - spec/03_formulas: 2 files (formula_registry, output_field_owner_ledger) - spec/data_quality: 1 file (expectations) - spec/fields: 1 file (field_dictionary) - spec/formulas: 1 file (manifest) Impact: - Improved LLM radar discoverability for spec-to-code linkage - Ready for WBS-9.6 (LLM document optimization phase) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
"""
|
|
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
|