Skip to content

Commit 6f83732

Browse files
committed
Harmonize performance endpoints
1 parent 8ec4e3e commit 6f83732

22 files changed

+122
-113
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 {
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: 7 additions & 17 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

@@ -56,24 +59,11 @@ class PortfolioDTOMapper {
5659
)
5760
}
5861

59-
func timeSeriesPerformance(from series: [DatedPortfolioPerformance]) async -> PortfolioPerformanceTimeSeriesDTO {
60-
let values: [DatedPerformanceDTO] = 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 DatedPerformanceDTO(
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

Sources/Grodt/DTOs/PerformanceTimeSeriesDTO.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

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

Sources/Grodt/Endpoints/PortfoliosController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ struct PortfoliosController: RouteCollection {
3232
portfolio.put(use: update)
3333
portfolio.delete(use: delete)
3434

35-
portfolio.group("historicalPerformance") { pref in
35+
portfolio.group("performance") { pref in
3636
pref.get(use: historicalPerformance)
3737
}
3838
}
@@ -121,7 +121,7 @@ struct PortfoliosController: RouteCollection {
121121
return .ok
122122
}
123123

124-
func historicalPerformance(req: Request) async throws -> PortfolioPerformanceTimeSeriesDTO {
124+
func historicalPerformance(req: Request) async throws -> PerformanceTimeSeriesDTO {
125125
let id = try req.requiredID()
126126
guard let userID = req.auth.get(User.self)?.id else { throw Abort(.badRequest) }
127127

@@ -136,4 +136,4 @@ struct PortfoliosController: RouteCollection {
136136

137137
extension PortfolioDTO: Content { }
138138
extension PortfolioInfoDTO: Content { }
139-
extension PortfolioPerformanceTimeSeriesDTO: Content { }
139+
extension PerformanceTimeSeriesDTO: Content { }

Sources/Grodt/Endpoints/BrokerageAccountController.swift renamed to Sources/Grodt/Endpoints/brokerages/BrokerageAccountController.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import Fluent
33

44
struct BrokerageAccountController: RouteCollection {
55
private let brokerageAccountRepository: BrokerageAccountRepository
6+
private let performanceRepository: PostgresBrokerageAccountDailyPerformanceRepository
67
private let currencyMapper: CurrencyDTOMapper
8+
private let performanceDTOMapper: DatedPerformanceDTOMapper
79
private let currencyRepository: CurrencyRepository
810

9-
init(brokerageAccountRepository: BrokerageAccountRepository, currencyMapper: CurrencyDTOMapper, currencyRepository: CurrencyRepository) {
11+
init(brokerageAccountRepository: BrokerageAccountRepository,
12+
performanceRepository: PostgresBrokerageAccountDailyPerformanceRepository,
13+
performanceDTOMapper: DatedPerformanceDTOMapper,
14+
currencyMapper: CurrencyDTOMapper,
15+
currencyRepository: CurrencyRepository) {
1016
self.brokerageAccountRepository = brokerageAccountRepository
17+
self.performanceRepository = performanceRepository
18+
self.performanceDTOMapper = performanceDTOMapper
1119
self.currencyMapper = currencyMapper
1220
self.currencyRepository = currencyRepository
1321
}
@@ -101,14 +109,16 @@ struct BrokerageAccountController: RouteCollection {
101109
return .noContent
102110
}
103111

104-
private func performanceSeries(req: Request) async throws -> [PerformancePointDTO] {
112+
private func performanceSeries(req: Request) async throws -> PerformanceTimeSeriesDTO {
105113
let userID = try req.requireUserID()
106114
let account = try await requireAccount(req, userID: userID)
107-
let rows = try await HistoricalBrokerageAccountPerformanceDaily.query(on: req.db)
108-
.filter(\.$account.$id == account.requireID())
109-
.sort(\.$date, .ascending)
110-
.all()
111-
return rows.map { PerformancePointDTO(date: $0.date, value: $0.value, moneyIn: $0.moneyIn) }
115+
let rows = try await performanceRepository.readSeries(for: account.requireID(),
116+
from: nil,
117+
to: nil)
118+
119+
let values = rows.map { performanceDTOMapper.performancePoint(from: $0) }
120+
.sorted { $0.date < $1.date }
121+
return PerformanceTimeSeriesDTO(values: values)
112122
}
113123

114124
private func requireAccount(_ req: Request, userID: UUID) async throws -> BrokerageAccount {
@@ -121,4 +131,3 @@ struct BrokerageAccountController: RouteCollection {
121131
}
122132

123133
extension BrokerageAccountDTO: Content { }
124-
extension PerformancePointDTO: Content { }

Sources/Grodt/Endpoints/brokerages/BrokerageController.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@ struct BrokerageController: RouteCollection {
77
private let accounts: BrokerageAccountRepository
88
private let currencyMapper: CurrencyDTOMapper
99
private let performanceRepository: PostgresBrokerageDailyPerformanceRepository
10-
private let performancePointDTOMapper: PerformancePointDTOMapper
10+
private let performanceDTOMapper: DatedPerformanceDTOMapper
1111

1212
init(brokerageRepository: BrokerageRepository,
1313
dtoMapper: BrokerageDTOMapper,
1414
accounts: BrokerageAccountRepository,
1515
currencyMapper: CurrencyDTOMapper,
1616
performanceRepository: PostgresBrokerageDailyPerformanceRepository,
17-
performancePointDTOMapper: PerformancePointDTOMapper
17+
performanceDTOMapper: DatedPerformanceDTOMapper
1818
) {
1919
self.brokerageRepository = brokerageRepository
2020
self.dtoMapper = dtoMapper
2121
self.accounts = accounts
2222
self.currencyMapper = currencyMapper
2323
self.performanceRepository = performanceRepository
24-
self.performancePointDTOMapper = performancePointDTOMapper
24+
self.performanceDTOMapper = performanceDTOMapper
2525
}
2626

2727
func boot(routes: RoutesBuilder) throws {
@@ -75,12 +75,15 @@ struct BrokerageController: RouteCollection {
7575
return .noContent
7676
}
7777

78-
private func performanceSeries(req: Request) async throws -> [PerformancePointDTO] {
78+
private func performanceSeries(req: Request) async throws -> PerformanceTimeSeriesDTO {
7979
let userID = try req.requireUserID()
8080
_ = try await requireBrokerage(req, userID: userID)
8181
let id = try req.parameters.require("id", as: UUID.self)
8282
let rows = try await performanceRepository.readSeries(for: id, from: nil, to: nil)
83-
return rows.map { performancePointDTOMapper.performancePoint(from: $0) }
83+
84+
let values = rows.map { performanceDTOMapper.performancePoint(from: $0) }
85+
.sorted { $0.date < $1.date }
86+
return PerformanceTimeSeriesDTO(values: values)
8487
}
8588

8689
private func requireBrokerage(_ req: Request, userID: UUID) async throws -> Brokerage {

0 commit comments

Comments
 (0)