Files
KArtSell.Aegis/research/hardening/kartsell_policy_contract.py
T
kjh2064 dcd1322d41
ci / backend (push) Failing after 12s
ci / frontend (push) Failing after 19s
ci / static (push) Failing after 45s
Initial commit: Add project files
2026-08-02 05:15:36 +09:00

225 lines
7.4 KiB
Python

"""Executable research contract for K-ArtSell Aegis sell/reentry policies.
This module is intentionally dependency-free. It is not a production trading engine.
It exists to make policy ordering, units, invariants, and golden vectors reproducible.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal, ROUND_HALF_EVEN
from enum import Enum
from typing import Iterable, Optional, Protocol
ZERO = Decimal("0")
ONE = Decimal("1")
class SellAction(str, Enum):
HOLD = "HOLD"
PARTIAL_SELL = "PARTIAL_SELL"
FULL_SELL = "FULL_SELL"
@dataclass(frozen=True)
class SellInput:
position_lot_id: str
cycle_id: str
evidence_id: str
dataset_id: str
model_version: str
config_version: str
code_sha: str
as_of: datetime
published_at_cutoff: datetime
current_portfolio_weight: Decimal
strategic_core_floor_weight: Decimal
hard_impairment_approved: bool = False
capital_floor_breached: bool = False
survival_sell_ratio_of_lot: Decimal = ZERO
gap_below_floor_atr: Decimal = ZERO
consecutive_close_breaches: int = 0
cooldown_satisfied: bool = True
concentration_sell_ratio_of_lot: Decimal = ZERO
opportunity_edge_lower_bound: Decimal = ZERO
opportunity_sell_ratio_of_lot: Decimal = ZERO
def validate(self) -> None:
if self.published_at_cutoff > self.as_of:
raise ValueError("published_at_cutoff cannot be later than as_of")
for name, value in (
("current_portfolio_weight", self.current_portfolio_weight),
("strategic_core_floor_weight", self.strategic_core_floor_weight),
("survival_sell_ratio_of_lot", self.survival_sell_ratio_of_lot),
("concentration_sell_ratio_of_lot", self.concentration_sell_ratio_of_lot),
("opportunity_sell_ratio_of_lot", self.opportunity_sell_ratio_of_lot),
):
if not ZERO <= value <= ONE:
raise ValueError(f"{name} must be between 0 and 1")
if self.strategic_core_floor_weight > self.current_portfolio_weight:
raise ValueError("strategic core cannot exceed current portfolio weight")
if self.gap_below_floor_atr < ZERO:
raise ValueError("gap_below_floor_atr cannot be negative")
if self.consecutive_close_breaches < 0:
raise ValueError("consecutive_close_breaches cannot be negative")
def max_sell_ratio_preserving_core(self) -> Decimal:
if self.current_portfolio_weight <= ZERO:
return ZERO
sellable_weight = max(ZERO, self.current_portfolio_weight - self.strategic_core_floor_weight)
return min(ONE, sellable_weight / self.current_portfolio_weight)
def target_weight_after(self, ratio: Decimal) -> Decimal:
bounded = min(ONE, max(ZERO, ratio))
return (self.current_portfolio_weight * (ONE - bounded)).quantize(
Decimal("0.00000001"), rounding=ROUND_HALF_EVEN
)
@dataclass(frozen=True)
class SellDecision:
action: SellAction
sell_ratio_of_lot: Decimal
target_portfolio_weight_after: Decimal
policy_id: str
priority: int
reason_code: str
reentry_eligible: bool
class SellPolicy(Protocol):
priority: int
policy_id: str
def evaluate(self, value: SellInput) -> Optional[SellDecision]: ...
class HardImpairmentPolicy:
priority = 1000
policy_id = "ALG-SELL-001"
def evaluate(self, value: SellInput) -> Optional[SellDecision]:
if not value.hard_impairment_approved:
return None
return _decision(value, ONE, self.policy_id, self.priority, "HARD_IMPAIRMENT", False)
class PortfolioSurvivalPolicy:
priority = 900
policy_id = "ALG-SELL-PORT-001"
def evaluate(self, value: SellInput) -> Optional[SellDecision]:
if not value.capital_floor_breached or not value.cooldown_satisfied:
return None
ratio = min(ONE, max(ZERO, value.survival_sell_ratio_of_lot))
return None if ratio == ZERO else _decision(
value, ratio, self.policy_id, self.priority, "PORTFOLIO_SURVIVAL", True
)
class GapFloorPolicy:
priority = 800
policy_id = "ALG-SELL-002"
def evaluate(self, value: SellInput) -> Optional[SellDecision]:
if not value.cooldown_satisfied or value.gap_below_floor_atr < Decimal("1.5"):
return None
ratio = min(Decimal("0.40"), value.max_sell_ratio_preserving_core())
return None if ratio == ZERO else _decision(
value, ratio, self.policy_id, self.priority, "GAP_FLOOR_BREACH", True
)
class TwoClosePolicy:
priority = 700
policy_id = "ALG-SELL-003"
def evaluate(self, value: SellInput) -> Optional[SellDecision]:
if not value.cooldown_satisfied or value.consecutive_close_breaches < 2:
return None
ratio = min(Decimal("0.20"), value.max_sell_ratio_preserving_core())
return None if ratio == ZERO else _decision(
value, ratio, self.policy_id, self.priority, "TWO_CLOSE_FLOOR_BREACH", True
)
class ConcentrationPolicy:
priority = 600
policy_id = "ALG-SELL-004"
def evaluate(self, value: SellInput) -> Optional[SellDecision]:
if not value.cooldown_satisfied:
return None
ratio = min(value.concentration_sell_ratio_of_lot, value.max_sell_ratio_preserving_core())
return None if ratio <= ZERO else _decision(
value, ratio, self.policy_id, self.priority, "CONCENTRATION_OR_LIQUIDITY", True
)
class OpportunityCostPolicy:
priority = 500
policy_id = "ALG-SELL-005"
def evaluate(self, value: SellInput) -> Optional[SellDecision]:
if not value.cooldown_satisfied or value.opportunity_edge_lower_bound <= ZERO:
return None
requested = min(Decimal("0.25"), max(Decimal("0.10"), value.opportunity_sell_ratio_of_lot))
ratio = min(requested, value.max_sell_ratio_preserving_core())
return None if ratio <= ZERO else _decision(
value, ratio, self.policy_id, self.priority, "OPPORTUNITY_REPLACEMENT", True
)
class SellPolicyChain:
def __init__(self, policies: Iterable[SellPolicy]):
self._policies = tuple(sorted(policies, key=lambda p: (-p.priority, p.policy_id)))
def evaluate(self, value: SellInput) -> SellDecision:
value.validate()
for policy in self._policies:
decision = policy.evaluate(value)
if decision is not None:
return decision
return SellDecision(
SellAction.HOLD,
ZERO,
value.current_portfolio_weight,
"ALG-HOLD-001",
0,
"NO_SELL_CONDITION",
False,
)
def default_chain() -> SellPolicyChain:
return SellPolicyChain(
[
OpportunityCostPolicy(),
ConcentrationPolicy(),
TwoClosePolicy(),
GapFloorPolicy(),
PortfolioSurvivalPolicy(),
HardImpairmentPolicy(),
]
)
def _decision(
value: SellInput,
ratio: Decimal,
policy_id: str,
priority: int,
reason: str,
reentry_eligible: bool,
) -> SellDecision:
action = SellAction.FULL_SELL if ratio == ONE else SellAction.PARTIAL_SELL
return SellDecision(
action,
ratio,
value.target_weight_after(ratio),
policy_id,
priority,
reason,
reentry_eligible,
)