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>
88 lines
3.0 KiB
Swift
88 lines
3.0 KiB
Swift
import SwiftUI
|
|
|
|
struct AlertsView: View {
|
|
@StateObject private var vm = AlertsViewModel()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if vm.isLoading && vm.alerts.isEmpty {
|
|
LoadingView(message: "Loading alerts...")
|
|
} else if vm.alerts.isEmpty {
|
|
EmptyStateView(
|
|
icon: "bell.slash",
|
|
title: "No alerts",
|
|
subtitle: "Alerts appear here when signal changes on your open positions."
|
|
)
|
|
} else {
|
|
List(vm.alerts) { alert in
|
|
AlertRowView(alert: alert) {
|
|
Task { await vm.acknowledge(alert) }
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.refreshable { await vm.load() }
|
|
}
|
|
}
|
|
.navigationTitle("Alerts")
|
|
.toolbar {
|
|
if vm.unreadCount > 0 {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button("Mark All Read") {
|
|
Task { await vm.acknowledgeAll() }
|
|
}
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.task { await vm.load() }
|
|
}
|
|
}
|
|
|
|
// ─── Row ──────────────────────────────────────────────────────────────────────
|
|
|
|
struct AlertRowView: View {
|
|
let alert: AppAlert
|
|
let onAcknowledge: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 10) {
|
|
// Unread indicator
|
|
Circle()
|
|
.fill(alert.acknowledged ? Color.clear : Constants.Color.accent)
|
|
.frame(width: 8, height: 8)
|
|
.padding(.top, 5)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(spacing: 6) {
|
|
Text(alert.ticker)
|
|
.font(.headline)
|
|
AlertTypeBadge(alertType: alert.alertType)
|
|
}
|
|
Text(alert.message)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.primary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
Text(RelativeDateTimeFormatter().localizedString(for: alert.sentAt, relativeTo: Date()))
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if !alert.acknowledged {
|
|
Button {
|
|
onAcknowledge()
|
|
} label: {
|
|
Image(systemName: "checkmark.circle")
|
|
.foregroundStyle(Constants.Color.accent)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
.opacity(alert.acknowledged ? 0.6 : 1.0)
|
|
}
|
|
}
|