trading/shared/schemas/trading.py
Viktor Barzin e2a3bd456d
feat: real data pipeline — market data, DB persistence, portfolio sync, signal-trade linkage
Wire the trading bot to real Alpaca market data and persist pipeline
state to the database so the dashboard displays live information.

- Add market-data service fetching OHLCV bars from Alpaca, publishing
  to market:bars Redis Stream; signal generator consumes bars and
  injects current_price into signals for position sizing
- Sentiment analyzer now persists Article + ArticleSentiment rows to
  DB after scoring, with duplicate and error handling
- API gateway runs a background portfolio sync task that snapshots
  Alpaca account state into PortfolioSnapshot/Position DB tables
  during market hours
- TradeSignal carries a signal_id UUID; signal generator and trade
  executor both persist their records to DB with cross-references
- 303 unit tests pass (57 new tests added)
2026-02-22 19:52:45 +00:00

164 lines
3.8 KiB
Python

"""Trading-related Pydantic schemas for Redis Streams messages and API payloads."""
from datetime import datetime
from enum import Enum
from typing import Any
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
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"
# ---------------------------------------------------------------------------
# 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
model_config = {"from_attributes": True}
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
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
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 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
bars: list[dict[str, Any]] = Field(default_factory=list)
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}