Use decimals instead of floats in price cache (#362)
## Description Previously, the notional cache was using the `float64` type to manipulate price data. Since floating point types can't represent price data accurately, this commit changes the codebase to use a lossless representation (i.e.: `decimal.Decimal`).
This commit is contained in:
parent
06d6950ad3
commit
0ec1cb86e2
|
@ -3,7 +3,6 @@ package metric
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -11,6 +10,7 @@ import (
|
|||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/write"
|
||||
"github.com/shopspring/decimal"
|
||||
wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
@ -156,11 +156,11 @@ func (m *Metric) volumeMeasurement(ctx context.Context, vaa *sdk.VAA) error {
|
|||
p := MakePointForVaaVolumeParams{
|
||||
Logger: m.logger,
|
||||
Vaa: vaa,
|
||||
TokenPriceFunc: func(symbol domain.Symbol, timestamp time.Time) (float64, error) {
|
||||
TokenPriceFunc: func(symbol domain.Symbol, timestamp time.Time) (decimal.Decimal, error) {
|
||||
|
||||
priceData, err := m.notionalCache.Get(symbol)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return decimal.NewFromInt(0), err
|
||||
}
|
||||
|
||||
return priceData.NotionalUsd, nil
|
||||
|
@ -190,25 +190,6 @@ func (m *Metric) volumeMeasurement(ctx context.Context, vaa *sdk.VAA) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// toInt converts a float64 into a big.Int with 8 decimals of implicit precision.
|
||||
//
|
||||
// If we ever upgrade the notional cache to store prices as big integers,
|
||||
// this gnarly function won't be needed anymore.
|
||||
func floatToBigInt(f float64) (*big.Int, error) {
|
||||
|
||||
integral, frac := math.Modf(f)
|
||||
|
||||
strIntegral := strconv.FormatFloat(integral, 'f', 0, 64)
|
||||
strFrac := fmt.Sprintf("%.8f", frac)[2:]
|
||||
|
||||
i, err := strconv.ParseInt(strIntegral+strFrac, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return big.NewInt(i), nil
|
||||
}
|
||||
|
||||
// MakePointForVaaCount generates a data point for the VAA count measurement.
|
||||
//
|
||||
// Some VAAs will not generate a measurement, so the caller must always check
|
||||
|
@ -239,7 +220,7 @@ type MakePointForVaaVolumeParams struct {
|
|||
Vaa *sdk.VAA
|
||||
|
||||
// TokenPriceFunc returns the price of the given token at the specified timestamp.
|
||||
TokenPriceFunc func(symbol domain.Symbol, timestamp time.Time) (float64, error)
|
||||
TokenPriceFunc func(symbol domain.Symbol, timestamp time.Time) (decimal.Decimal, error)
|
||||
|
||||
// Logger is an optional parameter, in case the caller wants additional visibility.
|
||||
Logger *zap.Logger
|
||||
|
@ -321,10 +302,10 @@ func MakePointForVaaVolume(params *MakePointForVaaVolumeParams) (*write.Point, e
|
|||
}
|
||||
|
||||
// Convert the notional value to an integer with an implicit precision of 8 decimals
|
||||
notionalBigInt, err := floatToBigInt(notionalUSD)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert notional to big integer: %w", err)
|
||||
}
|
||||
notionalBigInt := notionalUSD.
|
||||
Truncate(8).
|
||||
Mul(decimal.NewFromInt(1e8)).
|
||||
BigInt()
|
||||
|
||||
// Calculate the volume, with an implicit precision of 8 decimals
|
||||
var volume big.Int
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -32,8 +33,8 @@ type NotionalLocalCacheReadable interface {
|
|||
|
||||
// PriceData is the notional value of assets in cache.
|
||||
type PriceData struct {
|
||||
NotionalUsd float64 `json:"notional_usd"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
NotionalUsd decimal.Decimal `json:"notional_usd"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,7 +22,6 @@ type TokenMetadata struct {
|
|||
Symbol Symbol
|
||||
CoingeckoID string
|
||||
Decimals int64
|
||||
price float64
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -9,4 +9,5 @@ sed -i 's/addr\:/TokenAddress\:/g' generated_mainnet_tokens.go
|
|||
sed -i 's/symbol\:/Symbol\:/g' generated_mainnet_tokens.go
|
||||
sed -i 's/coinGeckoId\:/CoingeckoID\:/g' generated_mainnet_tokens.go
|
||||
sed -i 's/decimals\:/Decimals\:/g' generated_mainnet_tokens.go
|
||||
sed -i 's/\, price\:.*}/}/g' generated_mainnet_tokens.go
|
||||
sed -i 's/TokenMetadata{TokenChain/{TokenChain/g' generated_mainnet_tokens.go
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/write"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/analytic/metric"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
@ -151,21 +152,15 @@ func (lp *LineParser) ParseLine(line []byte) (string, error) {
|
|||
{
|
||||
p := metric.MakePointForVaaVolumeParams{
|
||||
Vaa: vaa,
|
||||
TokenPriceFunc: func(_ domain.Symbol, timestamp time.Time) (float64, error) {
|
||||
TokenPriceFunc: func(_ domain.Symbol, timestamp time.Time) (decimal.Decimal, error) {
|
||||
|
||||
// fetch the historic price from cache
|
||||
price, err := lp.PriceCache.GetPriceByTime(
|
||||
int16(vaa.EmitterChain),
|
||||
tokenMetadata.CoingeckoID,
|
||||
timestamp,
|
||||
)
|
||||
price, err := lp.PriceCache.GetPriceByTime(tokenMetadata.CoingeckoID, timestamp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return decimal.NewFromInt(0), err
|
||||
}
|
||||
|
||||
// convert to float64
|
||||
result, _ := price.Float64()
|
||||
return result, nil
|
||||
return price, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -22,22 +22,24 @@ func NewCoinPricesCache(priceFile string) *CoinPricesCache {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *CoinPricesCache) GetPriceByTime(chainID int16, symbol string, day time.Time) (*decimal.Decimal, error) {
|
||||
func (c *CoinPricesCache) GetPriceByTime(coingeckoID string, day time.Time) (decimal.Decimal, error) {
|
||||
|
||||
// remove hours and minutes
|
||||
// remove hours and minutes,
|
||||
// times are in UTC
|
||||
day = time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// generate key
|
||||
key := fmt.Sprintf("%d%s%d", chainID, symbol, day.UnixMilli())
|
||||
// look up the price
|
||||
key := fmt.Sprintf("%s%d", coingeckoID, day.UnixMilli())
|
||||
if price, ok := c.Prices[key]; ok {
|
||||
return &price, nil
|
||||
return price, nil
|
||||
}
|
||||
return nil, fmt.Errorf("price not found for %s", key)
|
||||
|
||||
return decimal.NewFromInt(0), fmt.Errorf("price not found for %s", key)
|
||||
}
|
||||
|
||||
// load the csv file with prices into a map
|
||||
func (cpc *CoinPricesCache) InitCache() {
|
||||
|
||||
// open prices file
|
||||
file := cpc.filename
|
||||
f, err := os.Open(file)
|
||||
|
@ -45,23 +47,28 @@ func (cpc *CoinPricesCache) InitCache() {
|
|||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// read line by line
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
row := scanner.Text()
|
||||
|
||||
// split line by comma
|
||||
row := scanner.Text()
|
||||
tokens := strings.Split(row, ",")
|
||||
if len(tokens) != 5 {
|
||||
panic(fmt.Errorf("invalid line: %s", row))
|
||||
}
|
||||
// build map key: chainid+coingecko_id+timestamp
|
||||
key := fmt.Sprintf("%s%s%s", tokens[0], tokens[1], tokens[3])
|
||||
|
||||
// build map key: coingecko_id+timestamp
|
||||
key := fmt.Sprintf("%s%s", tokens[1], tokens[3])
|
||||
|
||||
// parse price
|
||||
price, err := decimal.NewFromString(tokens[4])
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed to parse price err=%v line=%s", err, row)
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
cpc.Prices[key] = price
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
)
|
||||
|
||||
|
@ -26,7 +27,7 @@ func NewCoingeckoAPI(url string) *CoingeckoAPI {
|
|||
|
||||
// NotionalUSD is the response from the coingecko API.
|
||||
type NotionalUSD struct {
|
||||
Price *float64 `json:"usd"`
|
||||
Price *decimal.Decimal `json:"usd"`
|
||||
}
|
||||
|
||||
// GetNotionalUSD returns the notional USD value for the given ids
|
||||
|
|
Loading…
Reference in New Issue