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:
2026-04-09 14:38:25 -04:00
commit b7d4e900cc
61 changed files with 4953 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
from datetime import datetime, date
from sqlalchemy import Integer, String, Float, Boolean, DateTime, Date, ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Device(Base):
__tablename__ = "devices"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
apns_token: Mapped[str] = mapped_column(String, unique=True, index=True)
device_name: Mapped[str | None] = mapped_column(String, nullable=True)
registered_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
last_seen: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
stock_positions: Mapped[list["StockPosition"]] = relationship("StockPosition", back_populates="device", cascade="all, delete-orphan")
option_positions: Mapped[list["OptionPosition"]] = relationship("OptionPosition", back_populates="device", cascade="all, delete-orphan")
alerts: Mapped[list["Alert"]] = relationship("Alert", back_populates="device", cascade="all, delete-orphan")
class StockPosition(Base):
__tablename__ = "stock_positions"
__table_args__ = (UniqueConstraint("device_id", "ticker", name="uq_device_ticker"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
device_id: Mapped[int] = mapped_column(Integer, ForeignKey("devices.id"), nullable=False)
ticker: Mapped[str] = mapped_column(String, nullable=False)
shares: Mapped[int] = mapped_column(Integer, nullable=False)
cost_basis: Mapped[float | None] = mapped_column(Float, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
device: Mapped["Device"] = relationship("Device", back_populates="stock_positions")
class OptionPosition(Base):
__tablename__ = "option_positions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
device_id: Mapped[int] = mapped_column(Integer, ForeignKey("devices.id"), nullable=False)
ticker: Mapped[str] = mapped_column(String, nullable=False)
strategy: Mapped[str] = mapped_column(String, nullable=False) # covered_call | cash_secured_put
strike: Mapped[float] = mapped_column(Float, nullable=False)
expiration: Mapped[date] = mapped_column(Date, nullable=False)
premium_received: Mapped[float] = mapped_column(Float, nullable=False)
contracts: Mapped[int] = mapped_column(Integer, default=1)
status: Mapped[str] = mapped_column(String, default="open") # open | closed | rolled
close_reason: Mapped[str | None] = mapped_column(String, nullable=True) # expired | bought_back | rolled
opened_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
closed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
last_signal_hash: Mapped[str | None] = mapped_column(String(16), nullable=True)
device: Mapped["Device"] = relationship("Device", back_populates="option_positions")
alerts: Mapped[list["Alert"]] = relationship("Alert", back_populates="option_position")
class Recommendation(Base):
__tablename__ = "recommendations"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
device_id: Mapped[int] = mapped_column(Integer, ForeignKey("devices.id"), nullable=False)
ticker: Mapped[str] = mapped_column(String, nullable=False)
strategy: Mapped[str] = mapped_column(String, nullable=False) # covered_call | cash_secured_put
time_horizon: Mapped[str] = mapped_column(String, nullable=False) # 0dte | 1dte | weekly | monthly
current_price: Mapped[float] = mapped_column(Float, nullable=False)
recommended_strike: Mapped[float] = mapped_column(Float, nullable=False)
recommended_expiration: Mapped[date] = mapped_column(Date, nullable=False)
estimated_premium: Mapped[float] = mapped_column(Float, nullable=False)
delta: Mapped[float] = mapped_column(Float, nullable=False)
theta: Mapped[float] = mapped_column(Float, nullable=False)
iv_rank: Mapped[float] = mapped_column(Float, nullable=False)
signal_strength: Mapped[str] = mapped_column(String, nullable=False) # strong | moderate | weak
earnings_warning: Mapped[bool] = mapped_column(Boolean, default=False)
earnings_date: Mapped[date | None] = mapped_column(Date, nullable=True)
rationale: Mapped[str] = mapped_column(String, nullable=False)
signal_hash: Mapped[str] = mapped_column(String(16), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Alert(Base):
__tablename__ = "alerts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
device_id: Mapped[int] = mapped_column(Integer, ForeignKey("devices.id"), nullable=False)
option_position_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("option_positions.id"), nullable=True)
ticker: Mapped[str] = mapped_column(String, nullable=False)
alert_type: Mapped[str] = mapped_column(String, nullable=False) # close_early | roll_out | roll_up_down | earnings_warning | new_rec
message: Mapped[str] = mapped_column(String, nullable=False)
old_signal_hash: Mapped[str | None] = mapped_column(String(16), nullable=True)
new_signal_hash: Mapped[str | None] = mapped_column(String(16), nullable=True)
sent_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
acknowledged: Mapped[bool] = mapped_column(Boolean, default=False)
device: Mapped["Device"] = relationship("Device", back_populates="alerts")
option_position: Mapped["OptionPosition | None"] = relationship("OptionPosition", back_populates="alerts")