Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Sources/Grodt/Application/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ func routes(_ app: Application) async throws {
let transactionRepository = PostgresTransactionRepository(database: app.db)
let brokerageRepository = PostgresBrokerageRepository(database: app.db)
let brokerageAccountRepository = PostgresBrokerageAccountRepository(database: app.db)
let brokerageAccountDailyRepository = PostgresBrokerageAccountDailyPerformanceRepository(database: app.db)
let brokerageAccountDailyPerformanceRepository = PostgresBrokerageAccountDailyPerformanceRepository(database: app.db)
let brokerageDailyPerformanceRepository = PostgresBrokerageDailyPerformanceRepository(database: app.db)

let portfolioDTOMapper = PortfolioDTOMapper(investmentDTOMapper: investmentDTOMapper,
transactionDTOMapper: transactionDTOMapper,
performanceDTOMapper: DatedPerformanceDTOMapper(),
currencyDTOMapper: currencyDTOMapper)
let currencyRepository = PostgresCurrencyRepository(database: app.db)
let portfolioPerformanceUpdater = PortfolioPerformanceUpdater(
Expand Down Expand Up @@ -95,8 +96,10 @@ func routes(_ app: Application) async throws {
accounts: brokerageAccountRepository,
currencyMapper: currencyDTOMapper,
performanceRepository: brokerageDailyPerformanceRepository,
performancePointDTOMapper: PerformancePointDTOMapper()))
performanceDTOMapper: DatedPerformanceDTOMapper()))
try protected.register(collection: BrokerageAccountController(brokerageAccountRepository: brokerageAccountRepository,
performanceRepository: brokerageAccountDailyPerformanceRepository,
performanceDTOMapper: DatedPerformanceDTOMapper(),
currencyMapper: currencyDTOMapper,
currencyRepository: currencyRepository))
}
Expand All @@ -109,12 +112,12 @@ func routes(_ app: Application) async throws {
portfolioPerformanceUpdater: portfolioPerformanceUpdater,
brokerageAccountPerformanceUpdater: BrokerageAccountPerformanceUpdater(transactionRepository: transactionRepository,
brokerageAccountRepository: brokerageAccountRepository,
accountDailyRepository: brokerageAccountDailyRepository,
accountDailyRepository: brokerageAccountDailyPerformanceRepository,
userRepository: userRepository,
calculator: performanceCalculator),
brokeragePerformanceUpdater: BrokeragePerformanceUpdater(userRepository: userRepository,
brokerageAccountRepository: brokerageAccountRepository,
accountDailyRepository: brokerageAccountDailyRepository,
accountDailyRepository: brokerageAccountDailyPerformanceRepository,
brokerageDailyRepository: brokerageDailyPerformanceRepository)
)
app.queues.schedule(nightlyUpdaterJob)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ final class BrokeragePerformanceUpdater: BrokeragePerformanceUpdating {
}

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

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

// Sum across accounts for each day
var summed: [DatedPortfolioPerformance] = []
var summed: [DatedPerformance] = []
summed.reserveCapacity(days.count)

for day in days {
Expand All @@ -98,7 +98,7 @@ final class BrokeragePerformanceUpdater: BrokeragePerformanceUpdating {
value += point.value
}
}
summed.append(DatedPortfolioPerformance(moneyIn: moneyIn, value: value, date: day))
summed.append(DatedPerformance(moneyIn: moneyIn, value: value, date: day))
}

// Replace the brokerage's series
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

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

