Skip to content

Commit cbc5502

Browse files
author
Reed Es
committed
support for CAGR for #1
1 parent c7ee649 commit cbc5502

File tree

2 files changed

+148
-3
lines changed

2 files changed

+148
-3
lines changed

Sources/PeriodSummary/PeriodSummary.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ public final class PeriodSummary {
2626
let accountMap: AccountMap
2727

2828
public init(period: DateInterval,
29-
begPositions: [MValuationPosition],
30-
endPositions: [MValuationPosition],
31-
cashflows: [MValuationCashflow],
29+
begPositions: [MValuationPosition] = [],
30+
endPositions: [MValuationPosition] = [],
31+
cashflows: [MValuationCashflow] = [],
3232
accountMap: AccountMap = [:]) {
3333
self.period = period
3434
self.begPositions = begPositions
@@ -61,6 +61,12 @@ public final class PeriodSummary {
6161
guard let _singlePeriodReturn = singlePeriodReturn else { return nil }
6262
return _singlePeriodReturn / yearsInPeriod
6363
}()
64+
65+
// Compound Annual Growth Rate (CAGR)
66+
public lazy var singlePeriodCAGR: Double? = {
67+
guard begMarketValue > 0, endMarketValue > 0, yearsInPeriod > 0 else { return nil }
68+
return pow(endMarketValue / begMarketValue, 1 / yearsInPeriod) - 1
69+
}()
6470

6571
public lazy var daysInPeriod: Double = {
6672
period.duration / 24 / 60 / 60
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//
2+
// PeriodSummaryCAGRTests.swift
3+
//
4+
// Copyright 2021, 2022 OpenAlloc LLC
5+
//
6+
// This Source Code Form is subject to the terms of the Mozilla Public
7+
// License, v. 2.0. If a copy of the MPL was not distributed with this
8+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9+
//
10+
11+
import Foundation
12+
13+
@testable import FlowWorthLib
14+
import XCTest
15+
16+
import AllocData
17+
18+
import FlowBase
19+
20+
class PeriodSummaryCAGRTests: XCTestCase {
21+
22+
var tz: TimeZone!
23+
var df: ISO8601DateFormatter!
24+
var timestamp1a: Date!
25+
var timestamp1b: Date!
26+
var timestamp1c: Date!
27+
var timestamp2a: Date!
28+
var timestamp2b: Date!
29+
var zeroPos: MValuationPosition!
30+
var negPos: MValuationPosition!
31+
var pos1: MValuationPosition!
32+
var pos2: MValuationPosition!
33+
34+
override func setUpWithError() throws {
35+
tz = TimeZone.init(identifier: "EST")!
36+
df = ISO8601DateFormatter()
37+
timestamp1a = df.date(from: "2020-06-01T12:00:00Z")! // anchor
38+
timestamp2a = df.date(from: "2020-12-02T12:00:00Z")! // six months later
39+
40+
zeroPos = MValuationPosition(snapshotID: "X", accountID: "1", assetID: "Bond", totalBasis: 1, marketValue: 0)
41+
negPos = MValuationPosition(snapshotID: "X", accountID: "1", assetID: "Bond", totalBasis: 1, marketValue: -13)
42+
43+
pos1 = MValuationPosition(snapshotID: "X", accountID: "1", assetID: "Bond", totalBasis: 1, marketValue: 13)
44+
45+
pos2 = MValuationPosition(snapshotID: "X", accountID: "1", assetID: "Bond", totalBasis: 1, marketValue: 16)
46+
}
47+
48+
func testDurationIsNil() throws {
49+
let period = DateInterval(start: timestamp2a, end: timestamp2a)
50+
let ps = PeriodSummary(period: period)
51+
XCTAssertNil( ps.singlePeriodCAGR )
52+
}
53+
54+
func testNoBegCagrIsNil() throws {
55+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
56+
let ps = PeriodSummary(period: period, endPositions: [pos1])
57+
XCTAssertNil( ps.singlePeriodCAGR )
58+
}
59+
60+
func testNoEndCagrIsNil() throws {
61+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
62+
let ps = PeriodSummary(period: period, begPositions: [pos1])
63+
XCTAssertNil( ps.singlePeriodCAGR )
64+
}
65+
66+
func testZeroBegMVIsNil() throws {
67+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
68+
let ps = PeriodSummary(period: period, begPositions: [zeroPos], endPositions: [pos1])
69+
XCTAssertNil( ps.singlePeriodCAGR )
70+
}
71+
72+
func testZeroEndMVIsNil() throws {
73+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
74+
let ps = PeriodSummary(period: period, begPositions: [pos1], endPositions: [zeroPos])
75+
XCTAssertNil( ps.singlePeriodCAGR )
76+
}
77+
78+
func testNegBegMVIsNil() throws {
79+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
80+
let ps = PeriodSummary(period: period, begPositions: [negPos], endPositions: [pos1])
81+
XCTAssertNil( ps.singlePeriodCAGR )
82+
}
83+
84+
func testNegEndMVIsNil() throws {
85+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
86+
let ps = PeriodSummary(period: period, begPositions: [pos1], endPositions: [negPos])
87+
XCTAssertNil( ps.singlePeriodCAGR )
88+
}
89+
90+
func testNoChange() throws {
91+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
92+
let ps = PeriodSummary(period: period, begPositions: [pos1], endPositions: [pos1])
93+
XCTAssertEqual(0.0, ps.singlePeriodCAGR)
94+
}
95+
96+
func testPositive() throws {
97+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
98+
let ps = PeriodSummary(period: period, begPositions: [pos1], endPositions: [pos2])
99+
XCTAssertEqual(0.510, ps.singlePeriodCAGR!, accuracy: 0.001)
100+
}
101+
102+
func testNegative() throws {
103+
let period = DateInterval(start: timestamp1a, end: timestamp2a)
104+
let ps = PeriodSummary(period: period, begPositions: [pos2], endPositions: [pos1])
105+
XCTAssertEqual(-0.338, ps.singlePeriodCAGR!, accuracy: 0.001)
106+
}
107+
108+
// example from Investopedia entry for CAGR
109+
func testExample1() throws {
110+
let period = DateInterval(start: df.date(from: "2018-01-01T12:00:00Z")!,
111+
end: df.date(from: "2021-01-01T12:00:00Z")!)
112+
let ps = PeriodSummary(period: period,
113+
begPositions: [MValuationPosition(snapshotID: "X", accountID: "1", assetID: "Bond", totalBasis: 1, marketValue: 10000)],
114+
endPositions: [MValuationPosition(snapshotID: "X", accountID: "1", assetID: "Bond", totalBasis: 1, marketValue: 19000)])
115+
XCTAssertEqual(0.2385, ps.singlePeriodCAGR!, accuracy: 0.0001) // NOTE different from the 23.86%
116+
}
117+
118+
// a second example from Investopedia entry for CAGR
119+
func testExample2() throws {
120+
let period = DateInterval(start: df.date(from: "2017-12-01T12:00:00Z")!,
121+
end: df.date(from: "2020-12-01T12:00:00Z")!)
122+
let ps = PeriodSummary(period: period,
123+
begPositions: [MValuationPosition(snapshotID: "X", accountID: "1", assetID: "LC", totalBasis: 1, marketValue: 64900)],
124+
endPositions: [MValuationPosition(snapshotID: "X", accountID: "1", assetID: "LC", totalBasis: 1, marketValue: 176000)])
125+
XCTAssertEqual(0.3944, ps.singlePeriodCAGR!, accuracy: 0.0001) // NOTE different from the 39.5%
126+
}
127+
128+
// a third example from Investopedia entry for CAGR
129+
func testExample3() throws {
130+
let period = DateInterval(start: df.date(from: "2013-06-01T12:00:00Z")!,
131+
end: df.date(from: "2018-09-08T12:00:00Z")!)
132+
let ps = PeriodSummary(period: period,
133+
begPositions: [MValuationPosition(snapshotID: "X", accountID: "1", assetID: "LC", totalBasis: 1, marketValue: 10000.00)],
134+
endPositions: [MValuationPosition(snapshotID: "X", accountID: "1", assetID: "LC", totalBasis: 1, marketValue: 16897.14)])
135+
XCTAssertEqual(5.271, ps.yearsInPeriod, accuracy: 0.001)
136+
XCTAssertEqual(0.1046, ps.singlePeriodCAGR!, accuracy: 0.0001)
137+
}
138+
139+
}

0 commit comments

Comments
 (0)