github.com/diadata-org/diadata@v1.4.593/pkg/model/rates.go (about)

     1  package models
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/diadata-org/diadata/internal/pkg/rateDerivatives"
     8  	"math"
     9  	"sort"
    10  	"strconv"
    11  	"time"
    12  
    13  	"github.com/diadata-org/diadata/pkg/utils"
    14  	"github.com/go-redis/redis"
    15  )
    16  
    17  const (
    18  	keyAllRates = "all_rates"
    19  	// TimeLayoutRedis = "2006-01-02 15:04:05 +0000 UTC"
    20  )
    21  
    22  type InterestRate struct {
    23  	Symbol          string    `json:"Symbol"`
    24  	Value           float64   `json:"Value"`
    25  	PublicationTime time.Time `json:"PublicationTime"`
    26  	EffectiveDate   time.Time `json:"EffectiveDate"`
    27  	Source          string    `json:"Source"`
    28  }
    29  
    30  // MarshalBinary for interest rates
    31  func (e *InterestRate) MarshalBinary() ([]byte, error) {
    32  	return json.Marshal(e)
    33  }
    34  
    35  // UnmarshalBinary for interest rates
    36  func (e *InterestRate) UnmarshalBinary(data []byte) error {
    37  	if err := json.Unmarshal(data, &e); err != nil {
    38  		return err
    39  	}
    40  	return nil
    41  }
    42  
    43  type InterestRateMeta struct {
    44  	Symbol    string
    45  	FirstDate time.Time
    46  	Decimals  int
    47  	Issuer    string
    48  }
    49  
    50  // ---------------------------------------------------------------------------------------
    51  // Setter and getter for interest rates
    52  // ---------------------------------------------------------------------------------------
    53  
    54  // getKeyInterestRate returns a string that is used as key for storing an interest
    55  // rate in the Redis database.
    56  // @symbol is the symbol of the interest rate (such as SOFR) set at time @date.
    57  func getKeyInterestRate(symbol string, date time.Time) string {
    58  	return "dia_quotation_" + symbol + "_" + date.String()
    59  }
    60  
    61  // SetInterestRate writes the interest rate struct ir into the Redis database
    62  // and writes rate type into a set of all available rates (if not done yet).
    63  func (datastore *DB) SetInterestRate(ir *InterestRate) error {
    64  
    65  	if datastore.redisClient == nil {
    66  		return nil
    67  	}
    68  	// Prepare interest rate quantities for database
    69  	key := getKeyInterestRate(ir.Symbol, ir.EffectiveDate)
    70  	// Write interest rate quantities into database
    71  	log.Debug("setting", key, ir)
    72  	err := datastore.redisClient.Set(key, ir, TimeOutRedis).Err()
    73  	if err != nil {
    74  		log.Printf("Error: %v on SetInterestRate %v\n", err, ir.Symbol)
    75  	}
    76  
    77  	// Write rate type into set of available rates
    78  	err = datastore.redisClient.SAdd(keyAllRates, ir.Symbol).Err()
    79  	if err != nil {
    80  		log.Printf("Error: %v on writing rate %v into set of available rates\n", err, ir.Symbol)
    81  	}
    82  
    83  	return err
    84  }
    85  
    86  // GetInterestRate returns the interest rate value for the last time stamp before @date.
    87  // If @date is an empty string it returns the rate at the latest time stamp.
    88  // @symbol is the shorthand symbol for the requested interest rate.
    89  // @date is a string in the format yyyy-mm-dd.
    90  func (datastore *DB) GetInterestRate(symbol, date string) (*InterestRate, error) {
    91  
    92  	if date == "" {
    93  		date = time.Now().Format("2006-01-02")
    94  	}
    95  	key, _ := datastore.matchKeyInterestRate(symbol, date)
    96  
    97  	// Run database querie with found key
    98  	ir := &InterestRate{}
    99  	err := datastore.redisClient.Get(key).Scan(ir)
   100  	if err != nil {
   101  		if !errors.Is(err, redis.Nil) {
   102  			log.Errorf("Error: %v on GetInterestRate %v\n", err, symbol)
   103  		}
   104  		return ir, err
   105  	}
   106  	return ir, nil
   107  }
   108  
   109  // GetInterestRateRange returns the interest rate values for a range of timestamps.
   110  // @symbol is the shorthand symbol for the requested interest rate.
   111  // @dateInit and @dateFinal are strings in the format yyyy-mm-dd.
   112  func (datastore *DB) GetInterestRateRange(symbol, dateInit, dateFinal string) ([]*InterestRate, error) {
   113  
   114  	// Collect all possible keys in time range
   115  	keys := []string{}
   116  	auxDate := dateInit
   117  	for dateFinal >= auxDate {
   118  		keys = append(keys, "dia_quotation_"+symbol+"_"+auxDate+" 00:00:00 +0000 UTC")
   119  		auxDate = utils.GetTomorrow(auxDate, "2006-01-02")
   120  	}
   121  	// Retrieve corresponding values from database
   122  	result := datastore.redisClient.MGet(keys...).Val()
   123  	allValues := []*InterestRate{}
   124  	for _, val := range result {
   125  		if val != nil {
   126  			ir := &InterestRate{}
   127  			err := json.Unmarshal([]byte(fmt.Sprint(val)), ir)
   128  			if err != nil {
   129  				log.Error("error parsing json")
   130  				return []*InterestRate{}, err
   131  			}
   132  			allValues = append(allValues, ir)
   133  		}
   134  	}
   135  
   136  	// Sort entries with respect to effective date
   137  	sort.Slice(allValues, func(i, j int) bool {
   138  		return (allValues[i].EffectiveDate).Before(allValues[j].EffectiveDate)
   139  	})
   140  	return allValues, nil
   141  }
   142  
   143  // ---------------------------------------------------------------------------------------
   144  // Getter for rates' metadata
   145  // ---------------------------------------------------------------------------------------
   146  
   147  // GetRates returns a (unique) slice of all rates that have been written into the database
   148  func (datastore *DB) GetRates() []string {
   149  	// log.Info("Fetching set of available rates")
   150  	allRates := datastore.redisClient.SMembers(keyAllRates).Val()
   151  	return allRates
   152  }
   153  
   154  // GetRatesMeta returns a list of all available rate symbols along with their first
   155  // timestamp in the database.
   156  func (datastore *DB) GetRatesMeta() (RatesMeta []InterestRateMeta, err error) {
   157  	allRates := datastore.GetRates()
   158  	for _, symbol := range allRates {
   159  		// Get first publication date
   160  		newdate, err := datastore.GetFirstDate(symbol)
   161  		if err != nil {
   162  			return []InterestRateMeta{}, err
   163  		}
   164  		// Get issuing entity
   165  		issuer, err := datastore.GetIssuer(symbol)
   166  		if err != nil {
   167  			return []InterestRateMeta{}, err
   168  		}
   169  		// Number of decimals
   170  		decimals := 0
   171  		switch symbol {
   172  		case "SONIA":
   173  			decimals = 4
   174  		case "SOFR":
   175  			decimals = 2
   176  		case "SAFR":
   177  			decimals = 8
   178  		case "SOFR30":
   179  			decimals = 5
   180  		case "SOFR90":
   181  			decimals = 5
   182  		case "SOFR180":
   183  			decimals = 5
   184  		case "ESTER":
   185  			decimals = 3
   186  		default:
   187  			decimals = 8
   188  		}
   189  		// Fill meta type
   190  		newEntry := InterestRateMeta{symbol, newdate, decimals, issuer}
   191  		RatesMeta = append(RatesMeta, newEntry)
   192  	}
   193  	return
   194  }
   195  
   196  // GetIssuer returns the issuing entity of the rate given by @symbol
   197  func (datastore *DB) GetIssuer(symbol string) (string, error) {
   198  	newdate, err := datastore.GetFirstDate(symbol)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  	key := getKeyInterestRate(symbol, newdate)
   203  	ir := &InterestRate{}
   204  	err = datastore.redisClient.Get(key).Scan(ir)
   205  	if err != nil {
   206  		return "", err
   207  	}
   208  	return ir.Source, nil
   209  }
   210  
   211  // GetFirstDate returns the oldest date written in the database for the rate with symbol @symbol
   212  func (datastore *DB) GetFirstDate(symbol string) (time.Time, error) {
   213  	allSyms := datastore.GetRates()
   214  	if !(utils.Contains(&allSyms, symbol)) {
   215  		log.Errorf("The symbol %v does not exist in the database.", symbol)
   216  		return time.Time{}, errors.New("database error")
   217  	}
   218  	// Fetch all available keys for @symbol
   219  	patt := "dia_quotation_" + symbol + "_*"
   220  	// Comment: This could be improved. Should be when the database gets larger.
   221  	allKeys := datastore.redisClient.Keys(patt).Val()
   222  	oldestKey, _ := utils.MinString(allKeys)
   223  
   224  	// Scan the struct corresponding to the oldest timestamp and fetch effective date.
   225  	ir := &InterestRate{}
   226  	err := datastore.redisClient.Get(oldestKey).Scan(ir)
   227  	if err != nil {
   228  		return time.Time{}, err
   229  	}
   230  	return ir.EffectiveDate, nil
   231  }
   232  
   233  // ---------------------------------------------------------------------------------------
   234  // Risk-free rates methods
   235  // ---------------------------------------------------------------------------------------
   236  
   237  // GetCompoundedRate returns the compounded rate for the period @dateInit to @date. It computes the rate for all
   238  // days for which an entry is present in the database. All other days are assumed to be holidays (or weekends).
   239  func (datastore *DB) GetCompoundedRate(symbol string, dateInit, date time.Time, daysPerYear int, rounding int) (*InterestRate, error) {
   240  
   241  	// Get first publication date for the rate with @symbol in order to check feasibility of dateInit
   242  	firstPublication, err := datastore.GetFirstDate(symbol)
   243  	if err != nil {
   244  		return &InterestRate{}, err
   245  	}
   246  	if utils.AfterDay(firstPublication, dateInit) {
   247  		log.Error("dateInit cannot be earlier than first publication date.")
   248  		err = errors.New("dateInit cannot be earlier than first publication date")
   249  		return &InterestRate{}, err
   250  	}
   251  
   252  	ratesAPI, err := datastore.GetInterestRateRange(symbol, dateInit.Format("2006-01-02"), date.Format("2006-01-02"))
   253  	if err != nil {
   254  		return &InterestRate{}, err
   255  	}
   256  	if len(ratesAPI) == 0 {
   257  		err = errors.New("no rate information for this period")
   258  		return &InterestRate{}, err
   259  	}
   260  
   261  	// Determine holidays through missing database entries
   262  	existDates := []time.Time{}
   263  	for _, entry := range ratesAPI {
   264  		existDates = append(existDates, (*entry).EffectiveDate)
   265  	}
   266  	holidays := utils.GetHolidays(existDates, dateInit, date)
   267  
   268  	// Sort ratesApi (type []*InterestRates) in increasing order according to date
   269  	// and remove the data for the final date, as only past values are compounded.
   270  	sort.Slice(ratesAPI, func(i, j int) bool {
   271  		return (ratesAPI[i].EffectiveDate).Before(ratesAPI[j].EffectiveDate)
   272  	})
   273  	ratesAPI = ratesAPI[:len(ratesAPI)-1]
   274  
   275  	// Extract rates' values
   276  	rates := []float64{}
   277  	for i := range ratesAPI {
   278  		rates = append(rates, ratesAPI[i].Value)
   279  	}
   280  
   281  	// Check, whether first day is a holiday or weekend. If so, prepend rate of
   282  	// preceding business day (outside the considered time range!).
   283  	if utils.ContainsDay(holidays, dateInit) || !utils.CheckWeekDay(dateInit) {
   284  		var firstRate *InterestRate
   285  		firstRate, err = datastore.GetInterestRate(symbol, dateInit.Format("2006-01-02"))
   286  		if err != nil {
   287  			return &InterestRate{}, err
   288  		}
   289  		rates = append([]float64{firstRate.Value}, rates...)
   290  	}
   291  
   292  	// Get compounded rate
   293  	compRate, err := ratederivatives.CompoundedRate(rates, dateInit, date, holidays, daysPerYear, rounding)
   294  	if err != nil {
   295  		return &InterestRate{}, err
   296  	}
   297  
   298  	// Fill InterestRate type for return
   299  	ir := &InterestRate{}
   300  	ir.Symbol = symbol + "_compounded_by_DIA"
   301  	ir.Value = compRate
   302  	ir.EffectiveDate = date
   303  	ir.Source = ratesAPI[0].Source
   304  
   305  	return ir, nil
   306  }
   307  
   308  // GetCompoundedIndex returns the compounded index over the maximal period of existence of @symbol
   309  func (datastore *DB) GetCompoundedIndex(symbol string, date time.Time, daysPerYear int, rounding int) (*InterestRate, error) {
   310  	// Get initial date for the rate with @symbol
   311  	dateInit, err := datastore.GetFirstDate(symbol)
   312  	if err != nil {
   313  		return &InterestRate{}, err
   314  	}
   315  	return datastore.GetCompoundedRate(symbol, dateInit, date, daysPerYear, rounding)
   316  }
   317  
   318  // GetCompoundedIndexRange returns the compounded average of the index @symbol over rolling @calDays calendar days.
   319  func (datastore *DB) GetCompoundedIndexRange(symbol string, dateInit, dateFinal time.Time, daysPerYear int, rounding int) (values []*InterestRate, err error) {
   320  
   321  	// Get first publication date for the rate with @symbol in order to check feasibility of dateInit
   322  	firstPublication, err := datastore.GetFirstDate(symbol)
   323  	if err != nil {
   324  		return []*InterestRate{}, err
   325  	}
   326  	if utils.AfterDay(firstPublication, dateInit) {
   327  		log.Error("dateStart cannot be earlier than first publication date.")
   328  		err = errors.New("dateStart cannot be earlier than first publication date")
   329  		return []*InterestRate{}, err
   330  	}
   331  
   332  	// Get rate data from database for the computation of the compounded values
   333  	ratesAPI, err := datastore.GetInterestRateRange(symbol, dateInit.Format("2006-01-02"), dateFinal.Format("2006-01-02"))
   334  	if err != nil {
   335  		return []*InterestRate{}, err
   336  	}
   337  	if len(ratesAPI) == 0 {
   338  		err = errors.New("no rate information for this period")
   339  		return []*InterestRate{}, err
   340  	}
   341  	// Sort ratesApi (type []*InterestRates) in increasing order according to date.
   342  	sort.Slice(ratesAPI, func(i, j int) bool {
   343  		return (ratesAPI[i].EffectiveDate).Before(ratesAPI[j].EffectiveDate)
   344  	})
   345  
   346  	// Determine holidays through missing database entries
   347  	existDates := []time.Time{}
   348  	for _, entry := range ratesAPI {
   349  		existDates = append(existDates, (*entry).EffectiveDate)
   350  	}
   351  	holidays := utils.GetHolidays(existDates, firstPublication, dateFinal)
   352  
   353  	// Consider previous business day if @dateFinal is holiday or weekend
   354  	for utils.ContainsDay(holidays, dateFinal) || !utils.CheckWeekDay(dateFinal) {
   355  		dateFinal = dateFinal.AddDate(0, 0, -1)
   356  	}
   357  	// Consider next business day if @dateInit is holiday or weekend
   358  	for utils.ContainsDay(holidays, dateInit) || !utils.CheckWeekDay(dateInit) {
   359  		dateInit = dateInit.AddDate(0, 0, 1)
   360  	}
   361  
   362  	// Initialize return values
   363  	compRate, err := datastore.GetCompoundedRate(symbol, firstPublication, dateInit, daysPerYear, 0)
   364  	if err != nil {
   365  		return
   366  	}
   367  	value := compRate.Value
   368  	values = append(values, compRate)
   369  
   370  	// Iterate through all remaining business days
   371  	for i := 0; i < len(ratesAPI)-1; i++ {
   372  
   373  		n, _ := ratederivatives.RateFactor(ratesAPI[i].EffectiveDate, holidays)
   374  		factor := 1 + (ratesAPI[i].Value/100)*float64(n)/float64(daysPerYear)
   375  		value *= factor
   376  
   377  		// Fill return struct
   378  		compAvg := &InterestRate{}
   379  		compAvg.Symbol = symbol + "_compounded_by_DIA"
   380  		compAvg.Value = value
   381  		compAvg.EffectiveDate = ratesAPI[i+1].EffectiveDate
   382  		compAvg.Source = ratesAPI[0].Source
   383  
   384  		// Append data and increment initial date
   385  		values = append(values, compAvg)
   386  
   387  	}
   388  	if rounding != 0 {
   389  		for i := range values {
   390  			values[i].Value = math.Round(values[i].Value*math.Pow(10, float64(rounding))) / math.Pow(10, float64(rounding))
   391  		}
   392  		return values, nil
   393  	}
   394  	return values, nil
   395  }
   396  
   397  // GetCompoundedAvg returns the compounded average of the index @symbol over rolling @calDays calendar days.
   398  func (datastore *DB) GetCompoundedAvg(symbol string, date time.Time, calDays, daysPerYear int, rounding int) (*InterestRate, error) {
   399  
   400  	dateInit := date.AddDate(0, 0, -calDays)
   401  
   402  	index, err := datastore.GetCompoundedRate(symbol, dateInit, date, daysPerYear, rounding)
   403  	if err != nil {
   404  		return &InterestRate{}, err
   405  	}
   406  
   407  	// Fill return struct
   408  	compAvg := &InterestRate{}
   409  	compAvg.Symbol = symbol + strconv.Itoa(calDays) + "_compounded_by_DIA"
   410  	compAvg.Value = 100 * (index.Value - 1) * float64(daysPerYear) / float64(calDays)
   411  	compAvg.EffectiveDate = date
   412  	compAvg.Source = index.Source
   413  
   414  	return compAvg, nil
   415  }
   416  
   417  // --------------------------------------------------------------------------------------------
   418  // Computation of compounded average range as done by FED and BOE, i.e. neglecting higher order
   419  // terms accounting for holidays and weekends
   420  // --------------------------------------------------------------------------------------------
   421  
   422  // WeightedRates returns a map which maps a rate to each business day in the time period given by
   423  // @dateInit and @dateFinal.
   424  // Rates are weighted by the rate factor. intRates must be sorted by date in increasing order.
   425  func WeightedRates(intRates []*InterestRate, dateInit, dateFinal time.Time, holidays []time.Time, startIndex int) (map[time.Time]float64, int) {
   426  
   427  	rateMap := make(map[time.Time]float64)
   428  
   429  	// Adjust rate if dateInit is not a business day
   430  	initialFactor := 0
   431  	auxDate := dateInit
   432  	for !utils.CheckWeekDay(auxDate) || utils.ContainsDay(holidays, auxDate) {
   433  		initialFactor++
   434  		auxDate = auxDate.AddDate(0, 0, 1)
   435  	}
   436  
   437  	// Get index for first date inside global range
   438  	for utils.AfterDay(dateInit, intRates[startIndex].EffectiveDate) {
   439  		startIndex++
   440  	}
   441  	// Return index as cursor inside of entire range requested in API call
   442  	index := startIndex
   443  
   444  	// If first dateInit is non-business day, get previous rate
   445  	if !utils.CheckWeekDay(dateInit) || utils.ContainsDay(holidays, dateInit) {
   446  		startIndex = startIndex - 1
   447  		rateMap[dateInit] = float64(initialFactor) * intRates[startIndex].Value
   448  		startIndex++
   449  	}
   450  
   451  	// Compute compounded rate for period [dateInit, dateFinal]
   452  	for utils.AfterDay(dateFinal, intRates[startIndex].EffectiveDate) && startIndex < len(intRates)-1 {
   453  		ratefactor, _ := ratederivatives.RateFactor(intRates[startIndex].EffectiveDate, holidays)
   454  		rateMap[intRates[startIndex].EffectiveDate] = float64(ratefactor) * intRates[startIndex].Value
   455  		startIndex++
   456  	}
   457  
   458  	return rateMap, index
   459  }
   460  
   461  // GetCompoundedAvgRange returns the compounded average of the index @symbol over rolling @calDays calendar days.
   462  func (datastore *DB) GetCompoundedAvgRange(symbol string, dateInit, dateFinal time.Time, calDays, daysPerYear int, rounding int) (values []*InterestRate, err error) {
   463  
   464  	dateStart := dateInit.AddDate(0, 0, -calDays)
   465  
   466  	// Get first publication date for the rate with @symbol in order to check feasibility of dateInit
   467  	firstPublication, err := datastore.GetFirstDate(symbol)
   468  	if err != nil {
   469  		return []*InterestRate{}, err
   470  	}
   471  	if utils.AfterDay(firstPublication, dateStart) {
   472  		log.Error("dateStart cannot be earlier than first publication date.")
   473  		err = errors.New("dateStart cannot be earlier than first publication date")
   474  		return []*InterestRate{}, err
   475  	}
   476  
   477  	// Get rate data from database
   478  	ratesAPI, err := datastore.GetInterestRateRange(symbol, dateStart.Format("2006-01-02"), dateFinal.Format("2006-01-02"))
   479  	if err != nil {
   480  		return []*InterestRate{}, err
   481  	}
   482  	if len(ratesAPI) == 0 {
   483  		err = errors.New("no rate information for this period")
   484  		return []*InterestRate{}, err
   485  	}
   486  
   487  	// Check, whether first day is a holiday or weekend. If so, prepend rate of
   488  	// preceding business day (outside the considered time range!).
   489  	// Determine holidays through missing database entries
   490  	existDates := []time.Time{}
   491  	for _, entry := range ratesAPI {
   492  		existDates = append(existDates, (*entry).EffectiveDate)
   493  	}
   494  	holidays := utils.GetHolidays(existDates, dateStart, dateFinal)
   495  	if utils.ContainsDay(holidays, dateStart) || !utils.CheckWeekDay(dateStart) {
   496  		firstRate, err := datastore.GetInterestRate(symbol, dateStart.Format("2006-01-02"))
   497  		if err != nil {
   498  			return []*InterestRate{}, err
   499  		}
   500  		ratesAPI = append([]*InterestRate{firstRate}, ratesAPI...)
   501  	}
   502  
   503  	// Consider last business day if last given day is holiday or weekend
   504  	for utils.ContainsDay(holidays, dateFinal) || !utils.CheckWeekDay(dateFinal) {
   505  		dateFinal = dateFinal.AddDate(0, 0, -1)
   506  	}
   507  
   508  	// Sort ratesApi (type []*InterestRates) in increasing order according to date
   509  	// and remove the data for the final date, as only past values are compounded.
   510  	sort.Slice(ratesAPI, func(i, j int) bool {
   511  		return (ratesAPI[i].EffectiveDate).Before(ratesAPI[j].EffectiveDate)
   512  	})
   513  	ratesAPI = ratesAPI[:len(ratesAPI)-1]
   514  
   515  	// Iterate through interest periods of length @calDays
   516  	cursor := 0
   517  	for utils.AfterDay(dateFinal, dateInit) {
   518  
   519  		// Get starting date of compounding period
   520  		dateStart := dateInit.AddDate(0, 0, -calDays)
   521  
   522  		if utils.ContainsDay(holidays, dateInit) || !utils.CheckWeekDay(dateInit) {
   523  			// No rate information on holidays and weekends
   524  			dateInit = dateInit.AddDate(0, 0, 1)
   525  		} else {
   526  
   527  			// get a weighted rate for each business day in period of interest
   528  			mapRates, index := WeightedRates(ratesAPI, dateStart, dateInit, holidays, cursor)
   529  			cursor = index
   530  
   531  			auxDate := dateStart
   532  			ratesPeriod := []float64{}
   533  			for utils.AfterDay(dateInit, auxDate) {
   534  				val, ok := mapRates[auxDate]
   535  				if ok {
   536  					ratesPeriod = append(ratesPeriod, val)
   537  					auxDate = auxDate.AddDate(0, 0, 1)
   538  				} else {
   539  					auxDate = auxDate.AddDate(0, 0, 1)
   540  				}
   541  			}
   542  
   543  			compRate, err := ratederivatives.CompoundedRateSimple(ratesPeriod, dateStart, dateInit, daysPerYear, 0)
   544  
   545  			if err != nil || utils.ContainsDay(holidays, dateInit) {
   546  				dateInit = dateInit.AddDate(0, 0, 1)
   547  			} else {
   548  
   549  				// Fill return struct
   550  				compAvg := &InterestRate{}
   551  				compAvg.Symbol = symbol + strconv.Itoa(calDays) + "_compounded_by_DIA"
   552  				compAvg.Value = 100 * (compRate - 1) * float64(daysPerYear) / float64(calDays)
   553  				compAvg.EffectiveDate = dateInit
   554  				compAvg.Source = ratesAPI[0].Source
   555  
   556  				// Append data and increment initial date
   557  				values = append(values, compAvg)
   558  				dateInit = dateInit.AddDate(0, 0, 1)
   559  			}
   560  		}
   561  
   562  	}
   563  	if rounding != 0 {
   564  		for i := range values {
   565  			values[i].Value = math.Round(values[i].Value*math.Pow(10, float64(rounding))) / math.Pow(10, float64(rounding))
   566  		}
   567  		return values, nil
   568  	}
   569  	return values, nil
   570  }
   571  
   572  // ---------------------------------------------------------------------------------------------
   573  // Computation of compounded averages in conservative way, i.e. including higher order terms
   574  // ---------------------------------------------------------------------------------------------
   575  
   576  // StraightRates returns a map which maps a rate to each day in the time period. This includes (artificial)
   577  // rate values for non-business days. intRates must be sorted by date in increasing order.
   578  func StraightRates(intRates []*InterestRate) map[time.Time]float64 {
   579  
   580  	finalDay := intRates[len(intRates)-1].EffectiveDate
   581  	count := 0
   582  	day := intRates[count].EffectiveDate
   583  
   584  	rateMap := make(map[time.Time]float64)
   585  	rateMap[day] = intRates[count].Value
   586  	day = day.AddDate(0, 0, 1)
   587  	count++
   588  	for utils.AfterDay(finalDay, day) {
   589  		if utils.SameDays(day, intRates[count].EffectiveDate) {
   590  			// For business day assign rate and increment day
   591  			rateMap[day] = intRates[count].Value
   592  			day = day.AddDate(0, 0, 1)
   593  			count++
   594  		} else {
   595  			// holiday or weekend gets the previous rate
   596  			rateMap[day] = intRates[count-1].Value
   597  			day = day.AddDate(0, 0, 1)
   598  		}
   599  	}
   600  	return rateMap
   601  }
   602  
   603  // GetCompoundedAvgDIARange returns the compounded average DIA index of @symbol over rolling @calDays calendar days.
   604  func (datastore *DB) GetCompoundedAvgDIARange(symbol string, dateInit, dateFinal time.Time, calDays, daysPerYear int, rounding int) (values []*InterestRate, err error) {
   605  
   606  	dateStart := dateInit.AddDate(0, 0, -calDays)
   607  
   608  	// Get first publication date for the rate with @symbol in order to check feasibility of dateInit
   609  	firstPublication, err := datastore.GetFirstDate(symbol)
   610  	if err != nil {
   611  		return []*InterestRate{}, err
   612  	}
   613  	if utils.AfterDay(firstPublication, dateStart) {
   614  		log.Error("dateStart cannot be earlier than first publication date.")
   615  		err = errors.New("dateStart cannot be earlier than first publication date")
   616  		return []*InterestRate{}, err
   617  	}
   618  
   619  	// Get rate data from database
   620  	ratesAPI, err := datastore.GetInterestRateRange(symbol, dateStart.Format("2006-01-02"), dateFinal.Format("2006-01-02"))
   621  	if err != nil {
   622  		return []*InterestRate{}, err
   623  	}
   624  	if len(ratesAPI) == 0 {
   625  		err = errors.New("no rate information for this period")
   626  		return []*InterestRate{}, err
   627  	}
   628  
   629  	// Check, whether first day is a holiday or weekend. If so, prepend rate of
   630  	// preceding business day (outside the considered time range!).
   631  	// Determine holidays through missing database entries
   632  	existDates := []time.Time{}
   633  	for _, entry := range ratesAPI {
   634  		existDates = append(existDates, (*entry).EffectiveDate)
   635  	}
   636  	holidays := utils.GetHolidays(existDates, dateStart, dateFinal)
   637  	if utils.ContainsDay(holidays, dateStart) || !utils.CheckWeekDay(dateStart) {
   638  		firstRate, err := datastore.GetInterestRate(symbol, dateStart.Format("2006-01-02"))
   639  		if err != nil {
   640  			return []*InterestRate{}, err
   641  		}
   642  		ratesAPI = append([]*InterestRate{firstRate}, ratesAPI...)
   643  	}
   644  
   645  	// Consider last business day if last given day is holiday or weekend
   646  	for utils.ContainsDay(holidays, dateFinal) || !utils.CheckWeekDay(dateFinal) {
   647  		dateFinal = dateFinal.AddDate(0, 0, -1)
   648  	}
   649  
   650  	// Sort ratesApi (type []*InterestRates) in increasing order according to date
   651  	// and remove the data for the final date, as only past values are compounded.
   652  	sort.Slice(ratesAPI, func(i, j int) bool {
   653  		return (ratesAPI[i].EffectiveDate).Before(ratesAPI[j].EffectiveDate)
   654  	})
   655  	ratesAPI = ratesAPI[:len(ratesAPI)-1]
   656  
   657  	// get a rate for each calendar day in period of interest
   658  	mapRates := StraightRates(ratesAPI)
   659  
   660  	// Iterate through interest period
   661  	count := 0
   662  	for utils.AfterDay(dateFinal, dateInit) {
   663  		// Get compounded rate
   664  		dateStart := dateInit.AddDate(0, 0, -calDays)
   665  		auxDate := dateStart
   666  		ratesPeriod := []float64{}
   667  		for i := count; i < count+calDays; i++ {
   668  			ratesPeriod = append(ratesPeriod, mapRates[auxDate])
   669  			auxDate = auxDate.AddDate(0, 0, 1)
   670  		}
   671  		compRate, err := ratederivatives.CompoundedRateSimple(ratesPeriod, dateStart, dateInit, daysPerYear, 0)
   672  
   673  		if err != nil {
   674  			dateInit = dateInit.AddDate(0, 0, 1)
   675  		} else {
   676  
   677  			// Fill return struct
   678  			compAvg := &InterestRate{}
   679  			compAvg.Symbol = symbol + strconv.Itoa(calDays) + "_compounded_by_DIA"
   680  			compAvg.Value = 100 * (compRate - 1) * float64(daysPerYear) / float64(calDays)
   681  			compAvg.EffectiveDate = dateInit
   682  			compAvg.Source = ratesAPI[0].Source
   683  
   684  			// Append data and increment initial date
   685  			values = append(values, compAvg)
   686  			dateInit = dateInit.AddDate(0, 0, 1)
   687  			count++
   688  		}
   689  
   690  	}
   691  	if rounding != 0 {
   692  		for i := range values {
   693  			values[i].Value = math.Round(values[i].Value*math.Pow(10, float64(rounding))) / math.Pow(10, float64(rounding))
   694  		}
   695  		return values, nil
   696  	}
   697  	return values, nil
   698  }
   699  
   700  // ---------------------------------------------------------------------------------------
   701  // Auxiliary functions
   702  // ---------------------------------------------------------------------------------------
   703  
   704  // ExistInterestRate returns true if a database entry with given date stamp exists,
   705  // and false otherwise.
   706  // @date should be a substring of a string formatted as "yyyy-mm-dd hh:mm:ss".
   707  func (datastore *DB) ExistInterestRate(symbol, date string) bool {
   708  	pattern := "*" + symbol + "_" + date + "*"
   709  	strSlice := datastore.redisClient.Keys(pattern).Val()
   710  	return len(strSlice) != 0
   711  }
   712  
   713  // matchKeyInterestRate returns the key in the database db with the youngest timestamp
   714  // younger than the date @date, given as substring of a string formatted as "yyyy-mm-dd hh:mm:ss".
   715  func (datastore *DB) matchKeyInterestRate(symbol, date string) (string, error) {
   716  	exDate, err := datastore.findLastDay(symbol, date)
   717  	if err != nil {
   718  		return "", err
   719  	}
   720  	// Determine all database entries with given date
   721  	pattern := "*" + symbol + "_" + exDate + "*"
   722  	strSlice := datastore.redisClient.Keys(pattern).Val()
   723  
   724  	var strSliceFormatted []string
   725  	layout := "2006-01-02 15:04:05"
   726  	for _, key := range strSlice {
   727  		date, _ := time.Parse(layout, key)
   728  		strSliceFormatted = append(strSliceFormatted, date.String())
   729  	}
   730  	_, index := utils.MaxString(strSliceFormatted)
   731  	return strSlice[index], nil
   732  }
   733  
   734  // findLastDay returns the youngest date before @date that has an entry in the database.
   735  // @date should be a substring of a string formatted as "yyyy-mm-dd hh:mm:ss"
   736  func (datastore *DB) findLastDay(symbol, date string) (string, error) {
   737  	maxDays := 30 // Remark: This could be a function parameter as well...
   738  	for count := 0; count < maxDays; count++ {
   739  		if datastore.ExistInterestRate(symbol, date) {
   740  			return date, nil
   741  		}
   742  		// If date has no entry, look for one the day before
   743  		date = utils.GetYesterday(date, "2006-01-02")
   744  	}
   745  
   746  	// If no entry found in the last @maxDays days return error
   747  	err := errors.New("No database entry found in the last " + strconv.FormatInt(int64(maxDays), 10) + "days.")
   748  	return "", err
   749  }