Skip to content

Commit

Permalink
add International Monetary Fund exchange rates data source
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Aug 27, 2024
1 parent 62d3dc6 commit ab745ad
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 6 deletions.
1 change: 1 addition & 0 deletions conf/ezbookkeeping.ini
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ custom_map_tile_server_default_zoom_level = 14
# "reserve_bank_of_australia": https://www.rba.gov.au/statistics/frequency/exchange-rates.html
# "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/
# "national_bank_of_poland": https://nbp.pl/en/statistic-and-financial-reporting/rates/
# "international_monetary_fund": https://www.imf.org/external/np/fin/data/param_rms_mth.aspx
data_source = euro_central_bank

# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
Expand Down
3 changes: 3 additions & 0 deletions pkg/exchangerates/exchange_rates_datasource_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
Container.Current = &NationalBankOfPolandDataSource{}
return nil
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
Container.Current = &InternationalMonetaryFundDataSource{}
return nil
}

return errs.ErrInvalidExchangeRatesDataSource
Expand Down
231 changes: 231 additions & 0 deletions pkg/exchangerates/international_monetary_fund_datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package exchangerates

import (
"strings"
"time"

orderedmap "github.com/wk8/go-ordered-map/v2"

"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)

const internationalMonetaryFundExchangeRateUrl = "https://www.imf.org/external/np/fin/data/rms_five.aspx?tsvflag=Y"
const internationalMonetaryFundExchangeRateReferenceUrl = "https://www.imf.org/external/np/fin/data/param_rms_mth.aspx"
const internationalMonetaryFundDataSource = "International Monetary Fund"
const internationalMonetaryFundBaseCurrency = "USD"

const internationalMonetaryFundDataUpdateDateFormat = "January 02, 2006 15:04"
const internationalMonetaryFundDataUpdateDateTimezone = "America/New_York"

var internationalMonetaryFundCurrencyNameCodeMap map[string]string

// InternationalMonetaryFundDataSource defines the structure of exchange rates data source of international monetary fund
type InternationalMonetaryFundDataSource struct {
ExchangeRatesDataSource
}

func init() {
internationalMonetaryFundCurrencyNameCodeMap = make(map[string]string, 38)
internationalMonetaryFundCurrencyNameCodeMap["Chinese yuan"] = "CNY"
internationalMonetaryFundCurrencyNameCodeMap["Euro"] = "EUR"
internationalMonetaryFundCurrencyNameCodeMap["Japanese yen"] = "JPY"
internationalMonetaryFundCurrencyNameCodeMap["U.K. pound"] = "GBP"
internationalMonetaryFundCurrencyNameCodeMap["U.S. dollar"] = "USD"
internationalMonetaryFundCurrencyNameCodeMap["Algerian dinar"] = "DZD"
internationalMonetaryFundCurrencyNameCodeMap["Australian dollar"] = "AUD"
internationalMonetaryFundCurrencyNameCodeMap["Botswana pula"] = "BWP"
internationalMonetaryFundCurrencyNameCodeMap["Brazilian real"] = "BRL"
internationalMonetaryFundCurrencyNameCodeMap["Brunei dollar"] = "BND"
internationalMonetaryFundCurrencyNameCodeMap["Canadian dollar"] = "CAD"
internationalMonetaryFundCurrencyNameCodeMap["Chilean peso"] = "CLP"
internationalMonetaryFundCurrencyNameCodeMap["Czech koruna"] = "CZK"
internationalMonetaryFundCurrencyNameCodeMap["Danish krone"] = "DKK"
internationalMonetaryFundCurrencyNameCodeMap["Indian rupee"] = "INR"
internationalMonetaryFundCurrencyNameCodeMap["Israeli New Shekel"] = "ILS"
internationalMonetaryFundCurrencyNameCodeMap["Korean won"] = "KRW"
internationalMonetaryFundCurrencyNameCodeMap["Kuwaiti dinar"] = "KWD"
internationalMonetaryFundCurrencyNameCodeMap["Malaysian ringgit"] = "MYR"
internationalMonetaryFundCurrencyNameCodeMap["Mauritian rupee"] = "MUR"
internationalMonetaryFundCurrencyNameCodeMap["Mexican peso"] = "MXN"
internationalMonetaryFundCurrencyNameCodeMap["New Zealand dollar"] = "NZD"
internationalMonetaryFundCurrencyNameCodeMap["Norwegian krone"] = "NOK"
internationalMonetaryFundCurrencyNameCodeMap["Omani rial"] = "OMR"
internationalMonetaryFundCurrencyNameCodeMap["Peruvian sol"] = "PEN"
internationalMonetaryFundCurrencyNameCodeMap["Philippine peso"] = "PHP"
internationalMonetaryFundCurrencyNameCodeMap["Polish zloty"] = "PLN"
internationalMonetaryFundCurrencyNameCodeMap["Qatari riyal"] = "QAR"
internationalMonetaryFundCurrencyNameCodeMap["Russian ruble"] = "RUB"
internationalMonetaryFundCurrencyNameCodeMap["Saudi Arabian riyal"] = "SAR"
internationalMonetaryFundCurrencyNameCodeMap["Singapore dollar"] = "SGD"
internationalMonetaryFundCurrencyNameCodeMap["South African rand"] = "ZAR"
internationalMonetaryFundCurrencyNameCodeMap["Swedish krona"] = "SEK"
internationalMonetaryFundCurrencyNameCodeMap["Swiss franc"] = "CHF"
internationalMonetaryFundCurrencyNameCodeMap["Thai baht"] = "THB"
internationalMonetaryFundCurrencyNameCodeMap["Trinidadian dollar"] = "TTD"
internationalMonetaryFundCurrencyNameCodeMap["U.A.E. dirham"] = "AED"
internationalMonetaryFundCurrencyNameCodeMap["Uruguayan peso"] = "UYU"
}

// GetRequestUrls returns the international monetary fund data source urls
func (e *InternationalMonetaryFundDataSource) GetRequestUrls() []string {
return []string{internationalMonetaryFundExchangeRateUrl}
}

// Parse returns the common response entity according to the international monetary fund data source raw response
func (e *InternationalMonetaryFundDataSource) Parse(c core.Context, content []byte) (*models.LatestExchangeRateResponse, error) {
lines := strings.Split(string(content), "\n")

if len(lines) < 1 {
log.Errorf(c, "[international_monetary_fund_datasource.Parse] content is invalid, content is %s", string(content))
return nil, errs.ErrFailedToRequestRemoteApi
}

exchangeRatesToSDR := orderedmap.New[string, float64]()
latestUpdateDate := ""

findSDRsPerCurrencyUnitLine := false
findExchangeRateDataHeader := false

for i := 0; i < len(lines); i++ {
line := lines[i]

if line == "" {
continue
}

line = strings.ReplaceAll(line, "\r", "")

if strings.Index(line, "Currency units per SDR") == 0 {
break
}

if strings.Index(line, "SDRs per Currency unit") == 0 {
findSDRsPerCurrencyUnitLine = true
continue
}

if findExchangeRateDataHeader {
items := strings.Split(line, "\t")

if len(items) != 6 {
continue
}

currencyCode, exchangeRate := e.parseExchangeRate(c, line, items)

if currencyCode != nil && exchangeRate != nil {
exchangeRatesToSDR.Set(*currencyCode, *exchangeRate)
}

continue
}

if findSDRsPerCurrencyUnitLine {
items := strings.Split(line, "\t")

if len(items) != 6 {
continue
}

if items[0] == "Currency" {
findExchangeRateDataHeader = true
latestUpdateDate = items[1]
continue
}
}
}

if latestUpdateDate == "" {
log.Errorf(c, "[international_monetary_fund_datasource.Parse] latest update date is empty")
return nil, errs.ErrFailedToRequestRemoteApi
}

if exchangeRatesToSDR.Len() < 1 {
log.Errorf(c, "[international_monetary_fund_datasource.Parse] exchange rates date is empty")
return nil, errs.ErrFailedToRequestRemoteApi
}

defaultCurrencyExchangeRateToSDR, exists := exchangeRatesToSDR.Get(internationalMonetaryFundBaseCurrency)

if !exists {
log.Errorf(c, "[international_monetary_fund_datasource.Parse] exchange rates date does not have default currency \"%s\"", internationalMonetaryFundBaseCurrency)
return nil, errs.ErrFailedToRequestRemoteApi
}

exchangeRates := make(models.LatestExchangeRateSlice, 0, exchangeRatesToSDR.Len())

for pair := exchangeRatesToSDR.Oldest(); pair != nil; pair = pair.Next() {
exchangeRates = append(exchangeRates, &models.LatestExchangeRate{
Currency: pair.Key,
Rate: utils.Float64ToString(defaultCurrencyExchangeRateToSDR / pair.Value),
})
}

timezone, err := time.LoadLocation(internationalMonetaryFundDataUpdateDateTimezone)

if err != nil {
log.Errorf(c, "[international_monetary_fund_datasource.Parse] failed to get timezone, timezone name is %s", internationalMonetaryFundDataUpdateDateTimezone)
return nil, errs.ErrFailedToRequestRemoteApi
}

updateDateTime := latestUpdateDate + " 11:00" // The IMF posts Representative and SDR exchange rates every 20 minutes from 11:00 AM to 6:00 PM U.S. EST Monday to Friday except for these holidays
updateTime, err := time.ParseInLocation(internationalMonetaryFundDataUpdateDateFormat, updateDateTime, timezone)

if err != nil {
log.Errorf(c, "[international_monetary_fund_datasource.Parse] failed to parse update date, datetime is %s", updateDateTime)
return nil, errs.ErrFailedToRequestRemoteApi
}

latestExchangeRateResp := &models.LatestExchangeRateResponse{
DataSource: internationalMonetaryFundDataSource,
ReferenceUrl: internationalMonetaryFundExchangeRateReferenceUrl,
UpdateTime: updateTime.Unix(),
BaseCurrency: internationalMonetaryFundBaseCurrency,
ExchangeRates: exchangeRates,
}

return latestExchangeRateResp, nil
}

func (e *InternationalMonetaryFundDataSource) parseExchangeRate(c core.Context, line string, lineItems []string) (*string, *float64) {
currencyCode, exists := internationalMonetaryFundCurrencyNameCodeMap[lineItems[0]]

if !exists {
log.Warnf(c, "[international_monetary_fund_datasource.parseExchangeRate] unknown currency name %s, line is %s", lineItems[0], line)
return nil, nil
}

if _, exists := validators.AllCurrencyNames[currencyCode]; !exists {
return nil, nil
}

for i := 1; i < 6; i++ {
item := lineItems[i]

if item == "" {
continue
}

rate, err := utils.StringToFloat64(item)

if err != nil {
log.Warnf(c, "[international_monetary_fund_datasource.parseExchangeRate] failed to parse rate, line is %s", line)
return nil, nil
}

if rate <= 0 {
log.Warnf(c, "[international_monetary_fund_datasource.parseExchangeRate] rate is invalid, line is %s", line)
return nil, nil
}

return &currencyCode, &rate
}

log.Warnf(c, "[international_monetary_fund_datasource.parseExchangeRate] no exchange rate data exists for currency \"%s\", line is %s", currencyCode, line)
return nil, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package exchangerates
15 changes: 9 additions & 6 deletions pkg/settings/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ const (

// Exchange rates data source types
const (
EuroCentralBankDataSource string = "euro_central_bank"
BankOfCanadaDataSource string = "bank_of_canada"
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
EuroCentralBankDataSource string = "euro_central_bank"
BankOfCanadaDataSource string = "bank_of_canada"
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
InternationalMonetaryFundDataSource string = "international_monetary_fund"
)

const (
Expand Down Expand Up @@ -247,7 +248,7 @@ type Config struct {
DuplicateSubmissionsIntervalDuration time.Duration

// Cron
EnableRemoveExpiredTokens bool
EnableRemoveExpiredTokens bool
EnableCreateScheduledTransaction bool

// Secret
Expand Down Expand Up @@ -846,6 +847,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
config.ExchangeRatesDataSource = CzechNationalBankDataSource
} else if dataSource == NationalBankOfPolandDataSource {
config.ExchangeRatesDataSource = NationalBankOfPolandDataSource
} else if dataSource == InternationalMonetaryFundDataSource {
config.ExchangeRatesDataSource = InternationalMonetaryFundDataSource
} else {
return errs.ErrInvalidExchangeRatesDataSource
}
Expand Down

0 comments on commit ab745ad

Please sign in to comment.