trading/shared/schemas/kevin.py
Viktor Barzin 9d752aa0a2
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
feat(kevin): KevinDecision + KevinAccountState schemas
Standalone schemas (no BaseStrategy coupling) used by both the live
signal bridge and the backtest mini-engine.
2026-05-24 00:44:57 +00:00

66 lines
2.3 KiB
Python

"""Pydantic schemas for the Kevin strategy.
Used by KevinStrategy.evaluate_mention as input/output contracts and by
the live signal bridge + backtest engine to talk to the strategy.
"""
from __future__ import annotations
from decimal import Decimal
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, model_validator
class KevinDecisionType(str, Enum):
OPEN_LONG = "open_long" # new position or top-up
CLOSE_LONG = "close_long" # exit existing long
NO_OP = "no_op" # filter says skip
class KevinDecision(BaseModel):
"""A single trade decision emitted by KevinStrategy.evaluate_mention."""
model_config = ConfigDict(frozen=True)
decision: KevinDecisionType
symbol: str = Field(min_length=1, max_length=16)
target_dollars: Decimal | None = None # required for OPEN_LONG
holding_days: int | None = None # required for OPEN_LONG
effective_conviction: Decimal | None = None # post-aggregation, 0-1
rationale: str # one-line audit string
@model_validator(mode="after")
def _open_long_requires_target_dollars(self) -> "KevinDecision":
if self.decision == KevinDecisionType.OPEN_LONG:
if self.target_dollars is None:
raise ValueError("OPEN_LONG requires target_dollars")
if self.holding_days is None:
raise ValueError("OPEN_LONG requires holding_days")
if self.target_dollars <= 0:
raise ValueError("target_dollars must be positive")
return self
class KevinAccountState(BaseModel):
"""Snapshot of the account passed to KevinStrategy.evaluate_mention.
The bridge populates this from live Alpaca account + Redis counters; the
backtest populates it from the simulated portfolio state. Same shape.
"""
model_config = ConfigDict(frozen=True)
equity_usd: Decimal
cash_usd: Decimal
held_positions: dict[str, Decimal] # symbol -> cost-basis $
blocklisted_symbols: frozenset[str] | set[str]
daily_trade_count: int
daily_alloc_usd: Decimal
paused: bool
def is_held(self, symbol: str) -> bool:
return symbol in self.held_positions and self.held_positions[symbol] > 0
def is_blocklisted(self, symbol: str) -> bool:
return symbol in self.blocklisted_symbols