app is now using historical exchange rates for transactions

This commit is contained in:
Matthias Hinrichs
2025-07-05 03:44:53 +02:00
parent a96bbe4d2a
commit aef9342cc5
11 changed files with 926 additions and 237 deletions
+180 -16
View File
@@ -18,7 +18,7 @@ func calculateTotalInvested(activities []model.Activity) float64 {
return total
}
// calculateTotalInvestedInBaseCurrency calculates total invested converted to portfolio base currency
// calculateTotalInvestedInBaseCurrency calculates total invested converted to portfolio base currency using historical rates
func calculateTotalInvestedInBaseCurrency(activities []model.Activity, baseCurrency string) float64 {
var total float64 = 0
for _, activity := range activities {
@@ -28,10 +28,17 @@ func calculateTotalInvestedInBaseCurrency(activities []model.Activity, baseCurre
if activity.Currency == baseCurrency {
total += activityTotal
} else {
convertedTotal, err := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
// Use historical exchange rate for the transaction date
convertedTotal, err := util.ConvertCurrencyHistorical(activityTotal, activity.Currency, baseCurrency, activity.Date)
if err != nil {
// If conversion fails, add original amount (fallback)
total += activityTotal
// If historical conversion fails, try current rate as fallback
fallbackTotal, fallbackErr := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
if fallbackErr != nil {
// If all conversions fail, add original amount (last resort)
total += activityTotal
} else {
total += fallbackTotal
}
} else {
total += convertedTotal
}
@@ -55,13 +62,13 @@ func getLastBuyDate(activities []model.Activity) string {
return lastBuy.Format("02.01.2006")
}
// convertActivityPrice converts activity price to portfolio base currency if different
// convertActivityPrice converts activity price to portfolio base currency using historical rate
func convertActivityPrice(activity model.Activity, portfolioBaseCurrency string) (float64, string, error) {
if activity.Currency == portfolioBaseCurrency {
return activity.Price, "", nil // No conversion needed
}
convertedPrice, err := util.ConvertCurrency(activity.Price, activity.Currency, portfolioBaseCurrency)
convertedPrice, err := util.ConvertCurrencyHistorical(activity.Price, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return 0, "", err
}
@@ -69,13 +76,13 @@ func convertActivityPrice(activity model.Activity, portfolioBaseCurrency string)
return convertedPrice, portfolioBaseCurrency, nil
}
// formatActivityPrice formats activity price with conversion if needed
// formatActivityPrice formats activity price with conversion if needed using historical rates
func formatActivityPrice(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return fmt.Sprintf("%.2f %s", activity.Price, activity.Currency)
}
convertedPrice, err := util.ConvertCurrency(activity.Price, activity.Currency, portfolioBaseCurrency)
convertedPrice, err := util.ConvertCurrencyHistorical(activity.Price, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return fmt.Sprintf("%.2f %s (conv. error)", activity.Price, activity.Currency)
}
@@ -83,7 +90,7 @@ func formatActivityPrice(activity model.Activity, portfolioBaseCurrency string)
return fmt.Sprintf("%.2f %s (~%.2f %s)", activity.Price, activity.Currency, convertedPrice, portfolioBaseCurrency)
}
// formatActivityTotal formats activity total with conversion if needed
// formatActivityTotal formats activity total with conversion if needed using historical rates
func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string) string {
total := activity.Amount * activity.Price
@@ -91,7 +98,7 @@ func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string)
return fmt.Sprintf("%.2f %s", total, activity.Currency)
}
convertedTotal, err := util.ConvertCurrency(total, activity.Currency, portfolioBaseCurrency)
convertedTotal, err := util.ConvertCurrencyHistorical(total, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return fmt.Sprintf("%.2f %s (conv. error)", total, activity.Currency)
}
@@ -99,28 +106,38 @@ func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string)
return fmt.Sprintf("%.2f %s (~%.2f %s)", total, activity.Currency, convertedTotal, portfolioBaseCurrency)
}
// getConvertedPrice returns the converted price or original if conversion fails
// getConvertedPrice returns the converted price using historical exchange rate or original if conversion fails
func getConvertedPrice(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return ""
}
convertedPrice, err := util.ConvertCurrency(activity.Price, activity.Currency, portfolioBaseCurrency)
// Use historical exchange rate for the transaction date
convertedPrice, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "Conv. Error"
}
return fmt.Sprintf("%.2f %s", convertedPrice, portfolioBaseCurrency)
finalPrice := activity.Price * convertedPrice
// Add indicator if fallback (current) rate was used instead of historical
if !isHistorical {
return fmt.Sprintf("%.2f %s*", finalPrice, portfolioBaseCurrency)
}
return fmt.Sprintf("%.2f %s", finalPrice, portfolioBaseCurrency)
}
// getConvertedTotal returns the converted total or original if conversion fails
// getConvertedTotal returns the converted total using historical exchange rate or original if conversion fails
func getConvertedTotal(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return ""
}
total := activity.Amount * activity.Price
convertedTotal, err := util.ConvertCurrency(total, activity.Currency, portfolioBaseCurrency)
// Use historical exchange rate for the transaction date
convertedTotal, err := util.ConvertCurrencyHistorical(total, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "Conv. Error"
}
@@ -128,15 +145,47 @@ func getConvertedTotal(activity model.Activity, portfolioBaseCurrency string) st
return fmt.Sprintf("%.2f %s", convertedTotal, portfolioBaseCurrency)
}
// formatTotalInvestedWithConversion formats total invested with currency info
// getConvertedTotalWithFallbackInfo returns the converted total with information about rate source
func getConvertedTotalWithFallbackInfo(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return ""
}
total := activity.Amount * activity.Price
// Use historical exchange rate with fallback information
rate, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "Conv. Error"
}
convertedTotal := total * rate
// Add indicator if fallback (current) rate was used instead of historical
if !isHistorical {
return fmt.Sprintf("%.2f %s*", convertedTotal, portfolioBaseCurrency)
}
return fmt.Sprintf("%.2f %s", convertedTotal, portfolioBaseCurrency)
}
// formatTotalInvestedWithConversion formats total invested with currency info and conversion indicators
func formatTotalInvestedWithConversion(activities []model.Activity, baseCurrency string) string {
convertedTotal := calculateTotalInvestedInBaseCurrency(activities, baseCurrency)
// Check if any activities have different currencies
hasDifferentCurrencies := false
hasHistoricalConversions := false
for _, activity := range activities {
if activity.Type == model.Buy && activity.Currency != baseCurrency {
hasDifferentCurrencies = true
// Check if we can get historical rate for this transaction
_, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, baseCurrency, activity.Date)
if err == nil && isHistorical {
hasHistoricalConversions = true
}
break
}
}
@@ -145,5 +194,120 @@ func formatTotalInvestedWithConversion(activities []model.Activity, baseCurrency
return fmt.Sprintf("%.2f %s", convertedTotal, baseCurrency)
}
if hasHistoricalConversions {
return fmt.Sprintf("%.2f %s (historical rates)", convertedTotal, baseCurrency)
}
return fmt.Sprintf("%.2f %s (converted)", convertedTotal, baseCurrency)
}
// calculateTotalInvestedByDate calculates total invested up to a specific date
func calculateTotalInvestedByDate(activities []model.Activity, baseCurrency string, endDate time.Time) float64 {
var total float64 = 0
for _, activity := range activities {
if activity.Type == model.Buy && activity.Date.Before(endDate.AddDate(0, 0, 1)) { // Include transactions on endDate
activityTotal := activity.Amount * activity.Price
if activity.Currency == baseCurrency {
total += activityTotal
} else {
// Use historical exchange rate for the transaction date
convertedTotal, err := util.ConvertCurrencyHistorical(activityTotal, activity.Currency, baseCurrency, activity.Date)
if err != nil {
// If historical conversion fails, try current rate as fallback
fallbackTotal, fallbackErr := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
if fallbackErr != nil {
// If all conversions fail, add original amount (last resort)
total += activityTotal
} else {
total += fallbackTotal
}
} else {
total += convertedTotal
}
}
}
}
return total
}
// getConversionInfo returns information about currency conversions used in the portfolio
func getConversionInfo(activities []model.Activity, baseCurrency string) map[string]interface{} {
info := map[string]interface{}{
"hasDifferentCurrencies": false,
"historicalConversions": 0,
"fallbackConversions": 0,
"errorConversions": 0,
"currencies": make(map[string]int),
}
currencies := make(map[string]int)
historicalCount := 0
fallbackCount := 0
errorCount := 0
for _, activity := range activities {
if activity.Type == model.Buy {
currencies[activity.Currency]++
if activity.Currency != baseCurrency {
info["hasDifferentCurrencies"] = true
// Check conversion status
_, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, baseCurrency, activity.Date)
if err != nil {
errorCount++
} else if isHistorical {
historicalCount++
} else {
fallbackCount++
}
}
}
}
info["historicalConversions"] = historicalCount
info["fallbackConversions"] = fallbackCount
info["errorConversions"] = errorCount
info["currencies"] = currencies
return info
}
// formatCurrencyWithConversionNote formats currency amount with appropriate conversion notes
func formatCurrencyWithConversionNote(amount float64, fromCurrency, toCurrency string, transactionDate time.Time) string {
if fromCurrency == toCurrency {
return util.FormatCurrencyWithSymbol(amount, toCurrency)
}
rate, isHistorical, err := util.GetExchangeRateWithFallback(fromCurrency, toCurrency, transactionDate)
if err != nil {
return fmt.Sprintf("%.2f %s (conv. error)", amount, fromCurrency)
}
convertedAmount := amount * rate
if isHistorical {
return fmt.Sprintf("%.2f %s (historical rate)", convertedAmount, toCurrency)
}
return fmt.Sprintf("%.2f %s (current rate)", convertedAmount, toCurrency)
}
// getActivityConversionStatus returns the conversion status for a specific activity
func getActivityConversionStatus(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return "same_currency"
}
_, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "error"
}
if isHistorical {
return "historical"
}
return "fallback"
}