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) ChartDataTable( rows: drawdownTableRows, valueHeader: "Drawdown", deltaPrevHeader: "Δ prev", deltaFirstHeader: "Δ first" ) .padding(.top, 4) } 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) } private var drawdownTableRows: [ChartDataTableRow] { data.enumerated().map { idx, point in let val = abs(point.drawdown) let prevVal = idx > 0 ? abs(data[idx - 1].drawdown) : val let firstVal = abs(data.first?.drawdown ?? val) let deltaPrev = -(val - prevVal) // negative delta = improvement (less drawdown) let deltaFirst = -(val - firstVal) return ChartDataTableRow( label: chartTableDateLabel(point.date), value: String(format: "%.1f%%", val), deltaPrev: idx > 0 ? String(format: "%+.1f%%", deltaPrev) : nil, deltaFirst: idx > 0 ? String(format: "%+.1f%%", deltaFirst) : nil, isPrevPositive: deltaPrev >= 0, isFirstPositive: deltaFirst >= 0 ) } } } 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() } 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) ChartStatsRow(stats: [ ChartStat(label: "Current", value: String(format: "%.1f%%", currentVolatility), color: volatilityColor(currentVolatility)), ChartStat(label: "Max", value: String(format: "%.1f%%", data.map { $0.volatility }.max() ?? 0), color: .negativeRed), ChartStat(label: "Min", value: String(format: "%.1f%%", data.map { $0.volatility }.min() ?? 0), color: .positiveGreen), ChartStat(label: "Average", value: String(format: "%.1f%%", averageVolatility), color: .secondary), ]) .padding(.top, 8) ChartDataTable( rows: volatilityTableRows, valueHeader: "Volatility", deltaPrevHeader: "Δ prev", deltaFirstHeader: nil ) } 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 } } private var volatilityTableRows: [ChartDataTableRow] { data.enumerated().map { idx, point in let prevVal = idx > 0 ? data[idx - 1].volatility : point.volatility let delta = point.volatility - prevVal return ChartDataTableRow( label: chartTableDateLabel(point.date), value: String(format: "%.1f%%", point.volatility), deltaPrev: idx > 0 ? String(format: "%+.1f%%", delta) : nil, deltaFirst: nil, isPrevPositive: delta <= 0, isFirstPositive: false ) } } } 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() }