from datetime import datetime, date from pydantic import BaseModel, field_validator # ─── Device ─────────────────────────────────────────────────────────────────── class DeviceRegister(BaseModel): apns_token: str device_name: str | None = None class DeviceResponse(BaseModel): id: int apns_token: str device_name: str | None registered_at: datetime model_config = {"from_attributes": True} # ─── Stock Portfolio ─────────────────────────────────────────────────────────── class StockPositionCreate(BaseModel): ticker: str shares: int cost_basis: float | None = None @field_validator("ticker") @classmethod def uppercase_ticker(cls, v: str) -> str: return v.upper().strip() @field_validator("shares") @classmethod def positive_shares(cls, v: int) -> int: if v <= 0: raise ValueError("shares must be positive") return v class StockPositionResponse(BaseModel): id: int ticker: str shares: int cost_basis: float | None created_at: datetime model_config = {"from_attributes": True} # ─── Option Position ─────────────────────────────────────────────────────────── class OptionPositionCreate(BaseModel): ticker: str strategy: str # covered_call | cash_secured_put strike: float expiration: date premium_received: float contracts: int = 1 @field_validator("ticker") @classmethod def uppercase_ticker(cls, v: str) -> str: return v.upper().strip() @field_validator("strategy") @classmethod def valid_strategy(cls, v: str) -> str: if v not in ("covered_call", "cash_secured_put"): raise ValueError("strategy must be 'covered_call' or 'cash_secured_put'") return v @field_validator("contracts") @classmethod def positive_contracts(cls, v: int) -> int: if v <= 0: raise ValueError("contracts must be positive") return v class OptionPositionClose(BaseModel): status: str # closed | rolled close_reason: str | None = None class OptionPositionResponse(BaseModel): id: int ticker: str strategy: str strike: float expiration: date premium_received: float contracts: int status: str close_reason: str | None opened_at: datetime closed_at: datetime | None last_signal_hash: str | None model_config = {"from_attributes": True} # ─── Signals ────────────────────────────────────────────────────────────────── class SignalSnapshot(BaseModel): ticker: str current_price: float iv_rank: float sma_50: float sma_200: float nearest_support: float | None nearest_resistance: float | None trend: str # uptrend | downtrend | sideways earnings_date: date | None computed_at: datetime # ─── Recommendations ────────────────────────────────────────────────────────── class RecommendationResponse(BaseModel): id: int ticker: str strategy: str time_horizon: str current_price: float recommended_strike: float recommended_expiration: date estimated_premium: float delta: float theta: float iv_rank: float signal_strength: str earnings_warning: bool earnings_date: date | None rationale: str signal_hash: str created_at: datetime model_config = {"from_attributes": True} class RecommendationWithSignals(BaseModel): recommendation: RecommendationResponse signals: SignalSnapshot # ─── Alerts ─────────────────────────────────────────────────────────────────── class AlertResponse(BaseModel): id: int ticker: str option_position_id: int | None alert_type: str message: str sent_at: datetime acknowledged: bool model_config = {"from_attributes": True} # ─── Health ─────────────────────────────────────────────────────────────────── class HealthResponse(BaseModel): status: str scheduler_running: bool last_run: datetime | None