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:
70
src/strategy/position_state_machine.py
Normal file
70
src/strategy/position_state_machine.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Position state machine for staged exit control.
|
||||
|
||||
State progression is monotonic (promotion-only) except terminal EXITED.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PositionState(str, Enum):
|
||||
HOLDING = "HOLDING"
|
||||
BE_LOCK = "BE_LOCK"
|
||||
ARMED = "ARMED"
|
||||
EXITED = "EXITED"
|
||||
|
||||
|
||||
_STATE_RANK: dict[PositionState, int] = {
|
||||
PositionState.HOLDING: 0,
|
||||
PositionState.BE_LOCK: 1,
|
||||
PositionState.ARMED: 2,
|
||||
PositionState.EXITED: 3,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateTransitionInput:
|
||||
unrealized_pnl_pct: float
|
||||
be_arm_pct: float
|
||||
arm_pct: float
|
||||
hard_stop_hit: bool = False
|
||||
trailing_stop_hit: bool = False
|
||||
model_exit_signal: bool = False
|
||||
be_lock_threat: bool = False
|
||||
|
||||
|
||||
def evaluate_exit_first(inp: StateTransitionInput) -> bool:
|
||||
"""Return True when terminal exit conditions are met.
|
||||
|
||||
EXITED must be evaluated before any promotion.
|
||||
"""
|
||||
return (
|
||||
inp.hard_stop_hit
|
||||
or inp.trailing_stop_hit
|
||||
or inp.model_exit_signal
|
||||
or inp.be_lock_threat
|
||||
)
|
||||
|
||||
|
||||
def promote_state(current: PositionState, inp: StateTransitionInput) -> PositionState:
|
||||
"""Promote to highest admissible state for current tick/bar.
|
||||
|
||||
Rules:
|
||||
- EXITED has highest precedence and is terminal.
|
||||
- Promotions are monotonic (no downgrade).
|
||||
"""
|
||||
if current == PositionState.EXITED:
|
||||
return PositionState.EXITED
|
||||
|
||||
if evaluate_exit_first(inp):
|
||||
return PositionState.EXITED
|
||||
|
||||
target = PositionState.HOLDING
|
||||
if inp.unrealized_pnl_pct >= inp.arm_pct:
|
||||
target = PositionState.ARMED
|
||||
elif inp.unrealized_pnl_pct >= inp.be_arm_pct:
|
||||
target = PositionState.BE_LOCK
|
||||
|
||||
return target if _STATE_RANK[target] > _STATE_RANK[current] else current
|
||||
Reference in New Issue
Block a user