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:
64
backend/app/routers/portfolio.py
Normal file
64
backend/app/routers/portfolio.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.db_models import Device, StockPosition
|
||||
from app.models.schemas import StockPositionCreate, StockPositionResponse
|
||||
|
||||
router = APIRouter(prefix="/portfolio", tags=["portfolio"])
|
||||
|
||||
|
||||
def _get_device(x_device_token: str = Header(...), db: Session = Depends(get_db)) -> Device:
|
||||
device = db.query(Device).filter(Device.apns_token == x_device_token).first()
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail="Device not registered. Call /devices/register first.")
|
||||
return device
|
||||
|
||||
|
||||
@router.get("", response_model=list[StockPositionResponse])
|
||||
def get_portfolio(device: Device = Depends(_get_device), db: Session = Depends(get_db)):
|
||||
return db.query(StockPosition).filter(StockPosition.device_id == device.id).all()
|
||||
|
||||
|
||||
@router.post("", response_model=list[StockPositionResponse])
|
||||
def set_portfolio(
|
||||
positions: list[StockPositionCreate],
|
||||
device: Device = Depends(_get_device),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Full replace — client sends complete list of stock holdings."""
|
||||
# Delete all existing for this device
|
||||
db.query(StockPosition).filter(StockPosition.device_id == device.id).delete()
|
||||
|
||||
new_positions = []
|
||||
for p in positions:
|
||||
sp = StockPosition(
|
||||
device_id=device.id,
|
||||
ticker=p.ticker,
|
||||
shares=p.shares,
|
||||
cost_basis=p.cost_basis,
|
||||
)
|
||||
db.add(sp)
|
||||
new_positions.append(sp)
|
||||
|
||||
db.commit()
|
||||
for sp in new_positions:
|
||||
db.refresh(sp)
|
||||
return new_positions
|
||||
|
||||
|
||||
@router.delete("/{ticker}", status_code=204)
|
||||
def delete_ticker(
|
||||
ticker: str,
|
||||
device: Device = Depends(_get_device),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
ticker = ticker.upper()
|
||||
deleted = (
|
||||
db.query(StockPosition)
|
||||
.filter(StockPosition.device_id == device.id, StockPosition.ticker == ticker)
|
||||
.delete()
|
||||
)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail=f"Ticker {ticker} not in portfolio")
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user