Files
Options-SideKick/ios/OptionsSidekick/OptionsSidekick/Views/Positions/OpenPositionsView.swift
olsch01 b7d4e900cc 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>
2026-04-09 14:38:25 -04:00

104 lines
4.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}