@@ -2,48 +2,95 @@ import Foundation
22import CollectionConcurrencyKit
33
44class InvestmentDTOMapper {
5+ enum InvestmentError : Error {
6+ case invalidPrice( for: String )
7+ }
8+
59 private let currencyDTOMapper : CurrencyDTOMapper
10+ private let transactionDTOMapper : TransactionDTOMapper
611 private let tickerRepository : TickerRepository
712 private let priceService : PriceService
813
914 init ( currencyDTOMapper: CurrencyDTOMapper ,
15+ transactionDTOMapper: TransactionDTOMapper ,
1016 tickerRepository: TickerRepository ,
1117 priceService: PriceService ) {
1218 self . currencyDTOMapper = currencyDTOMapper
19+ self . transactionDTOMapper = transactionDTOMapper
1320 self . priceService = priceService
1421 self . tickerRepository = tickerRepository
1522 }
1623
1724 func investments( from transactions: [ Transaction ] ) async throws -> [ InvestmentDTO ] {
18- let investments : [ InvestmentDTO ] = try await transactions
19- . grouped { $0. ticker }
20- . asyncCompactMap { ( ticker, transactions) in
21- let name = try await tickerRepository. tickers ( for: ticker) ? . name ?? " "
22- let latestPrice = try await priceService. price ( for: ticker)
23- var pricePerPurchase : [ Decimal : Decimal ] = [ : ]
24- var numberOfShares : Decimal = 0
25- var totalCost : Decimal = 0
26- transactions. forEach { transaction in
27- pricePerPurchase [ transaction. pricePerShareAtPurchase] = transaction. numberOfShares
28- numberOfShares += transaction. numberOfShares
29- totalCost += ( transaction. pricePerShareAtPurchase * transaction. numberOfShares) + transaction. fees
30- }
31- let currentValue = numberOfShares * latestPrice
32- let avgBuyPrice = pricePerPurchase. keys. reduce ( 0 ) { $0 + $1 } / Decimal( pricePerPurchase. count)
33- let profit = currentValue - totalCost
34- let totalReturn = ( totalCost == 0 ? 0 : profit / totalCost) . ro
35-
36- return InvestmentDTO ( name: name,
37- shortName: ticker,
38- avgBuyPrice: avgBuyPrice,
39- latestPrice: latestPrice,
40- totalReturn: totalReturn,
41- profit: profit,
42- value: currentValue,
43- numberOfShares: numberOfShares,
44- currency: currencyDTOMapper. currency ( from: transactions. first!. currency) )
25+ let groupedTransactions = transactions. grouped { $0. ticker }
26+
27+ let investments : [ InvestmentDTO ] = try await groupedTransactions. asyncCompactMap { ticker, transactions in
28+ guard let firstTransaction = transactions. first else { return nil }
29+
30+ async let name = tickerRepository. tickers ( for: ticker) ? . name ?? " "
31+ async let latestPrice = priceService. price ( for: ticker)
32+ let aggregates = calculateTransactionAggregates ( transactions)
33+
34+ let fetchedLatestPrice = try await latestPrice
35+ guard fetchedLatestPrice > 0 else {
36+ throw InvestmentError . invalidPrice ( for: ticker)
4537 }
38+
39+ let currentValue = aggregates. numberOfShares * fetchedLatestPrice
40+ let profit = currentValue - aggregates. totalCost
41+ let totalReturn = calculateTotalReturn ( profit: profit, cost: aggregates. totalCost)
42+
43+ return InvestmentDTO (
44+ name: try await name,
45+ shortName: ticker,
46+ avgBuyPrice: aggregates. avgBuyPrice,
47+ latestPrice: fetchedLatestPrice,
48+ totalReturn: totalReturn,
49+ profit: profit,
50+ value: currentValue,
51+ numberOfShares: aggregates. numberOfShares,
52+ currency: currencyDTOMapper. currency ( from: firstTransaction. currency)
53+ )
54+ }
4655
4756 return investments
4857 }
58+
59+ func investmentDetail( from transactions: [ Transaction ] ) async throws -> InvestmentDetailDTO {
60+ let investmentDTO = try await investments ( from: transactions) . first!
61+ let transactions = transactions. compactMap { transactionDTOMapper. transaction ( from: $0) }
62+ return InvestmentDetailDTO ( name: investmentDTO. name,
63+ shortName: investmentDTO. shortName,
64+ avgBuyPrice: investmentDTO. avgBuyPrice,
65+ latestPrice: investmentDTO. latestPrice,
66+ totalReturn: investmentDTO. totalReturn,
67+ profit: investmentDTO. profit,
68+ value: investmentDTO. value,
69+ numberOfShares: investmentDTO. numberOfShares,
70+ currency: investmentDTO. currency,
71+ transactions: transactions)
72+ }
73+
74+ private func calculateTransactionAggregates( _ transactions: [ Transaction ] ) -> ( avgBuyPrice: Decimal , totalCost: Decimal , numberOfShares: Decimal ) {
75+ var totalCost : Decimal = 0
76+ var numberOfShares : Decimal = 0
77+ var pricePerPurchase : [ Decimal : Decimal ] = [ : ]
78+
79+ transactions. forEach { transaction in
80+ pricePerPurchase [ transaction. pricePerShareAtPurchase] = transaction. numberOfShares
81+ totalCost += ( transaction. pricePerShareAtPurchase * transaction. numberOfShares) + transaction. fees
82+ numberOfShares += transaction. numberOfShares
83+ }
84+
85+ let avgBuyPrice = pricePerPurchase. keys. reduce ( 0 ) { $0 + $1 } / Decimal( pricePerPurchase. count)
86+ return ( avgBuyPrice: avgBuyPrice, totalCost: totalCost, numberOfShares: numberOfShares)
87+ }
88+
89+ private func calculateTotalReturn( profit: Decimal , cost: Decimal ) -> Decimal {
90+ guard cost != 0 else { return 0 }
91+ var totalReturn = profit / cost
92+ var roundedReturn = Decimal ( )
93+ NSDecimalRound ( & roundedReturn, & totalReturn, 2 , . bankers)
94+ return roundedReturn
95+ }
4996}
0 commit comments