github.com/diadata-org/diadata@v1.4.593/pkg/graphql/resolver/root.go (about) 1 package resolver 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/diadata-org/diadata/pkg/dia" 11 queryhelper "github.com/diadata-org/diadata/pkg/dia/helpers/queryHelper" 12 scrapers "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers" 13 "github.com/diadata-org/diadata/pkg/utils" 14 "github.com/ethereum/go-ethereum/common" 15 16 models "github.com/diadata-org/diadata/pkg/model" 17 graphql "github.com/graph-gophers/graphql-go" 18 "github.com/sirupsen/logrus" 19 ) 20 21 var ( 22 log = logrus.New() 23 EXCHANGES = scrapers.Exchanges 24 BLOCKCHAINS = scrapers.Blockchains 25 lookbackTradesNumber = 10 26 errInvalidInputParams = errors.New("invalid input params") 27 statusCodesMap = make(map[string]int32) 28 statusError = "err" 29 EXCHANGESYMBOL_CACHE = make(map[string]dia.Asset) 30 ) 31 32 const ( 33 PAIR_SEPARATOR = "-" 34 LIST_SEPARATOR = "," 35 TRADE_VOLUME_THRESHOLD_DEFAULT = float64(0.001) 36 ) 37 38 func init() { 39 statusCodesMap[""] = int32(0) 40 statusCodesMap[statusError] = int32(1) 41 statusCodesMap["pair not available on"] = int32(2) 42 statusCodesMap["exchange name not valid"] = int32(3) 43 statusCodesMap["not available on"] = int32(7) 44 } 45 46 // Resolver is the root resolver 47 type DiaResolver struct { 48 DS models.DB 49 RelDB models.RelDB 50 InfluxBatchSize int64 51 } 52 53 func (r *DiaResolver) GetSupply(ctx context.Context, args struct{ Symbol graphql.NullString }) (*SupplyResolver, error) { 54 if containsSpecialChars(*args.Symbol.Value) { 55 return nil, errInvalidInputParams 56 } 57 q, err := r.DS.GetLatestSupply(*args.Symbol.Value, &r.RelDB) 58 if err != nil { 59 return nil, err 60 } 61 return &SupplyResolver{q: q}, nil 62 } 63 64 func (r *DiaResolver) GetSupplies(ctx context.Context, args struct{ Symbol graphql.NullString }) (*[]*SupplyResolver, error) { 65 starttime := time.Unix(1, 0) 66 endtime := time.Now() 67 if containsSpecialChars(*args.Symbol.Value) { 68 return nil, errInvalidInputParams 69 } 70 71 q, err := r.DS.GetSupply(*args.Symbol.Value, starttime, endtime, &r.RelDB) 72 if err != nil { 73 return nil, err 74 } 75 76 var sr []*SupplyResolver 77 78 for _, supply := range q { 79 sr = append(sr, &SupplyResolver{q: &supply}) 80 81 } 82 return &sr, nil 83 } 84 85 type TradeBlock struct { 86 Trades []dia.Trade 87 } 88 89 type Asset struct { 90 Address graphql.NullString 91 BlockChain graphql.NullString 92 } 93 94 type FeedSelection struct { 95 Address graphql.NullString 96 Blockchain graphql.NullString 97 LiquidityThreshold graphql.NullFloat 98 Exchangepairs *[]Exchangepair 99 } 100 101 type Exchangepair struct { 102 Exchange graphql.NullString 103 Pairs *[]graphql.NullString 104 LiquidityThreshold graphql.NullFloat 105 } 106 107 func (r *DiaResolver) GetChart(ctx context.Context, args struct { 108 Filter graphql.NullString 109 BlockDurationSeconds graphql.NullInt 110 BlockShiftSeconds graphql.NullInt 111 Symbol graphql.NullString 112 StartTime graphql.NullTime 113 EndTime graphql.NullTime 114 Exchanges *[]graphql.NullString 115 Address graphql.NullString 116 BlockChain graphql.NullString 117 BaseAsset *[]Asset 118 }) (*[]*FilterPointResolver, error) { 119 fpr, _ := r.GetChartMeta(ctx, args) 120 return fpr.fpr, nil 121 } 122 123 // TO DO: Use context? 124 func (r *DiaResolver) GetChartMeta(ctx context.Context, args struct { 125 Filter graphql.NullString 126 BlockDurationSeconds graphql.NullInt 127 BlockShiftSeconds graphql.NullInt 128 Symbol graphql.NullString 129 StartTime graphql.NullTime 130 EndTime graphql.NullTime 131 Exchanges *[]graphql.NullString 132 Address graphql.NullString 133 BlockChain graphql.NullString 134 BaseAsset *[]Asset 135 }) (*FilterPointMetaResolver, error) { 136 137 if containsSpecialChars(*args.Symbol.Value) || containsSpecialChars(*args.Address.Value) || containsSpecialChars(*args.BlockChain.Value) { 138 return nil, errInvalidInputParams 139 } 140 var ( 141 blockShiftSeconds int64 142 tradeBlocks []queryhelper.Block 143 blockchain string 144 address string 145 sr FilterPointMetaResolver 146 asset dia.Asset 147 err error 148 baseAssets []dia.Asset 149 ) 150 151 // Parsing input parameters. 152 filter := args.Filter.Value 153 if containsSpecialChars(*filter) { 154 return nil, errInvalidInputParams 155 } 156 157 blockSizeSeconds := int64(*args.BlockDurationSeconds.Value) 158 if args.BlockShiftSeconds.Value != nil { 159 blockShiftSeconds = int64(*args.BlockShiftSeconds.Value) 160 } 161 if args.Address.Value != nil { 162 if containsSpecialChars(*args.Address.Value) { 163 return nil, errInvalidInputParams 164 } 165 address = *args.Address.Value 166 167 } 168 if args.BlockChain.Value != nil { 169 if containsSpecialChars(*args.BlockChain.Value) { 170 return nil, errInvalidInputParams 171 } 172 blockchain = *args.BlockChain.Value 173 } 174 symbol := *args.Symbol.Value 175 starttime := args.StartTime.Value.Time 176 endtime := args.EndTime.Value.Time 177 starttimeimmutable := args.StartTime.Value.Time 178 endtimeimmutable := args.EndTime.Value.Time 179 180 if containsSpecialChars(symbol) { 181 return nil, errInvalidInputParams 182 } 183 exchanges := args.Exchanges 184 var exchangesString []string 185 if exchanges != nil { 186 for _, v := range *exchanges { 187 if containsSpecialChars(*v.Value) { 188 continue 189 } 190 exchangesString = append(exchangesString, *v.Value) 191 } 192 } 193 194 // Fetch base assets. 195 argsbaseasset := args.BaseAsset 196 if argsbaseasset != nil { 197 for _, baseasset := range *argsbaseasset { 198 if containsSpecialChars(*baseasset.Address.Value) || containsSpecialChars(*baseasset.BlockChain.Value) { 199 continue 200 } 201 asset, err = r.RelDB.GetAsset(*baseasset.Address.Value, *baseasset.BlockChain.Value) 202 if err != nil { 203 log.Errorf("Asset not found with address %s and blockchain %s ", address, blockchain) 204 continue 205 } 206 baseAssets = append(baseAssets, asset) 207 } 208 } 209 210 // Get quote asset either by blockchain&address or by topAsset method. 211 if address != "" && blockchain != "" { 212 asset, err = r.RelDB.GetAsset(address, blockchain) 213 if err != nil { 214 log.Errorf("Asset not found with address %s and blockchain %s ", address, blockchain) 215 return &sr, err 216 } 217 } else { 218 assets, err := r.RelDB.GetTopAssetByVolume(symbol) 219 if err != nil { 220 log.Errorf("Asset not found with symbol %s ", symbol) 221 return &sr, err 222 } 223 224 log.Infoln("All assets having same symbol", assets) 225 asset = assets[0] 226 } 227 228 log.Infoln("Asset Selected", asset) 229 var ( 230 filterPoints, emaFilterPoints []dia.FilterPoint 231 filterMetadata *dia.FilterPointMetadata 232 ) 233 234 if *filter != "ema" { 235 236 if endtime.After(time.Now()) { 237 endtime = time.Now() 238 } 239 240 // Limit the (time-)range of the query to 24 hours. 241 maxStartTime := endtime.Add(-time.Duration(60*24) * time.Minute) 242 if starttime.Before(maxStartTime) { 243 starttime = maxStartTime 244 } 245 246 // Set blockShiftSeconds = blockSizeSeconds per default if not given. 247 if blockShiftSeconds == 0 { 248 blockShiftSeconds = blockSizeSeconds 249 } 250 251 // (Potentially) decrease starttime such that an integer number of bins fits into the whole range. 252 if endtime.Unix()-starttime.Unix() < blockSizeSeconds { 253 // Just one block. 254 starttime = time.Unix(endtime.Unix()-blockSizeSeconds, 0) 255 } else { 256 starttime = time.Unix(starttime.Unix()-(blockShiftSeconds-((endtime.Unix()-starttime.Unix()-blockSizeSeconds)%blockShiftSeconds)), 0) 257 } 258 259 // Make time bins according to block size and block shift parameters. 260 bins := utils.MakeBins(starttime, endtime, blockSizeSeconds, blockShiftSeconds) 261 262 // Fetch trades. 263 var trades []dia.Trade 264 if blockShiftSeconds <= blockSizeSeconds { 265 // Fetch all trades in time range. 266 trades, err = r.DS.GetTradesByExchangesAndBaseAssets(asset, baseAssets, exchangesString, starttime, endtime, 0) 267 if err != nil { 268 log.Error("GetTradesByExchangesAndBaseAssets: ", err) 269 return &sr, err 270 } 271 } else { 272 // Fetch trades batched for disjoint bins. 273 var starttimes, endtimes []time.Time 274 for _, bin := range bins { 275 starttimes = append(starttimes, bin.Starttime) 276 endtimes = append(endtimes, bin.Endtime) 277 } 278 trades, err = r.DS.GetTradesByExchangesBatched(asset, baseAssets, exchangesString, starttimes, endtimes, 0) 279 if err != nil { 280 log.Error("GetTradesByExchangesAndBaseAssets: ", err) 281 return &sr, err 282 } 283 } 284 log.Println("Generating blocks, Total Trades", len(trades)) 285 log.Info("generating bins. Total bins: ", len(bins)) 286 287 if len(trades) > 0 && len(bins) > 0 { 288 // In case the first bin is empty, look for the last trade before @starttime. 289 if !utils.IsInBin(trades[0].Time, bins[0]) { 290 previousTrade, baseAsseteErr := r.DS.GetTradesByExchangesAndBaseAssets(asset, baseAssets, exchangesString, endtime.AddDate(0, 0, -10), starttime, 1) 291 if baseAsseteErr != nil || len(previousTrade) == 0 { 292 log.Error("get initial trade: ", err, baseAsseteErr) 293 // Fill with a zero trade so we can build blocks. 294 auxTrade := trades[0] 295 auxTrade.Volume = 0 296 auxTrade.Price = 0 297 auxTrade.EstimatedUSDPrice = 0 298 trades = append([]dia.Trade{auxTrade}, trades...) 299 } else { 300 trades = append([]dia.Trade{previousTrade[0]}, trades...) 301 } 302 } 303 tradeBlocks = queryhelper.NewBlockGenerator(trades).GenerateBlocks(blockSizeSeconds, blockShiftSeconds, bins) 304 log.Println("Total TradeBlocks", len(tradeBlocks)) 305 } 306 307 } else if *filter == "ema" { 308 emaFilterPoints, err = r.DS.GetFilter("MA120", asset, "", starttimeimmutable, endtimeimmutable) 309 if err != nil { 310 log.Errorln("Error getting filter", err) 311 } 312 } 313 314 switch *filter { 315 case "ema": 316 { 317 filterPoints, filterMetadata = queryhelper.FilterEMA(emaFilterPoints, asset, int(blockSizeSeconds)) 318 } 319 case "mair": 320 { 321 filterPoints, filterMetadata = queryhelper.FilterMAIR(tradeBlocks, asset, int(blockSizeSeconds)) 322 } 323 case "ma": 324 { 325 filterPoints, filterMetadata = queryhelper.FilterMA(tradeBlocks, asset, int(blockSizeSeconds)) 326 } 327 case "vwap": 328 { 329 filterPoints, filterMetadata = queryhelper.FilterVWAP(tradeBlocks, asset, int(blockSizeSeconds)) 330 } 331 case "vwapir": 332 { 333 filterPoints, filterMetadata = queryhelper.FilterVWAPIR(tradeBlocks, asset, int(blockSizeSeconds)) 334 } 335 case "medir": 336 { 337 filterPoints, filterMetadata = queryhelper.FilterMEDIR(tradeBlocks, asset, int(blockSizeSeconds)) 338 } 339 case "vol": 340 { 341 filterPoints, filterMetadata = queryhelper.FilterVOL(tradeBlocks, asset, int(blockSizeSeconds)) 342 } 343 344 } 345 346 var fpr []*FilterPointResolver 347 348 for _, fp := range filterPoints { 349 fpr = append(fpr, &FilterPointResolver{q: fp}) 350 351 } 352 353 return &FilterPointMetaResolver{fpr: &fpr, min: filterMetadata.Min, max: filterMetadata.Max}, nil 354 } 355 356 // GetxcFeed returns filter points for a (possibly) cross chain feed given by @QuoteAssets. 357 func (r *DiaResolver) GetxcFeed(ctx context.Context, args struct { 358 Filter graphql.NullString 359 QuoteAssets *[]Asset 360 Exchanges *[]graphql.NullString 361 BlockSizeSeconds graphql.NullInt 362 BlockShiftSeconds graphql.NullInt 363 StartTime graphql.NullTime 364 EndTime graphql.NullTime 365 }) (*[]*FilterPointResolver, error) { 366 367 // --- Parse input data --- 368 var ( 369 vr *[]*FilterPointResolver 370 tradeBlocks []queryhelper.Block 371 filterPoints []dia.FilterPoint 372 ) 373 374 filter := args.Filter.Value 375 if containsSpecialChars(*filter) { 376 return nil, errInvalidInputParams 377 } 378 379 var quoteAssets []dia.Asset 380 if len(*args.QuoteAssets) > 0 { 381 for i := range *args.QuoteAssets { 382 quoteAssets = append(quoteAssets, dia.Asset{Address: *(*args.QuoteAssets)[i].Address.Value, Blockchain: *(*args.QuoteAssets)[i].BlockChain.Value}) 383 } 384 } 385 386 var exchanges []string 387 if len(*args.Exchanges) > 0 { 388 for i := range *args.Exchanges { 389 if containsSpecialChars(*(*args.Exchanges)[i].Value) { 390 continue 391 } 392 exchanges = append(exchanges, *(*args.Exchanges)[i].Value) 393 } 394 } 395 396 blockSizeSeconds := int64(*args.BlockSizeSeconds.Value) 397 var blockShiftSeconds int64 398 if args.BlockShiftSeconds.Value != nil { 399 blockShiftSeconds = int64(*args.BlockShiftSeconds.Value) 400 } else { 401 blockShiftSeconds = blockSizeSeconds 402 } 403 404 // --- Make time bins according to block size and block shift parameters --- 405 starttime := args.StartTime.Value.Time 406 endtime := args.EndTime.Value.Time 407 if args.EndTime.Set { 408 endtime = args.EndTime.Value.Time 409 } 410 if endtime.After(time.Now()) { 411 endtime = time.Now() 412 } 413 414 // --- Limit the (time-)range of the query to 24 hours --- 415 maxStartTime := endtime.Add(-time.Duration(60*24) * time.Minute) 416 if starttime.Before(maxStartTime) { 417 starttime = maxStartTime 418 } 419 420 // (Potentially) decrease starttime such that an integer number of bins fits into the whole range. 421 if endtime.Unix()-starttime.Unix() < blockSizeSeconds { 422 // Just one block. 423 starttime = time.Unix(endtime.Unix()-blockSizeSeconds, 0) 424 } else { 425 starttime = time.Unix(starttime.Unix()-(blockShiftSeconds-((endtime.Unix()-starttime.Unix()-blockSizeSeconds)%blockShiftSeconds)), 0) 426 } 427 428 // --- Make time bins according to block size and block shift parameters --- 429 bins := utils.MakeBins(starttime, endtime, blockSizeSeconds, blockShiftSeconds) 430 var ( 431 starttimes []time.Time 432 endtimes []time.Time 433 ) 434 if blockShiftSeconds <= blockSizeSeconds { 435 // Bins overlap and hence all trades in total time range are needed. 436 starttimes = []time.Time{starttime} 437 endtimes = []time.Time{endtime} 438 } else { 439 // Bins are disjoint. Hence, only fetch trades per bin. 440 for _, bin := range bins { 441 starttimes = append(starttimes, bin.Starttime) 442 endtimes = append(endtimes, bin.Endtime) 443 } 444 } 445 446 // --- Fetch trades --- 447 trades, err := r.DS.GetxcTradesByExchangesBatched(quoteAssets, exchanges, starttimes, endtimes) 448 if err != nil { 449 return vr, err 450 } 451 452 if len(trades) > 0 && len(bins) > 0 { 453 // In case the first bin is empty, look for the last trade before @starttime. 454 if !utils.IsInBin(trades[0].Time, bins[0]) { 455 trades = r.fillFirstBin(quoteAssets, exchanges, trades, starttime.AddDate(0, 0, -10), starttime) 456 } 457 tradeBlocks = queryhelper.NewBlockGenerator(trades).GenerateBlocks(blockSizeSeconds, blockShiftSeconds, bins) 458 log.Println("Total TradeBlocks", len(tradeBlocks)) 459 } 460 461 switch *filter { 462 case "mair": 463 { 464 filterPoints, _ = queryhelper.FilterMAIR(tradeBlocks, quoteAssets[0], int(blockSizeSeconds)) 465 } 466 case "ma": 467 { 468 filterPoints, _ = queryhelper.FilterMA(tradeBlocks, quoteAssets[0], int(blockSizeSeconds)) 469 } 470 case "vwap": 471 { 472 filterPoints, _ = queryhelper.FilterVWAP(tradeBlocks, quoteAssets[0], int(blockSizeSeconds)) 473 } 474 case "vwapir": 475 { 476 filterPoints, _ = queryhelper.FilterVWAPIR(tradeBlocks, quoteAssets[0], int(blockSizeSeconds)) 477 } 478 case "medir": 479 { 480 filterPoints, _ = queryhelper.FilterMEDIR(tradeBlocks, quoteAssets[0], int(blockSizeSeconds)) 481 } 482 case "vol": 483 { 484 filterPoints, _ = queryhelper.FilterVOL(tradeBlocks, quoteAssets[0], int(blockSizeSeconds)) 485 } 486 } 487 488 var fpr []*FilterPointResolver 489 for _, fp := range filterPoints { 490 fpr = append(fpr, &FilterPointResolver{q: fp}) 491 } 492 493 return &fpr, nil 494 } 495 496 // TO DO: Use context? 497 func (r *DiaResolver) GetFeed(ctx context.Context, args struct { 498 Filter graphql.NullString 499 BlockSizeSeconds graphql.NullInt 500 BlockShiftSeconds graphql.NullInt 501 StartTime graphql.NullTime 502 EndTime graphql.NullTime 503 TradeVolumeThreshold graphql.NullFloat 504 NativeDenomination graphql.NullBool 505 FeedSelection *[]FeedSelection 506 }) (*[]*FilterPointExtendedResolver, error) { 507 var ( 508 tradeBlocks []queryhelper.Block 509 sr *[]*FilterPointExtendedResolver 510 starttime time.Time 511 endtime time.Time 512 blockShiftSeconds int64 513 tradeVolumeThreshold float64 514 err error 515 nativeDenomination bool 516 ) 517 518 // Parsing input parameters. 519 if args.Filter.Value == nil { 520 return sr, errors.New("filter must be set") 521 } 522 filter := *args.Filter.Value 523 if containsSpecialChars(filter) { 524 return nil, errInvalidInputParams 525 } 526 blockSizeSeconds := int64(*args.BlockSizeSeconds.Value) 527 if args.BlockShiftSeconds.Value != nil { 528 blockShiftSeconds = int64(*args.BlockShiftSeconds.Value) 529 } 530 // Set blockShiftSeconds = blockSizeSeconds per default if not given. 531 if blockShiftSeconds == 0 { 532 blockShiftSeconds = blockSizeSeconds 533 } 534 535 // Handle timestamps. 536 if args.EndTime.Value != nil { 537 endtime = args.EndTime.Value.Time 538 } else { 539 endtime = time.Now() 540 } 541 if args.StartTime.Value != nil { 542 starttime = args.StartTime.Value.Time 543 } else { 544 starttime = endtime.Add(-time.Duration(1 * time.Hour)) 545 } 546 if endtime.Before(starttime) { 547 return sr, errors.New("startTime must be before EndTime") 548 } 549 if endtime.After(time.Now()) { 550 endtime = time.Now() 551 } 552 // --- Limit the (time-)range of the query to 24 hours --- 553 maxStartTime := endtime.Add(-time.Duration(60*24) * time.Minute) 554 if starttime.Before(maxStartTime) { 555 starttime = maxStartTime 556 } 557 // (Potentially) decrease starttime such that an integer number of bins fits into the whole range. 558 if endtime.Unix()-starttime.Unix() < blockSizeSeconds { 559 // Just one block. 560 starttime = time.Unix(endtime.Unix()-blockSizeSeconds, 0) 561 } else { 562 starttime = time.Unix(starttime.Unix()-(blockShiftSeconds-((endtime.Unix()-starttime.Unix()-blockSizeSeconds)%blockShiftSeconds)), 0) 563 } 564 // starttimeimmutable := starttime 565 // endtimeimmutable := endtime 566 567 if args.TradeVolumeThreshold.Value != nil { 568 tradeVolumeThreshold = *args.TradeVolumeThreshold.Value 569 } else { 570 tradeVolumeThreshold = TRADE_VOLUME_THRESHOLD_DEFAULT 571 } 572 573 // If nativeDenomination is true, price is returned in terms of the respective base asset. 574 // Default is false, i.e. price is returned in USD denomination. 575 if args.NativeDenomination.Value != nil { 576 nativeDenomination = *args.NativeDenomination.Value 577 } 578 579 if args.FeedSelection == nil { 580 return sr, errors.New("at least 1 asset must be selected") 581 } 582 feedselection, statusMessage, statusCode, err := r.castLocalFeedSelection(*args.FeedSelection) 583 if err != nil { 584 return sr, err 585 } 586 587 var ( 588 // filterPoints, emaFilterPoints []dia.FilterPoint 589 filterPoints []dia.FilterPointExtended 590 ) 591 592 // Make time bins according to block size and block shift parameters. 593 bins := utils.MakeBins(starttime, endtime, blockSizeSeconds, blockShiftSeconds) 594 595 // Fetch trades. 596 var ( 597 trades []dia.Trade 598 starttimes []time.Time 599 endtimes []time.Time 600 ) 601 602 if blockShiftSeconds <= blockSizeSeconds { 603 // Fetch all trades in time range. 604 starttimes = []time.Time{starttime} 605 endtimes = []time.Time{endtime} 606 } else { 607 // Fetch trades batched for disjoint bins. 608 for _, bin := range bins { 609 starttimes = append(starttimes, bin.Starttime) 610 endtimes = append(endtimes, bin.Endtime) 611 } 612 } 613 trades, err = r.DS.GetTradesByFeedSelection(feedselection, starttimes, endtimes, 0) 614 if err != nil { 615 return sr, err 616 } 617 log.Println("Generating blocks, Total Trades", len(trades)) 618 log.Info("generating bins. Total bins: ", len(bins)) 619 620 if len(bins) > 0 { 621 // In case the first bin is empty, look for the last trades before @starttime 622 // in order to select the most recent one with sufficient volume. 623 if len(trades) == 0 || !utils.IsInBin(trades[0].Time, bins[0]) || trades[0].VolumeUSD() < tradeVolumeThreshold { 624 previousTrade, err := r.DS.GetTradesByFeedSelection(feedselection, []time.Time{endtime.AddDate(0, 0, -10)}, []time.Time{starttime}, lookbackTradesNumber) 625 if len(previousTrade) == 0 { 626 log.Error("get initial trade: ", err) 627 // Fill with a zero trade so we can build blocks. 628 auxTrade := trades[0] 629 auxTrade.Volume = 0 630 auxTrade.Price = 0 631 auxTrade.EstimatedUSDPrice = 0 632 trades = append([]dia.Trade{auxTrade}, trades...) 633 } else { 634 for _, t := range previousTrade { 635 if t.VolumeUSD() > tradeVolumeThreshold { 636 trades = append([]dia.Trade{t}, trades...) 637 break 638 } 639 } 640 } 641 } 642 tradeBlocks = queryhelper.NewBlockGenerator(trades).GenerateBlocks(blockSizeSeconds, blockShiftSeconds, bins) 643 log.Println("Total TradeBlocks", len(tradeBlocks)) 644 } 645 646 switch filter { 647 case "mair": 648 { 649 filterPoints = queryhelper.FilterMAIRextended(tradeBlocks, feedselection[0].Asset, int(blockSizeSeconds), tradeVolumeThreshold, nativeDenomination) 650 } 651 case "ma": 652 { 653 filterPoints = queryhelper.FilterMAextended(tradeBlocks, feedselection[0].Asset, int(blockSizeSeconds), tradeVolumeThreshold, nativeDenomination) 654 } 655 case "vwap": 656 { 657 filterPoints = queryhelper.FilterVWAPextended(tradeBlocks, feedselection[0].Asset, int(blockSizeSeconds), tradeVolumeThreshold, nativeDenomination) 658 } 659 case "vwapir": 660 { 661 filterPoints = queryhelper.FilterVWAPIRextended(tradeBlocks, feedselection[0].Asset, int(blockSizeSeconds), tradeVolumeThreshold, nativeDenomination) 662 } 663 case "medir": 664 { 665 filterPoints = queryhelper.FilterMEDIRextended(tradeBlocks, feedselection[0].Asset, int(blockSizeSeconds), tradeVolumeThreshold, nativeDenomination) 666 } 667 case "vol": 668 { 669 filterPoints = queryhelper.FilterVOLextended(tradeBlocks, feedselection[0].Asset, int(blockSizeSeconds)) 670 } 671 672 } 673 674 var fper []*FilterPointExtendedResolver 675 676 for _, fpe := range filterPoints { 677 fpe.StatusMessage = statusMessage 678 fpe.StatusCode = statusCode 679 fper = append(fper, &FilterPointExtendedResolver{q: fpe}) 680 } 681 682 return &fper, nil 683 } 684 685 // GetFeedAggregation computes stats such as volume and number of trades fo a given FeedSelection. 686 func (r *DiaResolver) GetFeedAggregation(ctx context.Context, args struct { 687 StartTime graphql.NullTime 688 EndTime graphql.NullTime 689 TradeVolumeThreshold graphql.NullFloat 690 FeedSelection *[]FeedSelection 691 }) (*[]*FeedSelectionAggregatedResolver, error) { 692 var ( 693 sr []*FeedSelectionAggregatedResolver 694 starttime time.Time 695 endtime time.Time 696 tradeVolumeThreshold float64 697 err error 698 ) 699 700 // Handle timestamps. 701 if args.EndTime.Value != nil { 702 endtime = args.EndTime.Value.Time 703 } else { 704 endtime = time.Now() 705 } 706 if args.StartTime.Value != nil { 707 starttime = args.StartTime.Value.Time 708 } else { 709 starttime = endtime.Add(-time.Duration(1 * time.Hour)) 710 } 711 if endtime.Before(starttime) { 712 return &sr, errors.New("startTime must be before EndTime") 713 } 714 if endtime.After(time.Now()) { 715 endtime = time.Now() 716 } 717 // --- Limit the (time-)range of the query to 24 hours --- 718 maxStartTime := endtime.Add(-time.Duration(7*24*60) * time.Minute) 719 if starttime.Before(maxStartTime) { 720 starttime = maxStartTime 721 } 722 723 if args.TradeVolumeThreshold.Value != nil { 724 tradeVolumeThreshold = *args.TradeVolumeThreshold.Value 725 } else { 726 tradeVolumeThreshold = TRADE_VOLUME_THRESHOLD_DEFAULT 727 } 728 729 if args.FeedSelection == nil { 730 return &sr, errors.New("at least 1 asset must be selected") 731 } 732 feedselection, statusMessage, statusCode, err := r.castLocalFeedSelection(*args.FeedSelection) 733 if err != nil { 734 return &sr, err 735 } 736 737 // Get aggregated data in given time-range. 738 fsa, err := r.DS.GetAggregatedFeedSelection(feedselection, starttime, endtime, tradeVolumeThreshold) 739 if err != nil { 740 log.Error("GetAggregatedFeedSelection: ", err) 741 return &sr, err 742 } 743 744 // Fill response slice. 745 var fsar []*FeedSelectionAggregatedResolver 746 for _, fs := range fsa { 747 fs.StatusMessage = statusMessage 748 fs.StatusCode = statusCode 749 if fs.Pooladdress != "" { 750 pool, err := r.RelDB.GetPoolByAddress(fs.Basetoken.Blockchain, fs.Pooladdress) 751 if err != nil { 752 log.Error("GetPoolByAddress: ", err) 753 } 754 if pool.Time.After(time.Now().AddDate(0, 0, -7)) { 755 fs.PoolLiquidityUSD, _ = pool.GetPoolLiquidityUSD() 756 } 757 } 758 fsar = append(fsar, &FeedSelectionAggregatedResolver{q: fs}) 759 } 760 761 return &fsar, nil 762 } 763 764 func (r *DiaResolver) GetVWALP(ctx context.Context, args struct { 765 Quotetokenblockchain graphql.NullString 766 Quotetokenaddress graphql.NullString 767 BaseAssets *[]Asset 768 Exchanges *[]graphql.NullString 769 BlockDurationSeconds graphql.NullInt 770 EndTime graphql.NullTime 771 BasisPoints graphql.NullInt 772 }) (*VWALPResolver, error) { 773 774 // --- Parse input data --- 775 var vr *VWALPResolver 776 if containsSpecialChars(*args.Quotetokenaddress.Value) || containsSpecialChars(*args.Quotetokenblockchain.Value) { 777 return nil, errInvalidInputParams 778 } 779 780 quoteAsset, err := r.RelDB.GetAsset(*args.Quotetokenaddress.Value, *args.Quotetokenblockchain.Value) 781 if err != nil { 782 log.Error("GetAsset: ", err) 783 } 784 785 var baseAssets []dia.Asset 786 if len(*args.BaseAssets) > 0 { 787 for i := range *args.BaseAssets { 788 if containsSpecialChars(*(*args.BaseAssets)[i].Address.Value) || containsSpecialChars(*(*args.BaseAssets)[i].BlockChain.Value) { 789 continue 790 } 791 baseAssets = append(baseAssets, dia.Asset{Address: *(*args.BaseAssets)[i].Address.Value, Blockchain: *(*args.BaseAssets)[i].BlockChain.Value}) 792 } 793 } 794 795 var exchanges []string 796 if len(*args.Exchanges) > 0 { 797 for i := range *args.Exchanges { 798 if containsSpecialChars(*(*args.Exchanges)[i].Value) { 799 continue 800 } 801 exchanges = append(exchanges, *(*args.Exchanges)[i].Value) 802 } 803 } 804 805 BlockDurationSeconds := *args.BlockDurationSeconds.Value 806 basisPoints := *args.BasisPoints.Value 807 endtime := time.Now() 808 if args.EndTime.Set { 809 endtime = args.EndTime.Value.Time 810 } 811 // ----------------------- 812 813 // Fetch trades from Influx. 814 trades, err := r.DS.GetTradesByExchangesAndBaseAssets( 815 quoteAsset, 816 baseAssets, 817 exchanges, 818 endtime.Add(-time.Duration(BlockDurationSeconds)*time.Second), 819 endtime, 820 0, 821 ) 822 if err != nil { 823 return vr, err 824 } 825 826 tradesByExchange := make(map[string][]dia.Trade) 827 for _, trade := range trades { 828 tradesByExchange[trade.Source] = append(tradesByExchange[trade.Source], trade) 829 } 830 831 // Get last trades and volumes. 832 var lastPrices []float64 833 var volumes []float64 834 for exchange := range tradesByExchange { 835 block := queryhelper.Block{Trades: tradesByExchange[exchange], TimeStamp: endtime.UnixNano()} 836 filterPoints, _ := queryhelper.FilterVOL([]queryhelper.Block{block}, quoteAsset, int(BlockDurationSeconds)) 837 if len(filterPoints) > 0 { 838 lastPrices = append(lastPrices, filterPoints[0].LastTrade.EstimatedUSDPrice) 839 volumes = append(volumes, filterPoints[0].Value) 840 } 841 } 842 843 // Outlier detection. 844 prices, volumes, _, err := utils.DiscardOutliers(lastPrices, volumes, float64(basisPoints)) 845 if err != nil { 846 log.Error("DiscardOutliers: ", err) 847 } 848 849 // Build vwap. 850 var vwap float64 851 var volTotal float64 852 for i := range prices { 853 vwap += prices[i] * volumes[i] 854 volTotal += volumes[i] 855 } 856 if volTotal > 0 { 857 vwap /= volTotal 858 } 859 860 var response vwalp 861 response.Symbol = quoteAsset.Symbol 862 response.Value = vwap 863 response.Time = endtime 864 865 return &VWALPResolver{q: response}, nil 866 } 867 868 func (r *DiaResolver) fillFirstBin(assets []dia.Asset, exchanges []string, trades []dia.Trade, starttime time.Time, endtime time.Time) []dia.Trade { 869 previousTrade, quoteAssetsErr := r.DS.GetxcTradesByExchangesBatched(assets, exchanges, []time.Time{starttime}, []time.Time{endtime}) 870 if quoteAssetsErr != nil || len(previousTrade) == 0 { 871 log.Error("get initial trade: ", quoteAssetsErr) 872 // Fill with a zero trade so we can build blocks. 873 auxTrade := trades[0] 874 auxTrade.Volume = 0 875 auxTrade.Price = 0 876 auxTrade.EstimatedUSDPrice = 0 877 trades = append([]dia.Trade{auxTrade}, trades...) 878 } else { 879 trades = append([]dia.Trade{previousTrade[len(previousTrade)-1]}, trades...) 880 log.Warn("previous trade: ", previousTrade[len(previousTrade)-1]) 881 } 882 return trades 883 } 884 885 // castLocalFeedSelection casts the input selection into a @[]dia.FeedSelection type 886 // so that we can call the corresponding influx trades getter. 887 // It also checks for admissibility of the underlying input data. 888 func (r *DiaResolver) castLocalFeedSelection(fs []FeedSelection) ([]dia.FeedSelection, string, int32, error) { 889 var dfs []dia.FeedSelection 890 var warning string 891 var warnings []string 892 for _, localFeedSelection := range fs { 893 var diaFeedSelection dia.FeedSelection 894 895 // Parse asset. 896 if localFeedSelection.Address.Value == nil || localFeedSelection.Blockchain.Value == nil { 897 err := errors.New("missing asset's address and blockchain") 898 return dfs, warning, 1, err 899 } 900 diaFeedSelection.Asset = dia.Asset{ 901 Address: normalizeAddress(*localFeedSelection.Address.Value, *localFeedSelection.Blockchain.Value), 902 Blockchain: *localFeedSelection.Blockchain.Value, 903 } 904 905 // Parse liquidity threshold. 906 if localFeedSelection.LiquidityThreshold.Value != nil { 907 diaFeedSelection.LiquidityThreshold = *localFeedSelection.LiquidityThreshold.Value 908 } 909 910 // Check for exchange input. Continue if neither of exchange and liquidty threshold are set. 911 if localFeedSelection.Exchangepairs == nil && diaFeedSelection.LiquidityThreshold == 0 { 912 dfs = append(dfs, diaFeedSelection) 913 continue 914 } 915 916 // Fetch admissible pools in case liquidity threshold is set. 917 if diaFeedSelection.LiquidityThreshold > 0 { 918 pools, errPools := r.RelDB.GetPoolsByAsset(diaFeedSelection.Asset, 0, diaFeedSelection.LiquidityThreshold) 919 if errPools != nil { 920 return dfs, warning, 1, errPools 921 } 922 for _, pool := range pools { 923 924 // In order to append pool to corresponding entry of diaFeedSelection first check 925 // if the exchange was already added. 926 var poolAdded bool 927 for i := range diaFeedSelection.Exchangepairs { 928 if diaFeedSelection.Exchangepairs[i].Exchange == pool.Exchange { 929 diaFeedSelection.Exchangepairs[i].Pools = append(diaFeedSelection.Exchangepairs[i].Pools, pool) 930 poolAdded = true 931 break 932 } 933 } 934 if !poolAdded { 935 var eps dia.ExchangepairSelection 936 eps.Exchange = pool.Exchange 937 eps.Pools = append(eps.Pools, pool) 938 diaFeedSelection.Exchangepairs = append(diaFeedSelection.Exchangepairs, eps) 939 } 940 941 } 942 if len(pools) > 0 { 943 dfs = append(dfs, diaFeedSelection) 944 } 945 // Continue, i.e. ignore (possibly) given exchangepairs. 946 continue 947 } 948 949 // Parse exchanges. 950 for _, ep := range *localFeedSelection.Exchangepairs { 951 var diaExchangepairselection dia.ExchangepairSelection 952 if ep.Exchange.Value == nil { 953 err := errors.New("missing exchange name") 954 return dfs, warning, statusCodesMap[statusError], err 955 } 956 957 // Check if exchange is valid. 958 var exchangeOk bool 959 var exchange dia.Exchange 960 for e := range EXCHANGES { 961 if strings.EqualFold(*ep.Exchange.Value, e) { 962 exchangeOk = true 963 exchange = EXCHANGES[e] 964 break 965 } 966 } 967 if !exchangeOk && *ep.Exchange.Value != "" { 968 warnings = append(warnings, fmt.Sprintf("exchange name not valid: %s. ", *ep.Exchange.Value)) 969 continue 970 } 971 diaExchangepairselection.Exchange = exchange 972 973 // Select all pairs on given exchange if not specified. 974 if ep.Pairs == nil { 975 // Check if asset is traded as quotetoken on exchange and emit warning if not. 976 eps, err := r.RelDB.GetExchangepairsByAsset( 977 dia.Asset{ 978 Address: diaFeedSelection.Asset.Address, 979 Blockchain: diaFeedSelection.Asset.Blockchain, 980 }, 981 exchange.Name, 982 false, 983 ) 984 log.Infof("Number of pairs on exchange %s: %v", exchange.Name, len(eps)) 985 for _, e := range eps { 986 log.Info("ForeignName: ", e.ForeignName) 987 } 988 if err != nil { 989 log.Errorf("GetExchangepairsByAsset for %s on %s: %v", diaFeedSelection.Asset.Address, diaFeedSelection.Asset.Blockchain, err) 990 } 991 if len(eps) == 0 { 992 warnings = append(warnings, fmt.Sprintf("%s-%s: asset not available on %s. ", diaFeedSelection.Asset.Blockchain, diaFeedSelection.Asset.Address, exchange.Name)) 993 } 994 995 diaFeedSelection.Exchangepairs = append(diaFeedSelection.Exchangepairs, diaExchangepairselection) 996 continue 997 } 998 999 // Check whether pairs or pools are given. 1000 var pairs bool 1001 var pairsCount int 1002 for _, p := range *ep.Pairs { 1003 if p.Value == nil { 1004 return dfs, warning, statusCodesMap[statusError], errors.New("missing pair identifier") 1005 } 1006 if len(strings.Split(*p.Value, PAIR_SEPARATOR)) == 2 { 1007 pairs = true 1008 pairsCount++ 1009 } 1010 } 1011 if !(pairsCount == len(*ep.Pairs) || pairsCount == 0) { 1012 return dfs, warning, statusCodesMap[statusError], errors.New("pair/pool notation not consistent") 1013 } 1014 1015 if exchange.Name != "" && exchange.Centralized { 1016 if !pairs { 1017 return dfs, warning, statusCodesMap[statusError], errors.New("wrong notation for pairs") 1018 } 1019 for _, p := range *ep.Pairs { 1020 if len(strings.Split(*p.Value, PAIR_SEPARATOR)) < 2 { 1021 return dfs, warning, statusCodesMap[statusError], errors.New("pair not in requested format TokenA-TokenB") 1022 } 1023 1024 quoteAsset, errQuote := r.GetAssetByExchangesymbol(exchange.Name, strings.Split(*p.Value, PAIR_SEPARATOR)[0]) 1025 baseAsset, errBase := r.GetAssetByExchangesymbol(exchange.Name, strings.Split(*p.Value, PAIR_SEPARATOR)[1]) 1026 if errQuote != nil || errBase != nil { 1027 warnings = append(warnings, fmt.Sprintf("%s: pair not available on %s. ", *p.Value, exchange.Name)) 1028 } 1029 pair := dia.Pair{ 1030 QuoteToken: quoteAsset, 1031 BaseToken: baseAsset, 1032 } 1033 diaExchangepairselection.Pairs = append(diaExchangepairselection.Pairs, pair) 1034 } 1035 } else if exchange.Name == "" { 1036 diaExchangepairselection.Exchange.Centralized = true 1037 for _, p := range *ep.Pairs { 1038 1039 // Check admissibility of input. 1040 pairString := strings.Split(*p.Value, LIST_SEPARATOR) 1041 if len(pairString) < 2 { 1042 return dfs, warning, statusCodesMap[statusError], errors.New("exactly 2 assets must be given for each pair") 1043 } 1044 var pair dia.Pair 1045 1046 if len(strings.Split(pairString[0], PAIR_SEPARATOR)) < 2 { 1047 return dfs, warning, statusCodesMap[statusError], errors.New("asset not in requested format Blockchain-Address") 1048 } 1049 blockchainQuotetoken := strings.Title(strings.ToLower(strings.Split(pairString[0], PAIR_SEPARATOR)[0])) 1050 pair.QuoteToken.Address = normalizeAddress(strings.Split(pairString[0], PAIR_SEPARATOR)[1], blockchainQuotetoken) 1051 pair.QuoteToken.Blockchain = blockchainQuotetoken 1052 1053 blockchainBasetoken := strings.Title(strings.ToLower(strings.Split(pairString[1], PAIR_SEPARATOR)[0])) 1054 pair.BaseToken.Address = normalizeAddress(strings.Split(pairString[1], PAIR_SEPARATOR)[1], blockchainBasetoken) 1055 pair.BaseToken.Blockchain = blockchainBasetoken 1056 1057 diaExchangepairselection.Pairs = append(diaExchangepairselection.Pairs, pair) 1058 } 1059 } else { 1060 for _, p := range *ep.Pairs { 1061 pool, err := r.RelDB.GetPoolByAddress( 1062 diaFeedSelection.Asset.Blockchain, 1063 normalizeAddress(*p.Value, diaFeedSelection.Asset.Blockchain), 1064 ) 1065 if err != nil { 1066 return dfs, warning, statusCodesMap[statusError], err 1067 } 1068 diaExchangepairselection.Pools = append(diaExchangepairselection.Pools, pool) 1069 } 1070 } 1071 diaFeedSelection.Exchangepairs = append(diaFeedSelection.Exchangepairs, diaExchangepairselection) 1072 } 1073 1074 dfs = append(dfs, diaFeedSelection) 1075 } 1076 uniqueWarnings := "" 1077 for _, w := range utils.UniqueStrings(warnings) { 1078 uniqueWarnings += w 1079 } 1080 return dfs, uniqueWarnings, statusCodes(uniqueWarnings), nil 1081 } 1082 1083 // GetAssetByExchangesymbol is a poor man's cache that returns the underlying asset given 1084 // a @symbol on @exchange. 1085 func (r *DiaResolver) GetAssetByExchangesymbol(exchange string, symbol string) (dia.Asset, error) { 1086 if asset, ok := EXCHANGESYMBOL_CACHE[exchangesymbolIdentifier(exchange, symbol)]; ok { 1087 return asset, nil 1088 } 1089 asset, err := r.RelDB.GetExchangeSymbol(exchange, symbol) 1090 if err != nil { 1091 return dia.Asset{}, err 1092 } 1093 EXCHANGESYMBOL_CACHE[exchangesymbolIdentifier(exchange, symbol)] = asset 1094 return asset, nil 1095 } 1096 1097 // Returns the EIP55 compliant address in case @blockchain has an Ethereum ChainID. 1098 func makeAddressEIP55Compliant(address string, blockchain string) string { 1099 if strings.Contains(BLOCKCHAINS[blockchain].ChainID, "Ethereum") { 1100 return common.HexToAddress(address).Hex() 1101 } 1102 return address 1103 } 1104 1105 // Normalize address depending on the blockchain. 1106 func normalizeAddress(address string, blockchain string) string { 1107 if strings.Contains(BLOCKCHAINS[blockchain].ChainID, "Ethereum") { 1108 return makeAddressEIP55Compliant(address, blockchain) 1109 } 1110 if BLOCKCHAINS[blockchain].Name == dia.OSMOSIS { 1111 if strings.Contains(address, "ibc-") && len(strings.Split(address, "-")[1]) > 1 { 1112 return "ibc/" + strings.Split(address, "-")[1] 1113 } 1114 } 1115 return address 1116 } 1117 1118 func containsSpecialChars(s string) bool { 1119 return strings.ContainsAny(s, "!@#$%^&*()'\"|{}[];><?/`~,") 1120 } 1121 1122 func statusCodes(statusMessage string) int32 { 1123 var statusCode int32 1124 if strings.Contains(statusMessage, statusError) { 1125 return statusCodesMap[statusError] 1126 } 1127 for status := range statusCodesMap { 1128 if strings.Contains(statusMessage, status) { 1129 statusCode += statusCodesMap[status] 1130 } 1131 } 1132 return statusCode 1133 } 1134 1135 func exchangesymbolIdentifier(exchange string, symbol string) string { 1136 return exchange + "_" + symbol 1137 }