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 }