305 lines
10 KiB
Swift
305 lines
10 KiB
Swift
import SwiftUI
|
|
import Charts
|
|
|
|
struct DrawdownChart: View {
|
|
let data: [(date: Date, drawdown: Double)]
|
|
|
|
var maxDrawdown: Double {
|
|
abs(data.map { $0.drawdown }.min() ?? 0)
|
|
}
|
|
|
|
var currentDrawdown: Double {
|
|
abs(data.last?.drawdown ?? 0)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack {
|
|
Text("Drawdown Analysis")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
Text("Max Drawdown")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
Text(String(format: "%.1f%%", maxDrawdown))
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundColor(.negativeRed)
|
|
}
|
|
}
|
|
|
|
Text("Shows percentage decline from peak values")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
if data.count >= 2 {
|
|
Chart(data, id: \.date) { item in
|
|
AreaMark(
|
|
x: .value("Date", item.date),
|
|
y: .value("Drawdown", item.drawdown)
|
|
)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color.negativeRed.opacity(0.5), Color.negativeRed.opacity(0.1)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.interpolationMethod(.catmullRom)
|
|
|
|
LineMark(
|
|
x: .value("Date", item.date),
|
|
y: .value("Drawdown", item.drawdown)
|
|
)
|
|
.foregroundStyle(Color.negativeRed)
|
|
.interpolationMethod(.catmullRom)
|
|
}
|
|
.chartXAxis {
|
|
AxisMarks(values: .stride(by: .month, count: 2)) { value in
|
|
AxisValueLabel(format: .dateTime.month(.abbreviated))
|
|
}
|
|
}
|
|
.chartYAxis {
|
|
AxisMarks { value in
|
|
AxisGridLine()
|
|
AxisValueLabel {
|
|
if let doubleValue = value.as(Double.self) {
|
|
Text(String(format: "%.0f%%", doubleValue))
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.chartYScale(domain: (data.map { $0.drawdown }.min() ?? -50)...0)
|
|
.frame(height: 250)
|
|
|
|
// Statistics
|
|
HStack(spacing: 20) {
|
|
DrawdownStatView(
|
|
title: "Current",
|
|
value: String(format: "%.1f%%", currentDrawdown),
|
|
isHighlighted: currentDrawdown > maxDrawdown * 0.8
|
|
)
|
|
|
|
DrawdownStatView(
|
|
title: "Maximum",
|
|
value: String(format: "%.1f%%", maxDrawdown),
|
|
isHighlighted: true
|
|
)
|
|
|
|
DrawdownStatView(
|
|
title: "Average",
|
|
value: String(format: "%.1f%%", averageDrawdown),
|
|
isHighlighted: false
|
|
)
|
|
}
|
|
.padding(.top, 8)
|
|
} else {
|
|
Text("Not enough data for drawdown analysis")
|
|
.foregroundColor(.secondary)
|
|
.frame(height: 250)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
}
|
|
|
|
private var averageDrawdown: Double {
|
|
guard !data.isEmpty else { return 0 }
|
|
let sum = data.reduce(0.0) { $0 + abs($1.drawdown) }
|
|
return sum / Double(data.count)
|
|
}
|
|
}
|
|
|
|
struct DrawdownStatView: View {
|
|
let title: String
|
|
let value: String
|
|
let isHighlighted: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
Text(title)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(value)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundColor(isHighlighted ? .negativeRed : .primary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
|
|
// MARK: - Volatility Chart View
|
|
|
|
struct VolatilityChartView: View {
|
|
let data: [(date: Date, volatility: Double)]
|
|
|
|
var currentVolatility: Double {
|
|
data.last?.volatility ?? 0
|
|
}
|
|
|
|
var averageVolatility: Double {
|
|
guard !data.isEmpty else { return 0 }
|
|
return data.reduce(0.0) { $0 + $1.volatility } / Double(data.count)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack {
|
|
Text("Volatility")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
Text("Current")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
Text(String(format: "%.1f%%", currentVolatility))
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundColor(volatilityColor(currentVolatility))
|
|
}
|
|
}
|
|
|
|
Text("Measures price variability over time")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
if data.count >= 2 {
|
|
Chart(data, id: \.date) { item in
|
|
LineMark(
|
|
x: .value("Date", item.date),
|
|
y: .value("Volatility", item.volatility)
|
|
)
|
|
.foregroundStyle(Color.appPrimary)
|
|
.interpolationMethod(.catmullRom)
|
|
|
|
AreaMark(
|
|
x: .value("Date", item.date),
|
|
y: .value("Volatility", item.volatility)
|
|
)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color.appPrimary.opacity(0.3), Color.appPrimary.opacity(0.0)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.interpolationMethod(.catmullRom)
|
|
|
|
// Average line
|
|
RuleMark(y: .value("Average", averageVolatility))
|
|
.foregroundStyle(Color.gray.opacity(0.5))
|
|
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
|
|
.annotation(position: .top, alignment: .leading) {
|
|
Text("Avg: \(String(format: "%.1f%%", averageVolatility))")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.chartXAxis {
|
|
AxisMarks(values: .stride(by: .month, count: 2)) { value in
|
|
AxisValueLabel(format: .dateTime.month(.abbreviated))
|
|
}
|
|
}
|
|
.chartYAxis {
|
|
AxisMarks { value in
|
|
AxisGridLine()
|
|
AxisValueLabel {
|
|
if let doubleValue = value.as(Double.self) {
|
|
Text(String(format: "%.0f%%", doubleValue))
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 250)
|
|
|
|
// Volatility interpretation
|
|
HStack(spacing: 16) {
|
|
VolatilityLevelView(level: "Low", range: "0-10%", color: .positiveGreen)
|
|
VolatilityLevelView(level: "Medium", range: "10-20%", color: .appWarning)
|
|
VolatilityLevelView(level: "High", range: "20%+", color: .negativeRed)
|
|
}
|
|
.padding(.top, 8)
|
|
} else {
|
|
Text("Not enough data for volatility analysis")
|
|
.foregroundColor(.secondary)
|
|
.frame(height: 250)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
}
|
|
|
|
private func volatilityColor(_ volatility: Double) -> Color {
|
|
switch volatility {
|
|
case 0..<10:
|
|
return .positiveGreen
|
|
case 10..<20:
|
|
return .appWarning
|
|
default:
|
|
return .negativeRed
|
|
}
|
|
}
|
|
}
|
|
|
|
struct VolatilityLevelView: View {
|
|
let level: String
|
|
let range: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
Circle()
|
|
.fill(color)
|
|
.frame(width: 8, height: 8)
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(level)
|
|
.font(.caption2)
|
|
Text(range)
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
let drawdownData: [(date: Date, drawdown: Double)] = [
|
|
(Date().adding(months: -6), -5),
|
|
(Date().adding(months: -5), -8),
|
|
(Date().adding(months: -4), -3),
|
|
(Date().adding(months: -3), -15),
|
|
(Date().adding(months: -2), -10),
|
|
(Date().adding(months: -1), -7),
|
|
(Date(), -4)
|
|
]
|
|
|
|
let volatilityData: [(date: Date, volatility: Double)] = [
|
|
(Date().adding(months: -6), 12),
|
|
(Date().adding(months: -5), 15),
|
|
(Date().adding(months: -4), 10),
|
|
(Date().adding(months: -3), 22),
|
|
(Date().adding(months: -2), 18),
|
|
(Date().adding(months: -1), 14),
|
|
(Date(), 11)
|
|
]
|
|
|
|
return VStack(spacing: 20) {
|
|
DrawdownChart(data: drawdownData)
|
|
VolatilityChartView(data: volatilityData)
|
|
}
|
|
.padding()
|
|
}
|