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) } } }