Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Task 18 — OrderRequest + AlpacaBroker BRACKET support:
- OrderRequest gains order_class ("simple" | "bracket"),
take_profit_price, stop_loss_price + model_validator that requires
both legs when order_class == "bracket".
- AlpacaBroker._build_order_request branches to a MarketOrderRequest
with OrderClass.BRACKET + TakeProfitRequest + StopLossRequest legs,
TimeInForce.GTC so the bracket survives day boundaries.
Task 19 — RiskManager Kevin caps + circuit-breaker:
- TradeExecutorConfig gains 4 fields: kevin_daily_trade_cap,
kevin_daily_alloc_cap_usd, kevin_equity_drawdown_halt_pct,
kevin_daily_loss_circuit_pct.
- check_risk() applies the caps only when
signal.strategy_id == KEVIN_STRATEGY_UUID; non-Kevin signals pass
through the existing path unchanged.
- 4 new checks in order: drawdown halt (sets permanent
trading:paused), daily-loss circuit (setex 24h), daily trade-count
cap, daily allocation cap (rolling today's $ + this trade's
notional).
- Counter keys: kevin:daily_trades:YYYY-MM-DD,
kevin:daily_alloc_usd:YYYY-MM-DD, kevin:daily_pnl_usd:YYYY-MM-DD,
kevin:starting_equity_usd. All read-only here; bridge + executor
write them.
Tests: 5 bracket + 9 kevin-caps + 28 regression-safe. Total 67 + 14
new = 81 passing (excluding -m integration). No DB needed.
216 lines
5.6 KiB
Python
216 lines
5.6 KiB
Python
"""Trading-related Pydantic schemas for Redis Streams messages and API payloads."""
|
|
|
|
from datetime import UTC, datetime
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Any, Literal
|
|
from uuid import UUID, uuid4
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
class OrderType(str, Enum):
|
|
MARKET = "market"
|
|
LIMIT = "limit"
|
|
STOP = "stop"
|
|
|
|
|
|
class OrderSide(str, Enum):
|
|
BUY = "BUY"
|
|
SELL = "SELL"
|
|
|
|
|
|
class OrderStatus(str, Enum):
|
|
PENDING = "PENDING"
|
|
FILLED = "FILLED"
|
|
CANCELLED = "CANCELLED"
|
|
REJECTED = "REJECTED"
|
|
|
|
|
|
class SignalDirection(str, Enum):
|
|
LONG = "LONG"
|
|
SHORT = "SHORT"
|
|
NEUTRAL = "NEUTRAL"
|
|
EXIT = "EXIT"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API request / response schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class OrderRequest(BaseModel):
|
|
"""Submitted by the trade executor or the API to place an order."""
|
|
|
|
ticker: str
|
|
side: OrderSide
|
|
qty: float = Field(gt=0)
|
|
order_type: OrderType = OrderType.MARKET
|
|
limit_price: float | None = None
|
|
stop_price: float | None = None
|
|
|
|
order_class: Literal["simple", "bracket"] = "simple"
|
|
take_profit_price: float | None = None
|
|
stop_loss_price: float | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
@model_validator(mode="after")
|
|
def _bracket_requires_legs(self) -> "OrderRequest":
|
|
if self.order_class == "bracket" and (
|
|
self.take_profit_price is None or self.stop_loss_price is None
|
|
):
|
|
raise ValueError(
|
|
"bracket orders require take_profit_price + stop_loss_price"
|
|
)
|
|
return self
|
|
|
|
|
|
class OrderResult(BaseModel):
|
|
"""Returned after order submission or status query."""
|
|
|
|
order_id: str
|
|
ticker: str
|
|
side: OrderSide
|
|
qty: float
|
|
filled_price: float | None = None
|
|
status: OrderStatus
|
|
timestamp: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class PositionInfo(BaseModel):
|
|
"""Current position state — used in API responses and portfolio views."""
|
|
|
|
ticker: str
|
|
qty: float
|
|
avg_entry: float
|
|
current_price: float
|
|
unrealized_pnl: float
|
|
market_value: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class AccountInfo(BaseModel):
|
|
"""Account-level summary from the brokerage."""
|
|
|
|
equity: float
|
|
cash: float
|
|
buying_power: float
|
|
portfolio_value: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Redis Stream message schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TradeSignal(BaseModel):
|
|
"""Published to ``signals:generated`` by the signal generator."""
|
|
|
|
signal_id: UUID = Field(default_factory=uuid4)
|
|
ticker: str
|
|
direction: SignalDirection
|
|
strength: float = Field(ge=0.0, le=1.0)
|
|
strategy_sources: list[str]
|
|
sentiment_context: dict[str, Any] | None = None
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
|
|
# --- Kevin v2 extensions (optional) ---
|
|
strategy_id: UUID | None = None
|
|
target_dollars: Decimal | None = None
|
|
stop_loss_pct: Decimal | None = None
|
|
take_profit_pct: Decimal | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TradeExecution(BaseModel):
|
|
"""Published to ``trades:executed`` by the trade executor."""
|
|
|
|
trade_id: UUID
|
|
ticker: str
|
|
side: OrderSide
|
|
qty: float
|
|
price: float
|
|
status: OrderStatus
|
|
signal_id: UUID | None = None
|
|
strategy_id: UUID | None = None
|
|
strategy_sources: list[str] = Field(default_factory=list)
|
|
timestamp: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class OHLCVBar(BaseModel):
|
|
"""Single OHLCV bar."""
|
|
|
|
timestamp: datetime
|
|
open: float
|
|
high: float
|
|
low: float
|
|
close: float
|
|
volume: float
|
|
|
|
|
|
class FundamentalsSnapshot(BaseModel):
|
|
"""Fundamental financial data for a single ticker — cached daily."""
|
|
|
|
ticker: str
|
|
eps_ttm: float | None = None
|
|
pe_ratio: float | None = None
|
|
peg_ratio: float | None = None
|
|
revenue_growth_yoy: float | None = None
|
|
profit_margin: float | None = None
|
|
debt_to_equity: float | None = None
|
|
market_cap: float | None = None
|
|
fetched_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class MarketSnapshot(BaseModel):
|
|
"""Snapshot of market data for a single ticker — used by strategies."""
|
|
|
|
ticker: str
|
|
current_price: float
|
|
open: float
|
|
high: float
|
|
low: float
|
|
close: float
|
|
volume: float
|
|
sma_20: float | None = None
|
|
sma_50: float | None = None
|
|
rsi: float | None = None
|
|
# Technical indicators — computed by MarketDataManager
|
|
ema_9: float | None = None
|
|
ema_21: float | None = None
|
|
sma_200: float | None = None
|
|
macd: float | None = None
|
|
macd_signal: float | None = None
|
|
macd_histogram: float | None = None
|
|
bollinger_upper: float | None = None
|
|
bollinger_mid: float | None = None
|
|
bollinger_lower: float | None = None
|
|
vwap: float | None = None
|
|
atr: float | None = None
|
|
bars: list[dict[str, Any]] = Field(default_factory=list)
|
|
fundamentals: FundamentalsSnapshot | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SentimentContext(BaseModel):
|
|
"""Aggregated sentiment for a ticker — passed to strategies."""
|
|
|
|
ticker: str
|
|
avg_score: float = Field(ge=-1.0, le=1.0)
|
|
article_count: int = Field(ge=0)
|
|
recent_scores: list[float] = Field(default_factory=list)
|
|
avg_confidence: float = Field(ge=0.0, le=1.0)
|
|
|
|
model_config = {"from_attributes": True}
|