struct HoldingsPerformanceCalculator: HoldingsPerformanceCalculating {
Expand All @@ -14,7 +14,7 @@ struct HoldingsPerformanceCalculator: HoldingsPerformanceCalculating {
for transactions: [Transaction],
from startDate: YearMonthDayDate,
to endDate: YearMonthDayDate
) async throws -> [DatedPortfolioPerformance] {
) async throws -> [DatedPerformance] {
guard !transactions.isEmpty else { return [] }
guard endDate >= startDate else { return [] }

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

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

for day in days {
Expand Down
37 changes: 0 additions & 37 deletions Sources/Grodt/DTOs/BrokerageDTO.swift

This file was deleted.

16 changes: 16 additions & 0 deletions Sources/Grodt/DTOs/DTOMappers/DatedPerformanceDTOMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

struct DatedPerformanceDTOMapper {
func performancePoint(from enity: DatedPerformance) -> DatedPerformanceDTO {
let moneyIn = enity.moneyIn
let moneyOut = enity.value
let profit = moneyOut - moneyIn
let totalReturn: Decimal = moneyIn > 0 ? (profit / moneyIn).rounded(to: 2) : 0

return DatedPerformanceDTO(date: enity.date.date,
moneyIn: moneyIn,
moneyOut: moneyOut,
profit: profit,
totalReturn: totalReturn)
}
}
7 changes: 0 additions & 7 deletions Sources/Grodt/DTOs/DTOMappers/PerformancePointDTOMapper.swift

This file was deleted.

30 changes: 10 additions & 20 deletions Sources/Grodt/DTOs/DTOMappers/PortfolioDTOMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ class PortfolioDTOMapper {
private let investmentDTOMapper: InvestmentDTOMapper
private let currencyDTOMapper: CurrencyDTOMapper
private let transactionDTOMapper: TransactionDTOMapper
private let performanceDTOMapper: DatedPerformanceDTOMapper

init(investmentDTOMapper: InvestmentDTOMapper,
transactionDTOMapper: TransactionDTOMapper,
performanceDTOMapper: DatedPerformanceDTOMapper,
currencyDTOMapper: CurrencyDTOMapper) {
self.investmentDTOMapper = investmentDTOMapper
self.transactionDTOMapper = transactionDTOMapper
self.performanceDTOMapper = performanceDTOMapper
self.currencyDTOMapper = currencyDTOMapper
}

Expand All @@ -35,45 +38,32 @@ class PortfolioDTOMapper {
)
}

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

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

return PortfolioPerformanceDTO(
return PerformanceDTO(
moneyIn: moneyIn,
moneyOut: moneyOut,
profit: profit,
totalReturn: totalReturn
)
}

func timeSeriesPerformance(from series: [DatedPortfolioPerformance]) async -> PortfolioPerformanceTimeSeriesDTO {
let values: [DatedPortfolioPerformanceDTO] = series
.map { point in
let moneyIn = point.moneyIn
let moneyOut = point.value
let profit = moneyOut - moneyIn
let totalReturn: Decimal = moneyIn > 0 ? (profit / moneyIn).rounded(to: 2) : 0
return DatedPortfolioPerformanceDTO(
date: point.date.date,
moneyIn: moneyIn,
moneyOut: moneyOut,
profit: profit,
totalReturn: totalReturn
)
}
func timeSeriesPerformance(from series: [DatedPerformance]) async -> PerformanceTimeSeriesDTO {
let values = series.map { performanceDTOMapper.performancePoint(from: $0) }
.sorted { $0.date < $1.date }

return PortfolioPerformanceTimeSeriesDTO(values: values)
return PerformanceTimeSeriesDTO(values: values)
}
}

Expand Down
17 changes: 17 additions & 0 deletions Sources/Grodt/DTOs/PerformanceDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

struct PerformanceDTO: Codable, Equatable {
let moneyIn: Decimal
let moneyOut: Decimal
let profit: Decimal
let totalReturn: Decimal

static var zero: PerformanceDTO {
return .init(
moneyIn: 0,
moneyOut: 0,
profit: 0,
totalReturn: 0
)
}
}
6 changes: 3 additions & 3 deletions Sources/Grodt/DTOs/PerformanceTimeSeriesDTO.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Foundation

struct PortfolioPerformanceTimeSeriesDTO: Codable, Equatable {
let values: [DatedPortfolioPerformanceDTO]
struct PerformanceTimeSeriesDTO: Codable, Equatable {
let values: [DatedPerformanceDTO]
}

struct DatedPortfolioPerformanceDTO: Codable, Equatable {
struct DatedPerformanceDTO: Codable, Equatable {
let date: Date
let moneyIn: Decimal
let moneyOut: Decimal
Expand Down
2 changes: 1 addition & 1 deletion Sources/Grodt/DTOs/PortfolioDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ struct PortfolioDTO: Codable, Equatable {
let id: String
let name: String
let currency: CurrencyDTO
let performance: PortfolioPerformanceDTO
let performance: PerformanceDTO
let investments: [InvestmentDTO]
let transactions: [TransactionDTO]
}
2 changes: 1 addition & 1 deletion Sources/Grodt/DTOs/PortfolioInfoDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ struct PortfolioInfoDTO: Codable, Equatable {
let id: String
let name: String
let currency: CurrencyDTO
let performance: PortfolioPerformanceDTO
let performance: PerformanceDTO
}
8 changes: 0 additions & 8 deletions Sources/Grodt/DTOs/PortfolioPerformanceDTO.swift

This file was deleted.

6 changes: 3 additions & 3 deletions Sources/Grodt/Endpoints/PortfoliosController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct PortfoliosController: RouteCollection {
portfolio.put(use: update)
portfolio.delete(use: delete)

portfolio.group("historicalPerformance") { pref in
portfolio.group("performance") { pref in
pref.get(use: historicalPerformance)
}
}
Expand Down Expand Up @@ -121,7 +121,7 @@ struct PortfoliosController: RouteCollection {
return .ok
}

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

Expand All @@ -136,4 +136,4 @@ struct PortfoliosController: RouteCollection {

extension PortfolioDTO: Content { }
extension PortfolioInfoDTO: Content { }
extension PortfolioPerformanceTimeSeriesDTO: Content { }
extension PerformanceTimeSeriesDTO: Content { }
Loading