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>
167 lines
4.8 KiB
Python
167 lines
4.8 KiB
Python
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
|