Files
InvestmentTrackerApp/PortfolioJournalTests/Services/CalculationServiceTests.swift
T
2026-02-01 11:12:57 +01:00

393 lines
11 KiB
Swift

import XCTest
@testable import PortfolioJournal
final class CalculationServiceTests: XCTestCase {
var sut: CalculationService!
override func setUp() {
super.setUp()
sut = CalculationService.shared
sut.invalidateCache()
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - CAGR Tests
func testCalculateCAGR_withValidInputs_returnsCorrectValue() {
// Given: 10,000 growing to 15,000 over 3 years
let startValue: Decimal = 10_000
let endValue: Decimal = 15_000
let startDate = Calendar.current.date(byAdding: .year, value: -3, to: Date())!
let endDate = Date()
// When
let cagr = sut.calculateCAGR(
startValue: startValue,
endValue: endValue,
startDate: startDate,
endDate: endDate
)
// Then: CAGR should be approximately 14.47%
// Formula: (15000/10000)^(1/3) - 1 = 0.1447
XCTAssertEqual(cagr, 14.47, accuracy: 0.5)
}
func testCalculateCAGR_withZeroStartValue_returnsZero() {
// Given
let startValue: Decimal = 0
let endValue: Decimal = 15_000
let startDate = Calendar.current.date(byAdding: .year, value: -3, to: Date())!
let endDate = Date()
// When
let cagr = sut.calculateCAGR(
startValue: startValue,
endValue: endValue,
startDate: startDate,
endDate: endDate
)
// Then
XCTAssertEqual(cagr, 0)
}
func testCalculateCAGR_withSameDate_returnsZero() {
// Given
let startValue: Decimal = 10_000
let endValue: Decimal = 15_000
let date = Date()
// When
let cagr = sut.calculateCAGR(
startValue: startValue,
endValue: endValue,
startDate: date,
endDate: date
)
// Then
XCTAssertEqual(cagr, 0)
}
func testCalculateCAGR_withNegativeReturn_returnsNegativeValue() {
// Given: 10,000 declining to 8,000 over 2 years
let startValue: Decimal = 10_000
let endValue: Decimal = 8_000
let startDate = Calendar.current.date(byAdding: .year, value: -2, to: Date())!
let endDate = Date()
// When
let cagr = sut.calculateCAGR(
startValue: startValue,
endValue: endValue,
startDate: startDate,
endDate: endDate
)
// Then: CAGR should be approximately -10.56%
XCTAssertLessThan(cagr, 0)
XCTAssertEqual(cagr, -10.56, accuracy: 0.5)
}
func testCalculateCAGR_with100PercentGrowthOver1Year_returns100Percent() {
// Given: 10,000 doubling to 20,000 in 1 year
let startValue: Decimal = 10_000
let endValue: Decimal = 20_000
let startDate = Calendar.current.date(byAdding: .year, value: -1, to: Date())!
let endDate = Date()
// When
let cagr = sut.calculateCAGR(
startValue: startValue,
endValue: endValue,
startDate: startDate,
endDate: endDate
)
// Then
XCTAssertEqual(cagr, 100, accuracy: 1.0)
}
// MARK: - Max Drawdown Tests
func testCalculateMaxDrawdown_withIncreasingValues_returnsZero() {
// Given
let values: [Decimal] = [100, 110, 120, 130, 140, 150]
// When
let maxDrawdown = sut.calculateMaxDrawdown(values: values)
// Then
XCTAssertEqual(maxDrawdown, 0)
}
func testCalculateMaxDrawdown_withDecline_returnsCorrectValue() {
// Given: Peak at 100, drops to 80 (20% drawdown)
let values: [Decimal] = [80, 100, 95, 80, 90]
// When
let maxDrawdown = sut.calculateMaxDrawdown(values: values)
// Then
XCTAssertEqual(maxDrawdown, 20, accuracy: 0.1)
}
func testCalculateMaxDrawdown_withMultipleDrawdowns_returnsLargest() {
// Given: First drawdown 10%, second drawdown 25%
let values: [Decimal] = [100, 90, 95, 120, 90, 110]
// When
let maxDrawdown = sut.calculateMaxDrawdown(values: values)
// Then: 25% is the max drawdown (120 to 90)
XCTAssertEqual(maxDrawdown, 25, accuracy: 0.1)
}
func testCalculateMaxDrawdown_withEmptyArray_returnsZero() {
// Given
let values: [Decimal] = []
// When
let maxDrawdown = sut.calculateMaxDrawdown(values: values)
// Then
XCTAssertEqual(maxDrawdown, 0)
}
func testCalculateMaxDrawdown_withSingleValue_returnsZero() {
// Given
let values: [Decimal] = [100]
// When
let maxDrawdown = sut.calculateMaxDrawdown(values: values)
// Then
XCTAssertEqual(maxDrawdown, 0)
}
// MARK: - Sharpe Ratio Tests
func testCalculateSharpeRatio_withPositiveReturn_returnsPositiveValue() {
// Given
let averageReturn = 1.5 // 1.5% monthly return
let volatility = 3.0 // 3% volatility
// When
let sharpeRatio = sut.calculateSharpeRatio(
averageReturn: averageReturn,
volatility: volatility
)
// Then
XCTAssertGreaterThan(sharpeRatio, 0)
}
func testCalculateSharpeRatio_withZeroVolatility_returnsZero() {
// Given
let averageReturn = 1.5
let volatility = 0.0
// When
let sharpeRatio = sut.calculateSharpeRatio(
averageReturn: averageReturn,
volatility: volatility
)
// Then
XCTAssertEqual(sharpeRatio, 0)
}
func testCalculateSharpeRatio_withNegativeReturn_returnsNegativeValue() {
// Given
let averageReturn = -1.0 // -1% monthly return (below risk-free rate)
let volatility = 5.0
// When
let sharpeRatio = sut.calculateSharpeRatio(
averageReturn: averageReturn,
volatility: volatility
)
// Then
XCTAssertLessThan(sharpeRatio, 0)
}
// MARK: - Win Rate Tests
func testCalculateWinRate_withAllPositiveMonths_returns100Percent() {
// Given
let monthlyReturns = [
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 5.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 3.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 2.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 8.0)
]
// When
let winRate = sut.calculateWinRate(monthlyReturns: monthlyReturns)
// Then
XCTAssertEqual(winRate, 100)
}
func testCalculateWinRate_withAllNegativeMonths_returnsZero() {
// Given
let monthlyReturns = [
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: -5.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: -3.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: -2.0)
]
// When
let winRate = sut.calculateWinRate(monthlyReturns: monthlyReturns)
// Then
XCTAssertEqual(winRate, 0)
}
func testCalculateWinRate_withMixedMonths_returnsCorrectPercentage() {
// Given: 3 positive, 1 negative = 75% win rate
let monthlyReturns = [
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 5.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: -3.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 2.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 1.0)
]
// When
let winRate = sut.calculateWinRate(monthlyReturns: monthlyReturns)
// Then
XCTAssertEqual(winRate, 75)
}
func testCalculateWinRate_withEmptyArray_returnsZero() {
// Given
let monthlyReturns: [InvestmentMetrics.MonthlyReturn] = []
// When
let winRate = sut.calculateWinRate(monthlyReturns: monthlyReturns)
// Then
XCTAssertEqual(winRate, 0)
}
// MARK: - Volatility Tests
func testCalculateVolatility_withStableReturns_returnsLowVolatility() {
// Given: Very stable returns
let monthlyReturns = [
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 1.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 1.1),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 0.9),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 1.0)
]
// When
let volatility = sut.calculateVolatility(monthlyReturns: monthlyReturns)
// Then
XCTAssertLessThan(volatility, 1.0) // Very low volatility
}
func testCalculateVolatility_withVolatileReturns_returnsHighVolatility() {
// Given: Very volatile returns
let monthlyReturns = [
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 10.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: -8.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 15.0),
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: -12.0)
]
// When
let volatility = sut.calculateVolatility(monthlyReturns: monthlyReturns)
// Then
XCTAssertGreaterThan(volatility, 30.0) // High volatility
}
func testCalculateVolatility_withSingleMonth_returnsZero() {
// Given
let monthlyReturns = [
InvestmentMetrics.MonthlyReturn(date: Date(), returnPercentage: 5.0)
]
// When
let volatility = sut.calculateVolatility(monthlyReturns: monthlyReturns)
// Then
XCTAssertEqual(volatility, 0)
}
// MARK: - Array Average Extension Tests
func testArrayAverage_withValues_returnsCorrectAverage() {
// Given
let values = [10.0, 20.0, 30.0, 40.0]
// When
let average = values.average()
// Then
XCTAssertEqual(average, 25.0)
}
func testArrayAverage_withEmptyArray_returnsZero() {
// Given
let values: [Double] = []
// When
let average = values.average()
// Then
XCTAssertEqual(average, 0)
}
func testArrayAverage_withNegativeValues_returnsCorrectAverage() {
// Given
let values = [-10.0, 10.0, -5.0, 5.0]
// When
let average = values.average()
// Then
XCTAssertEqual(average, 0)
}
}
// MARK: - Performance Tests
extension CalculationServiceTests {
func testCalculateMaxDrawdown_performance() {
// Given: Large dataset
let values: [Decimal] = (0..<10000).map { Decimal($0 % 100 + 50) }
// When/Then
measure {
_ = sut.calculateMaxDrawdown(values: values)
}
}
func testCalculateVolatility_performance() {
// Given: Large dataset
let monthlyReturns = (0..<120).map { index in
InvestmentMetrics.MonthlyReturn(
date: Calendar.current.date(byAdding: .month, value: -index, to: Date())!,
returnPercentage: Double.random(in: -10...10)
)
}
// When/Then
measure {
_ = sut.calculateVolatility(monthlyReturns: monthlyReturns)
}
}
}