Skip to content

Commit 57483c8

Browse files
authored
Harmonize performance endpoints (#16)
1 parent a7540b7 commit 57483c8

29 files changed

+183
-161
lines changed

Sources/Grodt/Application/routes.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ func routes(_ app: Application) async throws {
2424
let transactionRepository = PostgresTransactionRepository(database: app.db)
2525
let brokerageRepository = PostgresBrokerageRepository(database: app.db)
2626
let brokerageAccountRepository = PostgresBrokerageAccountRepository(database: app.db)
27-
let brokerageAccountDailyRepository = PostgresBrokerageAccountDailyPerformanceRepository(database: app.db)
27+
let brokerageAccountDailyPerformanceRepository = PostgresBrokerageAccountDailyPerformanceRepository(database: app.db)
2828
let brokerageDailyPerformanceRepository = PostgresBrokerageDailyPerformanceRepository(database: app.db)
2929

3030
let portfolioDTOMapper = PortfolioDTOMapper(investmentDTOMapper: investmentDTOMapper,
3131
transactionDTOMapper: transactionDTOMapper,
32+
performanceDTOMapper: DatedPerformanceDTOMapper(),
3233
currencyDTOMapper: currencyDTOMapper)
3334
let currencyRepository = PostgresCurrencyRepository(database: app.db)
3435
let portfolioPerformanceUpdater = PortfolioPerformanceUpdater(
@@ -95,8 +96,10 @@ func routes(_ app: Application) async throws {
9596
accounts: brokerageAccountRepository,
9697
currencyMapper: currencyDTOMapper,
9798
performanceRepository: brokerageDailyPerformanceRepository,
98-
performancePointDTOMapper: PerformancePointDTOMapper()))
99+
performanceDTOMapper: DatedPerformanceDTOMapper()))
99100
try protected.register(collection: BrokerageAccountController(brokerageAccountRepository: brokerageAccountRepository,
101+
performanceRepository: brokerageAccountDailyPerformanceRepository,
102+
performanceDTOMapper: DatedPerformanceDTOMapper(),
100103
currencyMapper: currencyDTOMapper,
101104
currencyRepository: currencyRepository))
102105
}
@@ -109,12 +112,12 @@ func routes(_ app: Application) async throws {
109112
portfolioPerformanceUpdater: portfolioPerformanceUpdater,
110113
brokerageAccountPerformanceUpdater: BrokerageAccountPerformanceUpdater(transactionRepository: transactionRepository,
111114
brokerageAccountRepository: brokerageAccountRepository,
112-
accountDailyRepository: brokerageAccountDailyRepository,
115+
accountDailyRepository: brokerageAccountDailyPerformanceRepository,
113116
userRepository: userRepository,
114117
calculator: performanceCalculator),
115118
brokeragePerformanceUpdater: BrokeragePerformanceUpdater(userRepository: userRepository,
116119
brokerageAccountRepository: brokerageAccountRepository,
117-
accountDailyRepository: brokerageAccountDailyRepository,
120+
accountDailyRepository: brokerageAccountDailyPerformanceRepository,
118121
brokerageDailyRepository: brokerageDailyPerformanceRepository)
119122
)
120123
app.queues.schedule(nightlyUpdaterJob)

Sources/Grodt/BusinessLogic/Performance Calculating/BrokeragePerformanceUpdater.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ final class BrokeragePerformanceUpdater: BrokeragePerformanceUpdating {
5858
}
5959

6060
// Read each account's full series and track the global date window
61-
var perAccountSeries: [[YearMonthDayDate: DatedPortfolioPerformance]] = []
61+
var perAccountSeries: [[YearMonthDayDate: DatedPerformance]] = []
6262
perAccountSeries.reserveCapacity(accounts.count)
6363

