feat: pydantic schemas for all service message types
- shared/schemas/trading.py: OrderRequest, OrderResult, PositionInfo, AccountInfo, TradeSignal, TradeExecution, MarketSnapshot, SentimentContext - shared/schemas/news.py: RawArticle, ScoredArticle - shared/schemas/learning.py: TradeOutcomeSchema, WeightAdjustment - shared/schemas/auth.py: RegisterRequest, LoginRequest, TokenResponse - 49 schema tests covering validation constraints, serialization round-trips, required fields, and range checks
This commit is contained in:
parent
72cb1b6fe5
commit
c8277e301e
11 changed files with 889 additions and 0 deletions
37
shared/schemas/__init__.py
Normal file
37
shared/schemas/__init__.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Pydantic v2 schemas for all service message types."""
|
||||
|
||||
from shared.schemas.trading import (
|
||||
AccountInfo,
|
||||
MarketSnapshot,
|
||||
OrderRequest,
|
||||
OrderResult,
|
||||
PositionInfo,
|
||||
SentimentContext,
|
||||
TradeExecution,
|
||||
TradeSignal,
|
||||
)
|
||||
from shared.schemas.news import RawArticle, ScoredArticle
|
||||
from shared.schemas.learning import TradeOutcomeSchema, WeightAdjustment
|
||||
from shared.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
|
||||
|
||||
__all__ = [
|
||||
# Trading
|
||||
"OrderRequest",
|
||||
"OrderResult",
|
||||
"PositionInfo",
|
||||
"AccountInfo",
|
||||
"TradeSignal",
|
||||
"TradeExecution",
|
||||
"MarketSnapshot",
|
||||
"SentimentContext",
|
||||
# News
|
||||
"RawArticle",
|
||||
"ScoredArticle",
|
||||
# Learning
|
||||
"TradeOutcomeSchema",
|
||||
"WeightAdjustment",
|
||||
# Auth
|
||||
"RegisterRequest",
|
||||
"LoginRequest",
|
||||
"TokenResponse",
|
||||
]
|
||||
BIN
shared/schemas/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
shared/schemas/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
shared/schemas/__pycache__/auth.cpython-314.pyc
Normal file
BIN
shared/schemas/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
shared/schemas/__pycache__/learning.cpython-314.pyc
Normal file
BIN
shared/schemas/__pycache__/learning.cpython-314.pyc
Normal file
Binary file not shown.
BIN
shared/schemas/__pycache__/news.cpython-314.pyc
Normal file
BIN
shared/schemas/__pycache__/news.cpython-314.pyc
Normal file
Binary file not shown.
BIN
shared/schemas/__pycache__/trading.cpython-314.pyc
Normal file
BIN
shared/schemas/__pycache__/trading.cpython-314.pyc
Normal file
Binary file not shown.
27
shared/schemas/auth.py
Normal file
27
shared/schemas/auth.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""Authentication Pydantic schemas for API request/response payloads."""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
"""Sent by the dashboard to begin passkey registration."""
|
||||
|
||||
username: str = Field(min_length=1, max_length=100)
|
||||
display_name: str | None = None
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""Sent by the dashboard to begin passkey authentication."""
|
||||
|
||||
username: str = Field(min_length=1, max_length=100)
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Returned after successful authentication."""
|
||||
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int = Field(
|
||||
description="Access token lifetime in seconds", default=900
|
||||
)
|
||||
32
shared/schemas/learning.py
Normal file
32
shared/schemas/learning.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Learning domain Pydantic schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TradeOutcomeSchema(BaseModel):
|
||||
"""Represents the evaluated outcome of a closed trade."""
|
||||
|
||||
trade_id: UUID
|
||||
hold_duration_seconds: float = Field(ge=0)
|
||||
realized_pnl: float
|
||||
roi_pct: float
|
||||
was_profitable: bool
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class WeightAdjustment(BaseModel):
|
||||
"""Represents a strategy weight change made by the learning engine."""
|
||||
|
||||
strategy_id: UUID
|
||||
strategy_name: str
|
||||
old_weight: float
|
||||
new_weight: float
|
||||
reason: str
|
||||
reward_signal: float
|
||||
timestamp: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
44
shared/schemas/news.py
Normal file
44
shared/schemas/news.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""News article Pydantic schemas for Redis Stream messages."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RawArticle(BaseModel):
|
||||
"""Published to ``news:raw`` by the news fetcher."""
|
||||
|
||||
source: str
|
||||
url: str
|
||||
title: str
|
||||
content: str
|
||||
published_at: datetime | None = None
|
||||
fetched_at: datetime
|
||||
content_hash: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ScoredArticle(BaseModel):
|
||||
"""Published to ``news:scored`` by the sentiment analyzer.
|
||||
|
||||
Inherits all fields from RawArticle conceptually plus scoring metadata.
|
||||
"""
|
||||
|
||||
# Original article fields
|
||||
source: str
|
||||
url: str
|
||||
title: str
|
||||
content: str
|
||||
published_at: datetime | None = None
|
||||
fetched_at: datetime
|
||||
content_hash: str
|
||||
|
||||
# Scoring fields
|
||||
ticker: str
|
||||
sentiment_score: float = Field(ge=-1.0, le=1.0)
|
||||
confidence: float = Field(ge=0.0, le=1.0)
|
||||
model_used: str
|
||||
entities: list[str] = Field(default_factory=list)
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
163
shared/schemas/trading.py
Normal file
163
shared/schemas/trading.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"""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
|
||||
|
||||
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."""
|
||||
|
||||
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}
|
||||
Loading…
Add table
Add a link
Reference in a new issue