Files
The-Ouroboros/src/strategy/position_state_machine.py
agentson 5e4c94bfeb
All checks were successful
Gitea CI / test (push) Successful in 32s
Gitea CI / test (pull_request) Successful in 33s
strategy: implement model assist be-lock path and clarify audit note
2026-03-02 09:31:38 +09:00

70 lines
1.9 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 StrEnum
class PositionState(StrEnum):
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.
"""
# model_exit_signal is assist-only and must not trigger EXIT directly.
return inp.hard_stop_hit or inp.trailing_stop_hit 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
elif inp.model_exit_signal:
# Model signal assists risk posture by tightening to BE_LOCK.
target = PositionState.BE_LOCK
return target if _STATE_RANK[target] > _STATE_RANK[current] else current