6464
var earliestDate: Date?
@@ -69,7 +69,7 @@ final class BrokeragePerformanceUpdater: BrokeragePerformanceUpdating {
6969
earliestDate = min(earliestDate ?? firstDate, firstDate)
7070
}
7171
// Index by date for O(1) lookups during summation
72-
var map: [YearMonthDayDate: DatedPortfolioPerformance] = [:]
72+
var map: [YearMonthDayDate: DatedPerformance] = [:]
7373
map.reserveCapacity(series.count)
7474
for point in series { map[point.date] = point }
7575
perAccountSeries.append(map)
@@ -86,7 +86,7 @@ final class BrokeragePerformanceUpdater: BrokeragePerformanceUpdating {
8686
let days = YearMonthDayDate.days(from: start, to: end)
8787

8888
// Sum across accounts for each day
89-
var summed: [DatedPortfolioPerformance] = []
89+
var summed: [DatedPerformance] = []
9090
summed.reserveCapacity(days.count)
9191

9292
for day in days {
@@ -98,7 +98,7 @@ final class BrokeragePerformanceUpdater: BrokeragePerformanceUpdating {
9898
value += point.value
9999
}
100100
}
101-
summed.append(DatedPortfolioPerformance(moneyIn: moneyIn, value: value, date: day))
101+
summed.append(DatedPerformance(moneyIn: moneyIn, value: value, date: day))
102102
}
103103

104104
// Replace the brokerage's series

Sources/Grodt/BusinessLogic/Performance Calculating/HoldingsPerformanceCalculating.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
protocol HoldingsPerformanceCalculating {
4-
func performanceSeries(for transactions: [Transaction], from startDate: YearMonthDayDate, to endDate: YearMonthDayDate) async throws -> [DatedPortfolioPerformance]
4+
func performanceSeries(for transactions: [Transaction], from startDate: YearMonthDayDate, to endDate: YearMonthDayDate) async throws -> [DatedPerformance]
55
}
66

