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:
78
backend/app/routers/positions.py
Normal file
78
backend/app/routers/positions.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.db_models import Device, OptionPosition
|
||||
from app.models.schemas import OptionPositionCreate, OptionPositionClose, OptionPositionResponse
|
||||
|
||||
router = APIRouter(prefix="/positions", tags=["positions"])
|
||||
|
||||
|
||||
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.")
|
||||
return device
|
||||
|
||||
|
||||
@router.get("", response_model=list[OptionPositionResponse])
|
||||
def get_positions(
|
||||
status: str | None = Query(None),
|
||||
device: Device = Depends(_get_device),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = db.query(OptionPosition).filter(OptionPosition.device_id == device.id)
|
||||
if status:
|
||||
query = query.filter(OptionPosition.status == status)
|
||||
return query.order_by(OptionPosition.opened_at.desc()).all()
|
||||
|
||||
|
||||
@router.post("", response_model=OptionPositionResponse, status_code=201)
|
||||
def log_position(
|
||||
body: OptionPositionCreate,
|
||||
device: Device = Depends(_get_device),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
position = OptionPosition(
|
||||
device_id=device.id,
|
||||
ticker=body.ticker,
|
||||
strategy=body.strategy,
|
||||
strike=body.strike,
|
||||
expiration=body.expiration,
|
||||
premium_received=body.premium_received,
|
||||
contracts=body.contracts,
|
||||
status="open",
|
||||
)
|
||||
db.add(position)
|
||||
db.commit()
|
||||
db.refresh(position)
|
||||
return position
|
||||
|
||||
|
||||
@router.patch("/{position_id}", response_model=OptionPositionResponse)
|
||||
def close_position(
|
||||
position_id: int,
|
||||
body: OptionPositionClose,
|
||||
device: Device = Depends(_get_device),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
position = (
|
||||
db.query(OptionPosition)
|
||||
.filter(OptionPosition.id == position_id, OptionPosition.device_id == device.id)
|
||||
.first()
|
||||
)
|
||||
if not position:
|
||||
raise HTTPException(status_code=404, detail="Position not found.")
|
||||
|
||||
valid_statuses = ("closed", "rolled")
|
||||
if body.status not in valid_statuses:
|
||||
raise HTTPException(status_code=422, detail=f"status must be one of {valid_statuses}")
|
||||
|
||||
position.status = body.status
|
||||
position.close_reason = body.close_reason
|
||||
position.closed_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(position)
|
||||
return position
|
||||
Reference in New Issue
Block a user