71 lines
1.8 KiB
Python
71 lines
1.8 KiB
Python
"""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
|