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
12 changes: 6 additions & 6 deletions Sources/Grodt/Application/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ func routes(_ app: Application) async throws {
let tickerDTOMapper = TickerDTOMapper()
let loginResponseDTOMapper = LoginResponseDTOMapper()
let transactionDTOMapper = TransactionDTOMapper(currencyDTOMapper: currencyDTOMapper)
let priceService = CachedPriceService(quoteRepository: PostgresQuoteRepository(database: app.db),
alphavantage: alphavantage)
let livePriceService = LivePriceService(alphavantage: alphavantage)
let quoteCache = PostgresQuoteRepository(database: app.db)
let priceService = CachedPriceService(priceService: livePriceService, cache: quoteCache)
let portfolioPerformanceCalculator = PortfolioPerformanceCalculator(priceService: priceService)
let portfolioDTOMapper = PortfolioDTOMapper(transactionDTOMapper: transactionDTOMapper,
currencyDTOMapper: currencyDTOMapper,
Expand All @@ -19,10 +20,9 @@ func routes(_ app: Application) async throws {
userRepository: PostgresUserRepository(database: app.db),
portfolioRepository: PostgresPortfolioRepository(database: app.db),
tickerRepository: PostgresTickerRepository(database: app.db),
quoteRepository: PostgresQuoteRepository(database: app.db),
quoteCache: quoteCache,
priceService: priceService,
performanceCalculator: portfolioPerformanceCalculator,
dataMapper: portfolioDTOMapper)
performanceCalculator: portfolioPerformanceCalculator)
let transactionChangedHandler = TransactionChangedHandler(portfolioRepository: PostgresPortfolioRepository(database: app.db),
historicalPerformanceUpdater: portfolioPerformanceUpdater)

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

app.queues.schedule(PortfolioPerformanceUpdaterJob(performanceUpdater: portfolioPerformanceUpdater))
.daily()
.at(9, 0)
.at(1, 0)
app.queues.add(LoggingJobEventDelegate(logger: app.logger))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

class DictionaryInMemoryTickerPriceCache: InMemoryTickerPriceCache {
private var dictionary = [QuoteKey: Decimal]()

func price(for ticker: String, on date: YearMonthDayDate) -> Decimal? {
return dictionary[QuoteKey(ticker, date)]
}

func setPrice(_ price: Decimal, for ticker: String, on date: YearMonthDayDate) {
dictionary[QuoteKey(ticker, date)] = price
}
}

fileprivate struct QuoteKey: Hashable {
let ticker: String
let date: YearMonthDayDate

init(_ ticker: String, _ date: YearMonthDayDate) {
self.ticker = ticker
self.date = date
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

protocol InMemoryTickerPriceCache {
func price(for ticker: String, on date: YearMonthDayDate) -> Decimal?
func setPrice(_ price: Decimal, for ticker: String, on date: YearMonthDayDate)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Foundation

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

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

Expand All @@ -23,15 +25,7 @@ class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating {
for transaction in transactionsUntilDate {
let inAmount = transaction.numberOfShares * transaction.pricePerShareAtPurchase + transaction.fees
await financialsForDate.addMoneyIn(inAmount)

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 price = try await self.price(for: transaction.ticker, on: date, using: cache)
let value = transaction.numberOfShares * price
await financialsForDate.addValue(value)
}
Expand All @@ -43,4 +37,39 @@ class PortfolioPerformanceCalculator: PortfolioPerformanceCalculating {
)
return performanceForDate
}

private func price(for ticker: String,
on date: YearMonthDayDate,
using cache: InMemoryTickerPriceCache) async throws -> Decimal {
if let cachedPrice = cache.price(for: ticker, on: date) {
return cachedPrice
}
let price = try await computePrice(for: ticker, on: date, using: cache)
cache.setPrice(price, for: ticker, on: date)
return price
}

private func computePrice(for ticker: String,
on date: YearMonthDayDate,
using cache: InMemoryTickerPriceCache) async throws -> Decimal {
if date == YearMonthDayDate(Date()) {
return try await priceService.price(for: ticker)
}

var calendar = Calendar.current
calendar.timeZone = TimeZone.universalGMT
var quote: Decimal?
var dateToCheck = date

for _ in 0..<7 {
if quote != nil {
break
}
dateToCheck = YearMonthDayDate(calendar.date(byAdding: .day, value: -1, to: dateToCheck.date)!)
quote = cache.price(for: ticker, on: dateToCheck)
}

// TODO: What to do if no price????
return quote!
}
}
Original file line number Diff line number Diff line change
@@ -1,72 +1,60 @@
import Foundation

protocol PortfolioHistoricalPerformanceUpdater {
func recalculateHistoricalPerformance(of portfolio: Portfolio) async throws
func recalculatePerformance(of portfolio: Portfolio) async throws
func updatePerformanceOfAllPortfolios() async throws
}

class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater {
private let userRepository: UserRepository
private let portfolioRepository: PortfolioRepository
private let tickerRepository: TickerRepository
private let quoteRepository: QuoteRepository
private let quoteCache: QuoteCache
private let priceService: PriceService
private let performanceCalculator: PortfolioPerformanceCalculating
private let dataMapper: PortfolioDTOMapper

private let rateLimiter = RateLimiter(maxRequestsPerMinute: 5)

init(userRepository: UserRepository,
portfolioRepository: PortfolioRepository,
tickerRepository: TickerRepository,
quoteRepository: QuoteRepository,
quoteCache: QuoteCache,
priceService: PriceService,
performanceCalculator: PortfolioPerformanceCalculating,
dataMapper: PortfolioDTOMapper) {
performanceCalculator: PortfolioPerformanceCalculating) {
self.userRepository = userRepository
self.portfolioRepository = portfolioRepository
self.tickerRepository = tickerRepository
self.quoteRepository = quoteRepository
self.quoteCache = quoteCache
self.priceService = priceService
self.performanceCalculator = performanceCalculator
self.dataMapper = dataMapper
}

func updatePerformanceOfAllPortfolios() async throws {
// 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
let users = try await userRepository.allUsers()
for user in users {
let allPortfolios = try await portfolioRepository.allPortfolios(for: user.id!)
for portfolio in allPortfolios {
try await recalculateHistoricalPerformance(of: portfolio)
}
}
try await updateAllTickerPrices()
try await updateHistotrycalPerformances()
}

func recalculateHistoricalPerformance(of portfolio: Portfolio) async throws {
func recalculatePerformance(of portfolio: Portfolio) async throws {
var datedPerformance = [DatedPortfolioPerformance]()
guard let earliestTransaction = portfolio.earliestTransaction else { return }
let dates = dateRangeUntilToday(from: earliestTransaction.purchaseDate)
var priceCache = [String: Decimal]()

let startDate: Date = {
guard let earliestTransaction = portfolio.earliestTransaction else { return Date() }
return earliestTransaction.purchaseDate
}()

let dictCache = DictionaryInMemoryTickerPriceCache()
for transaction in portfolio.transactions {
if let storedQuotes = try await quoteCache.historicalQuote(for: transaction.ticker) {
storedQuotes.datedQuotes.forEach { datedQuote in
dictCache.setPrice(datedQuote.price, for: transaction.ticker, on: datedQuote.date)
}
}
}

let dates = dateRangeUntilToday(from: startDate)
for date in dates {
let performanceForDate = try await performanceCalculator.performance(of: portfolio, on: date, priceCache: &priceCache)
let performanceForDate = try await performanceCalculator.performance(of: portfolio,
on: date,
using: dictCache)
datedPerformance.append(performanceForDate)
}

Expand All @@ -81,6 +69,33 @@ class PortfolioPerformanceUpdater: PortfolioHistoricalPerformanceUpdater {
try await portfolioRepository.createHistoricalPerformance(historicalPerformance)
}
}

private func updateAllTickerPrices() async throws{
let allTickers = try await tickerRepository.allTickers()
for ticker in allTickers {
try await clearCache(for: ticker.symbol)

await rateLimiter.waitIfNeeded()
_ = try await priceService.historicalPrice(for: ticker.symbol)
await rateLimiter.waitIfNeeded()
_ = try await priceService.price(for: ticker.symbol)
}
}

private func updateHistotrycalPerformances() async throws {
let users = try await userRepository.allUsers()
for user in users {
let allPortfolios = try await portfolioRepository.allPortfolios(for: user.id!)
for portfolio in allPortfolios {
try await recalculatePerformance(of: portfolio)
}
}
}

private func clearCache(for ticker: String) async throws {
try await quoteCache.clearHistoricalQuote(for: ticker)
try await quoteCache.clearQuote(for: ticker)
}

private func dateRangeUntilToday(from startDate: Date) -> [YearMonthDayDate] {
var dates: [YearMonthDayDate] = []
Expand Down
Loading
Loading