Initial implementation of Options Sidekick
Full-stack iOS options trading assistant: - Python FastAPI backend with SQLite, APScheduler (15-min position monitor), APNs push notifications, and yfinance market data integration - Signal engine: IV Rank (rolling HV proxy), SMA-50/200, swing-based support/resistance, earnings detection, signal strength scoring and noise-resistant SHA hash for change detection - Recommendation engine: covered call and cash-secured put strike/expiry selection across 0DTE, 1DTE, weekly, and monthly horizons - REST API: /devices, /portfolio, /recommendations, /positions, /signals, /alerts - iOS SwiftUI app (iOS 17+): dashboard, recommendations, trades, portfolio, and alerts tabs with push notification deep-linking - Unit + integration tests for signal engine and API layer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
backend/app/models/schemas.py
Normal file
166
backend/app/models/schemas.py
Normal file
@@ -0,0 +1,166 @@
|
||||
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
|
||||
Reference in New Issue
Block a user