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>
104 lines
4.0 KiB
Swift
104 lines
4.0 KiB
Swift
import SwiftUI
|
||
|
||
struct OpenPositionsView: View {
|
||
@StateObject private var vm = PositionsViewModel()
|
||
@State private var showLogSheet = false
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Group {
|
||
if vm.isLoading && vm.positions.isEmpty {
|
||
LoadingView(message: "Loading positions...")
|
||
} else if let error = vm.error {
|
||
ErrorView(message: error) { await vm.load() }
|
||
} else if vm.positions.isEmpty {
|
||
EmptyStateView(
|
||
icon: "doc.text",
|
||
title: "No logged trades",
|
||
subtitle: "Log your executed options trades here. The app will monitor them and alert you when signals change."
|
||
)
|
||
} else {
|
||
List {
|
||
if !vm.openPositions.isEmpty {
|
||
Section("Open") {
|
||
ForEach(vm.openPositions) { position in
|
||
NavigationLink(destination: PositionDetailView(position: position, vm: vm)) {
|
||
LoggedPositionRow(position: position)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if !vm.closedPositions.isEmpty {
|
||
Section("Closed / Rolled") {
|
||
ForEach(vm.closedPositions) { position in
|
||
LoggedPositionRow(position: position)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.listStyle(.insetGrouped)
|
||
.refreshable { await vm.load() }
|
||
}
|
||
}
|
||
.navigationTitle("My Trades")
|
||
.toolbar {
|
||
ToolbarItem(placement: .navigationBarTrailing) {
|
||
Button {
|
||
showLogSheet = true
|
||
} label: {
|
||
Image(systemName: "plus")
|
||
}
|
||
}
|
||
}
|
||
.sheet(isPresented: $showLogSheet) {
|
||
LogTradeSheet(vm: vm)
|
||
}
|
||
}
|
||
.task { await vm.load() }
|
||
}
|
||
}
|
||
|
||
// ─── Row ──────────────────────────────────────────────────────────────────────
|
||
|
||
struct LoggedPositionRow: View {
|
||
let position: OptionPosition
|
||
|
||
var statusColor: Color {
|
||
switch position.status {
|
||
case "open": return Constants.Color.strong
|
||
case "rolled": return Constants.Color.moderate
|
||
default: return .secondary
|
||
}
|
||
}
|
||
|
||
var body: some View {
|
||
HStack {
|
||
VStack(alignment: .leading, spacing: 3) {
|
||
HStack(spacing: 6) {
|
||
Text(position.ticker).font(.headline)
|
||
Text(position.strategyLabel).font(.caption).foregroundStyle(.secondary)
|
||
Circle().fill(statusColor).frame(width: 6, height: 6)
|
||
}
|
||
HStack(spacing: 10) {
|
||
Text(String(format: "$%.0f", position.strike)).font(.caption)
|
||
if let dte = position.daysToExpiry {
|
||
Text("\(dte)d to expiry")
|
||
.font(.caption)
|
||
.foregroundStyle(dte <= 5 ? Constants.Color.warning : .secondary)
|
||
}
|
||
}
|
||
}
|
||
Spacer()
|
||
VStack(alignment: .trailing, spacing: 2) {
|
||
Text(String(format: "$%.2f", position.premiumReceived))
|
||
.font(.subheadline.weight(.semibold))
|
||
Text(String(format: "×%d", position.contracts))
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
.padding(.vertical, 3)
|
||
}
|
||
}
|