Base fixes and test harness
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user