77
struct HoldingsPerformanceCalculator: HoldingsPerformanceCalculating {
@@ -14,7 +14,7 @@ struct HoldingsPerformanceCalculator: HoldingsPerformanceCalculating {
1414
for transactions: [Transaction],
1515
from startDate: YearMonthDayDate,
1616
to endDate: YearMonthDayDate
17-
) async throws -> [DatedPortfolioPerformance] {
17+
) async throws -> [DatedPerformance] {
1818
guard !transactions.isEmpty else { return [] }
1919
guard endDate >= startDate else { return [] }
2020

@@ -36,7 +36,7 @@ struct HoldingsPerformanceCalculator: HoldingsPerformanceCalculating {
3636
var state = Self.initialState(at: startDate, with: sortedTransactions, baselinePrices: baselinePrices)
3737

3838
// 4) Sweep day-by-day, applying new transactions and price-change events.
39-
var series: [DatedPortfolioPerformance] = []
39+
var series: [DatedPerformance] = []
4040
series.reserveCapacity(days.count)
4141

4242
for day in days {

Sources/Grodt/DTOs/BrokerageDTO.swift

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
struct DatedPerformanceDTOMapper {
4+
func performancePoint(from enity: DatedPerformance) -> DatedPerformanceDTO {
5+
let moneyIn = enity.moneyIn
6+
let moneyOut = enity.value
7+
let profit = moneyOut - moneyIn
8+
let totalReturn: Decimal = moneyIn > 0 ? (profit / moneyIn).rounded(to: 2) : 0
9+
10+
return DatedPerformanceDTO(date: enity.date.date,
11+
moneyIn: moneyIn,
12+
moneyOut: moneyOut,
13+
profit: profit,
14+
totalReturn: totalReturn)
15+
}
16+
}

Sources/Grodt/DTOs/DTOMappers/PerformancePointDTOMapper.swift

Lines changed: 0 additions & 7 deletions
This file was deleted.

Sources/Grodt/DTOs/DTOMappers/PortfolioDTOMapper.swift

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ class PortfolioDTOMapper {
55
private let investmentDTOMapper: InvestmentDTOMapper
66
private let currencyDTOMapper: CurrencyDTOMapper
77
private let transactionDTOMapper: TransactionDTOMapper
8+
private let performanceDTOMapper: DatedPerformanceDTOMapper
89

910
init(investmentDTOMapper: InvestmentDTOMapper,
1011
transactionDTOMapper: TransactionDTOMapper,
12+
performanceDTOMapper: DatedPerformanceDTOMapper,
1113
currencyDTOMapper: CurrencyDTOMapper) {
1214
self.investmentDTOMapper = investmentDTOMapper
1315
self.transactionDTOMapper = transactionDTOMapper
16+
self.performanceDTOMapper = performanceDTOMapper
1417
self.currencyDTOMapper = currencyDTOMapper
1518
}
1619

@@ -35,45 +38,32 @@ class PortfolioDTOMapper {
3538
)
3639
}
3740

38-
func performance(for portfolio: Portfolio) async throws -> PortfolioPerformanceDTO {
41+
func performance(for portfolio: Portfolio) async throws -> PerformanceDTO {
3942
// Expect the daily series to be eager-loaded by the caller. If it's not loaded, fall back to zeros.
4043
guard portfolio.$historicalDailyPerformance.value != nil,
4144
let latest = portfolio.historicalDailyPerformance.max(by: { $0.date < $1.date })
4245
else {
43-
return PortfolioPerformanceDTO(moneyIn: 0, moneyOut: 0, profit: 0, totalReturn: 0)
46+
return PerformanceDTO(moneyIn: 0, moneyOut: 0, profit: 0, totalReturn: 0)
4447
}
4548

4649
let moneyIn = latest.moneyIn
4750
let moneyOut = latest.value
4851
let profit = moneyOut - moneyIn
4952
let totalReturn: Decimal = moneyIn > 0 ? (profit / moneyIn).rounded(to: 2) : 0
5053

51-
return PortfolioPerformanceDTO(
54+
return PerformanceDTO(
5255
moneyIn: moneyIn,
5356
moneyOut: moneyOut,
5457
profit: profit,
5558
totalReturn: totalReturn
5659
)
5760
}
5861

59-
func timeSeriesPerformance(from series: [DatedPortfolioPerformance]) async -> PortfolioPerformanceTimeSeriesDTO {
60-
let values: [DatedPortfolioPerformanceDTO] = series
61-
.map { point in
62-
let moneyIn = point.moneyIn
63-
let moneyOut = point.value
64-
let profit = moneyOut - moneyIn
65-
let totalReturn: Decimal = moneyIn > 0 ? (profit / moneyIn).rounded(to: 2) : 0
66-
return DatedPortfolioPerformanceDTO(
67-
date: point.date.date,
68-
moneyIn: moneyIn,
69-
moneyOut: moneyOut,
70-
profit: profit,
71-
totalReturn: totalReturn
72-
)
73-
}
62+
func timeSeriesPerformance(from series: [DatedPerformance]) async -> PerformanceTimeSeriesDTO {
63+
let values = series.map { performanceDTOMapper.performancePoint(from: $0) }
7464
.sorted { $0.date < $1.date }
75-
76-
return PortfolioPerformanceTimeSeriesDTO(values: values)
65+
66+
return PerformanceTimeSeriesDTO(values: values)
7767
}
7868
}
7969

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
3+
struct PerformanceDTO: Codable, Equatable {
4+
let moneyIn: Decimal
5+
let moneyOut: Decimal
6+
let profit: Decimal
7+
let totalReturn: Decimal
8+
9+
static var zero: PerformanceDTO {
10+
return .init(
11+
moneyIn: 0,
12+
moneyOut: 0,
13+
profit: 0,
14+
totalReturn: 0
15+
)
16+
}
17+
}

Sources/Grodt/DTOs/PerformanceTimeSeriesDTO.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Foundation
22

3-
struct PortfolioPerformanceTimeSeriesDTO: Codable, Equatable {
4-
let values: [DatedPortfolioPerformanceDTO]
3+
struct PerformanceTimeSeriesDTO: Codable, Equatable {
4+
let values: [DatedPerformanceDTO]
55
}
66

7-
struct DatedPortfolioPerformanceDTO: Codable, Equatable {
7+
struct DatedPerformanceDTO: Codable, Equatable {
88
let date: Date
99
let moneyIn: Decimal
1010
let moneyOut: Decimal

Sources/Grodt/DTOs/PortfolioDTO.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ struct PortfolioDTO: Codable, Equatable {
44
let id: String
55
let name: String
66
let currency: CurrencyDTO
7-
let performance: PortfolioPerformanceDTO
7+
let performance: PerformanceDTO
88
let investments: [InvestmentDTO]
99
let transactions: [TransactionDTO]
1010
}

0 commit comments

Comments
 (0)