Skip to content

Commit 6285b3e

Browse files
authored
Portfolio performance imp (#7)
1 parent 5a0eb6b commit 6285b3e

File tree

12 files changed

+269
-219
lines changed

12 files changed

+269
-219
lines changed

Sources/Grodt/Application/routes.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ func routes(_ app: Application) async throws {
99
let tickerDTOMapper = TickerDTOMapper()
1010
let loginResponseDTOMapper = LoginResponseDTOMapper()
1111
let transactionDTOMapper = TransactionDTOMapper(currencyDTOMapper: currencyDTOMapper)
12-
let priceService = CachedPriceService(quoteRepository: PostgresQuoteRepository(database: app.db),
13-
alphavantage: alphavantage)
12+
let livePriceService = LivePriceService(alphavantage: alphavantage)
13+
let quoteCache = PostgresQuoteRepository(database: app.db)
14+
let priceService = CachedPriceService(priceService: livePriceService, cache: quoteCache)
1415
let portfolioPerformanceCalculator = PortfolioPerformanceCalculator(priceService: priceService)
1516
let portfolioDTOMapper = PortfolioDTOMapper(transactionDTOMapper: transactionDTOMapper,
1617
currencyDTOMapper: currencyDTOMapper,
@@ -19,10 +20,9 @@ func routes(_ app: Application) async throws {
1920
userRepository: PostgresUserRepository(database: app.db),
2021
portfolioRepository: PostgresPortfolioRepository(database: app.db),
2122
tickerRepository: PostgresTickerRepository(database: app.db),
22-
quoteRepository: PostgresQuoteRepository(database: app.db),
23+
quoteCache: quoteCache,
2324
priceService: priceService,
24-
performanceCalculator: portfolioPerformanceCalculator,
25-
dataMapper: portfolioDTOMapper)
25+
performanceCalculator: portfolioPerformanceCalculator)
2626
let transactionChangedHandler = TransactionChangedHandler(portfolioRepository: PostgresPortfolioRepository(database: app.db),
2727
historicalPerformanceUpdater: portfolioPerformanceUpdater)
2828

@@ -67,6 +67,6 @@ func routes(_ app: Application) async throws {
6767

6868
app.queues.schedule(PortfolioPerformanceUpdaterJob(performanceUpdater: portfolioPerformanceUpdater))
6969
.daily()
70-
.at(9, 0)
70+
.at(1, 0)
7171
app.queues.add(LoggingJobEventDelegate(logger: app.logger))
7272
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
3+
class DictionaryInMemoryTickerPriceCache: InMemoryTickerPriceCache {
4+
private var dictionary = [QuoteKey: Decimal]()
5+
6+
func price(for ticker: String, on date: YearMonthDayDate) -> Decimal? {
7+
return dictionary[QuoteKey(ticker, date)]
8+
}
9+
10+
func setPrice(_ price: Decimal, for ticker: String, on date: YearMonthDayDate) {
11+
dictionary[QuoteKey(ticker, date)] = price
12+
}
13+
}
14+
15+
fileprivate struct QuoteKey: Hashable {
16+
let ticker: String
17+
let date: YearMonthDayDate
18+
19+
init(_ ticker: String, _ date: YearMonthDayDate) {
20+
self.ticker = ticker
21+
self.date = date
22+
}
23+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Foundation
2+
3+
protocol InMemoryTickerPriceCache {
4+
func price(for ticker: String, on date: YearMonthDayDate) -> Decimal?
5+
func setPrice(_ price: Decimal, for ticker: String, on date: YearMonthDayDate)
6+
}
Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import Foundation
22

33
protocol PortfolioPerformanceCalculating {
4-
func performance(of portfolio: Portfolio, on date: YearMonthDayDate, priceCache: inout [String: Decimal]) async throws -> DatedPortfolioPerformance
4+
func performance(of portfolio: Portfolio,
5+
on date: YearMonthDayDate,
6+
using cache: InMemoryTickerPriceCache) async throws -> DatedPortfolioPerformance
57
}
68

79
class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating {
@@ -14,7 +16,7 @@ class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating {
1416
func performance(
1517
of portfolio: Portfolio,
1618
on date: YearMonthDayDate,
17-
priceCache: inout [String: Decimal]
19+
using cache: InMemoryTickerPriceCache
1820
) async throws -> DatedPortfolioPerformance {
1921
let transactionsUntilDate = portfolio.transactions.filter { YearMonthDayDate($0.purchaseDate) <= date }
2022

@@ -23,15 +25,7 @@ class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating {
2325
for transaction in transactionsUntilDate {
2426
let inAmount = transaction.numberOfShares * transaction.pricePerShareAtPurchase + transaction.fees
2527
await financialsForDate.addMoneyIn(inAmount)
26-
27-
let price: Decimal
28-
if let cachedPrice = priceCache[transaction.ticker] {
29-
price = cachedPrice
30-
} else {
31-
price = try await self.priceService.price(for: transaction.ticker, on: date)
32-
priceCache[transaction.ticker] = price
33-
}
34-
28+
let price = try await self.price(for: transaction.ticker, on: date, using: cache)
3529
let value = transaction.numberOfShares * price
3630
await financialsForDate.addValue(value)
3731
}
@@ -43,4 +37,39 @@ class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating {
4337
)
4438
return performanceForDate
4539
}
40+
41+
private func price(for ticker: String,
42+
on date: YearMonthDayDate,
43+
using cache: InMemoryTickerPriceCache) async throws -> Decimal {
44+
if let cachedPrice = cache.price(for: ticker, on: date) {
45+
return cachedPrice
46+
}
47+
let price = try await computePrice(for: ticker, on: date, using: cache)
48+
cache.setPrice(price, for: ticker, on: date)
49+
return price
50+
}
51+
52+
private func computePrice(for ticker: String,
53+
on date: YearMonthDayDate,
54+
using cache: InMemoryTickerPriceCache) async throws -> Decimal {
55+
if date == YearMonthDayDate(Date()) {
56+
return try await priceService.price(for: ticker)
57+
}
58+
59+
var calendar = Calendar.current
60+
calendar.timeZone = TimeZone.universalGMT
61+
var quote: Decimal?
62+
var dateToCheck = date
63+
64+
for _ in 0..<7 {
65+
if quote != nil {
66+
break
67+
}
68+
dateToCheck = YearMonthDayDate(calendar.date(byAdding: .day, value: -1, to: dateToCheck.date)!)
69+
quote = cache.price(for: ticker, on: dateToCheck)
70+
}
71+
72+
// TODO: What to do if no price????
73+
return quote!
74+
}
4675
}

Sources/Grodt/BusinessLogic/PortfolioPerformance/PortfolioPerformanceUpdater.swift

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,60 @@
11
import Foundation
22

33
protocol PortfolioHistoricalPerformanceUpdater {
4-
func recalculateHistoricalPerformance(of portfolio: Portfolio) async throws
4+
func recalculatePerformance(of portfolio: Portfolio) async throws
55
func updatePerformanceOfAllPortfolios() async throws
66
}
77

88
class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater {
99
private let userRepository: UserRepository
1010
private let portfolioRepository: PortfolioRepository
1111
private let tickerRepository: TickerRepository
12-
private let quoteRepository: QuoteRepository
12+
private let quoteCache: QuoteCache
1313
private let priceService: PriceService
1414
private let performanceCalculator: PortfolioPerformanceCalculating
15-
private let dataMapper: PortfolioDTOMapper
1615

1716
private let rateLimiter = RateLimiter(maxRequestsPerMinute: 5)
1817

1918
init(userRepository: UserRepository,
2019
portfolioRepository: PortfolioRepository,
2120
tickerRepository: TickerRepository,
22-
quoteRepository: QuoteRepository,
21+
quoteCache: QuoteCache,
2322
priceService: PriceService,
24-
performanceCalculator: PortfolioPerformanceCalculating,
25-
dataMapper: PortfolioDTOMapper) {
23+
performanceCalculator: PortfolioPerformanceCalculating) {
2624
self.userRepository = userRepository
2725
self.portfolioRepository = portfolioRepository
2826
self.tickerRepository = tickerRepository
29-
self.quoteRepository = quoteRepository
27+
self.quoteCache = quoteCache
3028
self.priceService = priceService
3129
self.performanceCalculator = performanceCalculator
32-
self.dataMapper = dataMapper
3330
}
3431

3532
func updatePerformanceOfAllPortfolios() async throws {
36-
// Remove all historrical prices
37-
for quote in try await quoteRepository.allHistoricalQuote() {
38-
try await quoteRepository.delete(quote)
39-
}
40-
41-
// Update historical prices and latest prices for all tickers
42-
let allTickers = try await tickerRepository.allTickers()
43-
for ticker in allTickers {
44-
await rateLimiter.waitIfNeeded()
45-
_ = try await priceService.fetchAndCreateHistoricalPrices(for: ticker.symbol)
46-
await rateLimiter.waitIfNeeded()
47-
if let quote = try await quoteRepository.quote(for: ticker.symbol) {
48-
_ = try await priceService.fetchAndUpdatePrice(for: quote)
49-
}
50-
}
51-
52-
// Update historical performance for all portfolios
53-
let users = try await userRepository.allUsers()
54-
for user in users {
55-
let allPortfolios = try await portfolioRepository.allPortfolios(for: user.id!)
56-
for portfolio in allPortfolios {
57-
try await recalculateHistoricalPerformance(of: portfolio)
58-
}
59-
}
33+
try await updateAllTickerPrices()
34+
try await updateHistotrycalPerformances()
6035
}
6136

62-
func recalculateHistoricalPerformance(of portfolio: Portfolio) async throws {
37+
func recalculatePerformance(of portfolio: Portfolio) async throws {
6338
var datedPerformance = [DatedPortfolioPerformance]()
64-
guard let earliestTransaction = portfolio.earliestTransaction else { return }
65-
let dates = dateRangeUntilToday(from: earliestTransaction.purchaseDate)
66-
var priceCache = [String: Decimal]()
67-
39+
let startDate: Date = {
40+
guard let earliestTransaction = portfolio.earliestTransaction else { return Date() }
41+
return earliestTransaction.purchaseDate
42+
}()
43+
44+
let dictCache = DictionaryInMemoryTickerPriceCache()
45+
for transaction in portfolio.transactions {
46+
if let storedQuotes = try await quoteCache.historicalQuote(for: transaction.ticker) {
47+
storedQuotes.datedQuotes.forEach { datedQuote in
48+
dictCache.setPrice(datedQuote.price, for: transaction.ticker, on: datedQuote.date)
49+
}
50+
}
51+
}
52+
53+
let dates = dateRangeUntilToday(from: startDate)
6854
for date in dates {
69-
let performanceForDate = try await performanceCalculator.performance(of: portfolio, on: date, priceCache: &priceCache)
55+
let performanceForDate = try await performanceCalculator.performance(of: portfolio,
56+
on: date,
57+
using: dictCache)
7058
datedPerformance.append(performanceForDate)
7159
}
7260

@@ -81,6 +69,33 @@ class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater {
8169
try await portfolioRepository.createHistoricalPerformance(historicalPerformance)
8270
}
8371
}
72+
73+
private func updateAllTickerPrices() async throws{
74+
let allTickers = try await tickerRepository.allTickers()
75+
for ticker in allTickers {
76+
try await clearCache(for: ticker.symbol)
77+
78+
await rateLimiter.waitIfNeeded()
79+
_ = try await priceService.historicalPrice(for: ticker.symbol)
80+
await rateLimiter.waitIfNeeded()
81+
_ = try await priceService.price(for: ticker.symbol)
82+
}
83+
}
84+
85+
private func updateHistotrycalPerformances() async throws {
86+
let users = try await userRepository.allUsers()
87+
for user in users {
88+
let allPortfolios = try await portfolioRepository.allPortfolios(for: user.id!)
89+
for portfolio in allPortfolios {
90+
try await recalculatePerformance(of: portfolio)
91+
}
92+
}
93+
}
94+
95+
private func clearCache(for ticker: String) async throws {
96+
try await quoteCache.clearHistoricalQuote(for: ticker)
97+
try await quoteCache.clearQuote(for: ticker)
98+
}
8499

85100
private func dateRangeUntilToday(from startDate: Date) -> [YearMonthDayDate] {
86101
var dates: [YearMonthDayDate] = []

0 commit comments

Comments
 (0)