Files
InvestmentTrackerApp/PortfolioJournal/Models/InvestmentMetrics.swift
T
2026-01-19 14:40:43 +01:00

250 lines
7.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 Foundation
struct InvestmentMetrics {
// Basic Metrics
let totalValue: Decimal
let totalContributions: Decimal
let absoluteReturn: Decimal
let percentageReturn: Decimal
// Advanced Metrics
let cagr: Double // Compound Annual Growth Rate
let twr: Double // Time-Weighted Return
let volatility: Double // Standard deviation annualized
let maxDrawdown: Double // Maximum peak-to-trough decline
let sharpeRatio: Double // Risk-adjusted return
let bestMonth: MonthlyReturn?
let worstMonth: MonthlyReturn?
let winRate: Double // Percentage of positive months
let averageMonthlyReturn: Double
// Time period
let startDate: Date?
let endDate: Date?
let totalMonths: Int
struct MonthlyReturn: Identifiable {
let id = UUID()
let date: Date
let returnPercentage: Double
var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMM yyyy"
return formatter.string(from: date)
}
var formattedReturn: String {
String(format: "%.1f%%", returnPercentage)
}
}
}
// MARK: - Formatting Helpers
extension InvestmentMetrics {
var formattedTotalValue: String {
formatCurrency(totalValue)
}
var formattedAbsoluteReturn: String {
let prefix = absoluteReturn >= 0 ? "+" : ""
return prefix + formatCurrency(absoluteReturn)
}
var formattedPercentageReturn: String {
let prefix = percentageReturn >= 0 ? "+" : ""
return String(format: "\(prefix)%.2f%%", NSDecimalNumber(decimal: percentageReturn).doubleValue)
}
var formattedCAGR: String {
String(format: "%.2f%%", cagr)
}
var formattedTWR: String {
String(format: "%.2f%%", twr)
}
var formattedVolatility: String {
String(format: "%.2f%%", volatility)
}
var formattedMaxDrawdown: String {
String(format: "%.2f%%", maxDrawdown)
}
var formattedSharpeRatio: String {
String(format: "%.2f", sharpeRatio)
}
var formattedWinRate: String {
String(format: "%.0f%%", winRate)
}
var formattedAverageMonthlyReturn: String {
let prefix = averageMonthlyReturn >= 0 ? "+" : ""
return String(format: "\(prefix)%.2f%%", averageMonthlyReturn)
}
private func formatCurrency(_ value: Decimal) -> String {
CurrencyFormatter.format(value, style: .currency, maximumFractionDigits: 2)
}
}
// MARK: - Empty State
extension InvestmentMetrics {
static var empty: InvestmentMetrics {
InvestmentMetrics(
totalValue: 0,
totalContributions: 0,
absoluteReturn: 0,
percentageReturn: 0,
cagr: 0,
twr: 0,
volatility: 0,
maxDrawdown: 0,
sharpeRatio: 0,
bestMonth: nil,
worstMonth: nil,
winRate: 0,
averageMonthlyReturn: 0,
startDate: nil,
endDate: nil,
totalMonths: 0
)
}
}
// MARK: - Category Metrics
struct CategoryMetrics: Identifiable {
let id: UUID
let categoryName: String
let colorHex: String
let icon: String
let totalValue: Decimal
let percentageOfPortfolio: Double
let metrics: InvestmentMetrics
var formattedTotalValue: String {
CurrencyFormatter.format(totalValue, style: .currency, maximumFractionDigits: 0)
}
var formattedPercentage: String {
String(format: "%.1f%%", percentageOfPortfolio)
}
}
// MARK: - Portfolio Summary
struct PortfolioSummary {
let totalValue: Decimal
let totalContributions: Decimal
let dayChange: Decimal
let dayChangePercentage: Double
let weekChange: Decimal
let weekChangePercentage: Double
let monthChange: Decimal
let monthChangePercentage: Double
let yearChange: Decimal
let yearChangePercentage: Double
let allTimeReturn: Decimal
let allTimeReturnPercentage: Double
let sourceCount: Int
let lastUpdated: Date?
var formattedTotalValue: String {
CurrencyFormatter.format(totalValue, style: .currency, maximumFractionDigits: 0)
}
var formattedDayChange: String {
formatChange(dayChange, dayChangePercentage)
}
var formattedMonthChange: String {
formatChange(monthChange, monthChangePercentage)
}
var formattedYearChange: String {
formatChange(yearChange, yearChangePercentage)
}
var formattedAllTimeReturn: String {
formatChange(allTimeReturn, allTimeReturnPercentage)
}
private func formatChange(_ absolute: Decimal, _ percentage: Double) -> String {
let prefix = absolute >= 0 ? "+" : ""
let absString = CurrencyFormatter.format(absolute, style: .currency, maximumFractionDigits: 0)
let pctString = String(format: "%.2f%%", percentage)
return "\(prefix)\(absString) (\(prefix)\(pctString))"
}
static var empty: PortfolioSummary {
PortfolioSummary(
totalValue: 0,
totalContributions: 0,
dayChange: 0,
dayChangePercentage: 0,
weekChange: 0,
weekChangePercentage: 0,
monthChange: 0,
monthChangePercentage: 0,
yearChange: 0,
yearChangePercentage: 0,
allTimeReturn: 0,
allTimeReturnPercentage: 0,
sourceCount: 0,
lastUpdated: nil
)
}
}
// MARK: - Portfolio Forecast
struct PortfolioForecast {
let currentValue: Decimal
let forecastValue: Decimal
let forecastDate: Date
let confidenceLower: Decimal
let confidenceUpper: Decimal
let monthlyGrowthRate: Decimal
let annualizedGrowthRate: Decimal
var formattedForecastValue: String {
CurrencyFormatter.format(forecastValue, style: .currency, maximumFractionDigits: 0)
}
var formattedCurrentValue: String {
CurrencyFormatter.format(currentValue, style: .currency, maximumFractionDigits: 0)
}
var formattedConfidenceRange: String {
let lower = CurrencyFormatter.format(confidenceLower, style: .currency, maximumFractionDigits: 0)
let upper = CurrencyFormatter.format(confidenceUpper, style: .currency, maximumFractionDigits: 0)
return "\(lower) \(upper)"
}
var formattedForecastDate: String {
forecastDate.monthYearString
}
var formattedGrowthPercentage: String {
guard currentValue > 0 else { return "+0%" }
let growthPct = ((forecastValue - currentValue) / currentValue) * 100
let prefix = growthPct >= 0 ? "+" : ""
return String(format: "\(prefix)%.1f%%", NSDecimalNumber(decimal: growthPct).doubleValue)
}
var formattedAnnualizedGrowthRate: String {
let prefix = annualizedGrowthRate >= 0 ? "+" : ""
return String(format: "\(prefix)%.1f%%", NSDecimalNumber(decimal: annualizedGrowthRate * 100).doubleValue)
}
var isPositiveGrowth: Bool {
forecastValue >= currentValue
}
}