feat: implement phase1 state machine, composite exits, and kill-switch orchestration (#275)
Some checks are pending
CI / test (pull_request) Waiting to run
Some checks are pending
CI / test (pull_request) Waiting to run
This commit is contained in:
55
tests/test_kill_switch.py
Normal file
55
tests/test_kill_switch.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import pytest
|
||||
|
||||
from src.core.kill_switch import KillSwitchOrchestrator
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kill_switch_executes_steps_in_order() -> None:
|
||||
ks = KillSwitchOrchestrator()
|
||||
calls: list[str] = []
|
||||
|
||||
async def _cancel() -> None:
|
||||
calls.append("cancel")
|
||||
|
||||
def _refresh() -> None:
|
||||
calls.append("refresh")
|
||||
|
||||
def _reduce() -> None:
|
||||
calls.append("reduce")
|
||||
|
||||
def _snapshot() -> None:
|
||||
calls.append("snapshot")
|
||||
|
||||
def _notify() -> None:
|
||||
calls.append("notify")
|
||||
|
||||
report = await ks.trigger(
|
||||
reason="test",
|
||||
cancel_pending_orders=_cancel,
|
||||
refresh_order_state=_refresh,
|
||||
reduce_risk=_reduce,
|
||||
snapshot_state=_snapshot,
|
||||
notify=_notify,
|
||||
)
|
||||
|
||||
assert report.steps == [
|
||||
"block_new_orders",
|
||||
"cancel_pending_orders",
|
||||
"refresh_order_state",
|
||||
"reduce_risk",
|
||||
"snapshot_state",
|
||||
"notify",
|
||||
]
|
||||
assert calls == ["cancel", "refresh", "reduce", "snapshot", "notify"]
|
||||
assert report.errors == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kill_switch_collects_step_errors() -> None:
|
||||
ks = KillSwitchOrchestrator()
|
||||
|
||||
def _boom() -> None:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
report = await ks.trigger(reason="test", cancel_pending_orders=_boom)
|
||||
assert any(err.startswith("cancel_pending_orders:") for err in report.errors)
|
||||
38
tests/test_strategy_exit_rules.py
Normal file
38
tests/test_strategy_exit_rules.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from src.strategy.exit_rules import ExitRuleConfig, ExitRuleInput, evaluate_exit
|
||||
from src.strategy.position_state_machine import PositionState
|
||||
|
||||
|
||||
def test_hard_stop_exit() -> None:
|
||||
out = evaluate_exit(
|
||||
current_state=PositionState.HOLDING,
|
||||
config=ExitRuleConfig(hard_stop_pct=-2.0, arm_pct=3.0),
|
||||
inp=ExitRuleInput(current_price=97.0, entry_price=100.0, peak_price=100.0),
|
||||
)
|
||||
assert out.should_exit is True
|
||||
assert out.reason == "hard_stop"
|
||||
|
||||
|
||||
def test_take_profit_exit_for_backward_compatibility() -> None:
|
||||
out = evaluate_exit(
|
||||
current_state=PositionState.HOLDING,
|
||||
config=ExitRuleConfig(hard_stop_pct=-2.0, arm_pct=3.0),
|
||||
inp=ExitRuleInput(current_price=104.0, entry_price=100.0, peak_price=104.0),
|
||||
)
|
||||
assert out.should_exit is True
|
||||
assert out.reason == "arm_take_profit"
|
||||
|
||||
|
||||
def test_model_assist_exit_signal() -> None:
|
||||
out = evaluate_exit(
|
||||
current_state=PositionState.ARMED,
|
||||
config=ExitRuleConfig(model_prob_threshold=0.62, arm_pct=10.0),
|
||||
inp=ExitRuleInput(
|
||||
current_price=101.0,
|
||||
entry_price=100.0,
|
||||
peak_price=105.0,
|
||||
pred_down_prob=0.8,
|
||||
liquidity_weak=True,
|
||||
),
|
||||
)
|
||||
assert out.should_exit is True
|
||||
assert out.reason == "model_liquidity_exit"
|
||||
30
tests/test_strategy_state_machine.py
Normal file
30
tests/test_strategy_state_machine.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from src.strategy.position_state_machine import (
|
||||
PositionState,
|
||||
StateTransitionInput,
|
||||
promote_state,
|
||||
)
|
||||
|
||||
|
||||
def test_gap_jump_promotes_to_armed_directly() -> None:
|
||||
state = promote_state(
|
||||
PositionState.HOLDING,
|
||||
StateTransitionInput(
|
||||
unrealized_pnl_pct=4.0,
|
||||
be_arm_pct=1.2,
|
||||
arm_pct=2.8,
|
||||
),
|
||||
)
|
||||
assert state == PositionState.ARMED
|
||||
|
||||
|
||||
def test_exited_has_priority_over_promotion() -> None:
|
||||
state = promote_state(
|
||||
PositionState.HOLDING,
|
||||
StateTransitionInput(
|
||||
unrealized_pnl_pct=5.0,
|
||||
be_arm_pct=1.2,
|
||||
arm_pct=2.8,
|
||||
hard_stop_hit=True,
|
||||
),
|
||||
)
|
||||
assert state == PositionState.EXITED
|
||||
Reference in New Issue
Block a user