diff --git a/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceCalculator.swift b/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceCalculator.swift index 5e7d345..8615abb 100644 --- a/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceCalculator.swift +++ b/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceCalculator.swift @@ -1,7 +1,7 @@ import Foundation protocol PortfolioPerformanceCalculating { - func performance(of portfolio: Portfolio, on date: YearMonthDayDate) async throws -> DatedPortfolioPerformance + func performance(of portfolio: Portfolio, on date: YearMonthDayDate, priceCache: inout [String: Decimal]) async throws -> DatedPortfolioPerformance } class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating { @@ -11,15 +11,28 @@ class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating { self.priceService = priceService } - func performance(of portfolio: Portfolio, on date: YearMonthDayDate) async throws -> DatedPortfolioPerformance { + func performance( + of portfolio: Portfolio, + on date: YearMonthDayDate, + priceCache: inout [String: Decimal] + ) async throws -> DatedPortfolioPerformance { let transactionsUntilDate = portfolio.transactions.filter { YearMonthDayDate($0.purchaseDate) <= date } let financialsForDate = Financials() + for transaction in transactionsUntilDate { let inAmount = transaction.numberOfShares * transaction.pricePerShareAtPurchase + transaction.fees await financialsForDate.addMoneyIn(inAmount) - let value = try await transaction.numberOfShares * self.priceService.price(for: transaction.ticker, on: date) + let price: Decimal + if let cachedPrice = priceCache[transaction.ticker] { + price = cachedPrice + } else { + price = try await self.priceService.price(for: transaction.ticker, on: date) + priceCache[transaction.ticker] = price + } + + let value = transaction.numberOfShares * price await financialsForDate.addValue(value) } diff --git a/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceUpdater.swift b/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceUpdater.swift index 0359ff9..91d60ea 100644 --- a/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceUpdater.swift +++ b/Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceUpdater.swift @@ -33,11 +33,20 @@ class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater { } func updatePerformanceOfAllPortfolios() async throws { - // Update historical prices for all tickers + // Remove all historrical prices + for quote in try await quoteRepository.allHistoricalQuote() { + try await quoteRepository.delete(quote) + } + + // Update historical prices and latest prices for all tickers let allTickers = try await tickerRepository.allTickers() for ticker in allTickers { await rateLimiter.waitIfNeeded() _ = try await priceService.fetchAndCreateHistoricalPrices(for: ticker.symbol) + await rateLimiter.waitIfNeeded() + if let quote = try await quoteRepository.quote(for: ticker.symbol) { + _ = try await priceService.fetchAndUpdatePrice(for: quote) + } } // Update historical performance for all portfolios @@ -53,10 +62,11 @@ class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater { func recalculateHistoricalPerformance(of portfolio: Portfolio) async throws { var datedPerformance = [DatedPortfolioPerformance]() guard let earliestTransaction = portfolio.earliestTransaction else { return } - let dates = dateRangeUntilYesterday(from: earliestTransaction.purchaseDate) + let dates = dateRangeUntilToday(from: earliestTransaction.purchaseDate) + var priceCache = [String: Decimal]() for date in dates { - let performanceForDate = try await performanceCalculator.performance(of: portfolio, on: date) + let performanceForDate = try await performanceCalculator.performance(of: portfolio, on: date, priceCache: &priceCache) datedPerformance.append(performanceForDate) } @@ -72,14 +82,14 @@ class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater { } } - private func dateRangeUntilYesterday(from startDate: Date) -> [YearMonthDayDate] { + private func dateRangeUntilToday(from startDate: Date) -> [YearMonthDayDate] { var dates: [YearMonthDayDate] = [] var currentDate = startDate let calendar = Calendar.current - let yesterday = calendar.date(byAdding: .day, value: -1, to: Date())! + let today = Date() - while currentDate <= yesterday { + while currentDate <= today { let ymdDate = YearMonthDayDate(currentDate) dates.append(ymdDate) currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)! diff --git a/Sources/Grodt/BusinessLogic/PriceService.swift b/Sources/Grodt/BusinessLogic/PriceService.swift index 6784ea3..627c47d 100644 --- a/Sources/Grodt/BusinessLogic/PriceService.swift +++ b/Sources/Grodt/BusinessLogic/PriceService.swift @@ -5,6 +5,7 @@ protocol PriceService { func price(for ticker: String) async throws -> Decimal func price(for ticker: String, on date: YearMonthDayDate) async throws -> Decimal func fetchAndCreateHistoricalPrices(for ticker: String) async throws -> HistoricalQuote + func fetchAndUpdatePrice(for outdatedQuote: Quote) async throws -> Decimal } class CachedPriceService: PriceService { @@ -93,15 +94,10 @@ class CachedPriceService: PriceService { let quotes = try await liveHistoricalPrices(for: ticker) let historicalQuote = HistoricalQuote(symbol: ticker, datedQuotes: quotes) try await quoteRepository.create(historicalQuote) - - if let previousQuote = try await quoteRepository.historicalQuote(for: ticker) { - try await quoteRepository.delete(previousQuote) - } - return historicalQuote } - private func fetchAndUpdatePrice(for outdatedQuote: Quote) async throws -> Decimal { + func fetchAndUpdatePrice(for outdatedQuote: Quote) async throws -> Decimal { let quote = try await latestQuote(for: outdatedQuote.symbol) outdatedQuote.lastUpdate = Date() try await quoteRepository.update(outdatedQuote) diff --git a/Sources/Grodt/Persistency/Repositories/QuoteRepository.swift b/Sources/Grodt/Persistency/Repositories/QuoteRepository.swift index 8831c57..cf16988 100644 --- a/Sources/Grodt/Persistency/Repositories/QuoteRepository.swift +++ b/Sources/Grodt/Persistency/Repositories/QuoteRepository.swift @@ -8,11 +8,15 @@ protocol QuoteRepository { func update(_ quote: Quote) async throws + func allHistoricalQuote() async throws -> [HistoricalQuote] + func historicalQuote(for ticker: String) async throws -> HistoricalQuote? func create(_ historicalQuote: HistoricalQuote) async throws func delete(_ historicalQuote: HistoricalQuote) async throws + + func update(_ historicalQuote: HistoricalQuote) async throws } class PostgresQuoteRepository: QuoteRepository { @@ -36,6 +40,11 @@ class PostgresQuoteRepository: QuoteRepository { try await quote.update(on: database) } + func allHistoricalQuote() async throws -> [HistoricalQuote] { + return try await HistoricalQuote.query(on: database) + .all() + } + func historicalQuote(for ticker: String) async throws -> HistoricalQuote? { return try await HistoricalQuote.query(on: database) .filter(\HistoricalQuote.$symbol == ticker) @@ -46,6 +55,10 @@ class PostgresQuoteRepository: QuoteRepository { try await historicalQuote.save(on: database) } + func update(_ historicalQuote: HistoricalQuote) async throws { + try await historicalQuote.update(on: database) + } + func delete(_ historicalQuote: HistoricalQuote) async throws { try await historicalQuote.delete(on: database) } diff --git a/docker-compose.yml b/docker-compose.yml index 2df3223..f5d9977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: db: image: postgres