diff --git a/main/main.go b/main/main.go index 31a6f51..ad3c5a4 100644 --- a/main/main.go +++ b/main/main.go @@ -1,8 +1,8 @@ package main import ( + "fmt" "log" - "sort" "time" "github.com/gnoswap-labs/vwap" @@ -11,33 +11,24 @@ import ( // testing purposes func main() { - interval := time.Minute - ticker := time.NewTicker(interval) + start := time.Now().Unix() + ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() - calculateAndPrintVWAP() - for range ticker.C { - calculateAndPrintVWAP() - } -} - -func calculateAndPrintVWAP() { - vwapResults, err := vwap.FetchAndCalculateVWAP() - if err != nil { - log.Printf("Error fetching and calculating VWAP: %v\n", err) - return - } - - var tokens []string - for token := range vwapResults { - tokens = append(tokens, token) - } - sort.Strings(tokens) - - log.Println("VWAP results updated at", time.Now().Format("15:04:05")) - - for _, token := range tokens { - log.Printf("Token: %s, VWAP: %.4f\n", token, vwapResults[token]) + <-ticker.C + vwapResults, err := vwap.VWAP() + if err != nil { + log.Printf("Failed to calculate VWAP: %v", err) + continue + } + + fmt.Println("VWAP Results:") + for token, vwap := range vwapResults { + fmt.Printf("%s: %.8f\n", token, vwap) + } + + fmt.Println(time.Now().Unix() - start) + fmt.Println("--------------------") } } diff --git a/price.go b/price.go index 3f35c2c..776e87b 100644 --- a/price.go +++ b/price.go @@ -5,13 +5,13 @@ import ( "encoding/json" "fmt" "log" - "math" - "math/big" "net/http" "strconv" "time" ) +const priceEndpoint = "http://dev.api.gnoswap.io/v1/tokens/prices" + type TokenPrice struct { Path string `json:"path"` USD string `json:"usd"` @@ -46,9 +46,8 @@ type APIResponse struct { } func fetchTokenPrices(endpoint string) ([]TokenPrice, error) { - client := &http.Client{} // Timeout is managed by context + client := &http.Client{} - // Create a context with a 30-second timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -77,73 +76,24 @@ func fetchTokenPrices(endpoint string) ([]TokenPrice, error) { return nil, fmt.Errorf("received non-OK status code: %d", resp.StatusCode) } -// calculateVolume calculates the total volume in the USD for each token. -func calculateVolume(prices []TokenPrice) map[string]float64 { - volumeByToken := make(map[string]float64) - - for _, price := range prices { - volume, err := strconv.ParseFloat(price.VolumeUSD24h, 64) - if err != nil { - fmt.Printf("failed to parse volume for token %s: %v\n", price.Path, err) - } - - volumeByToken[price.Path] = volume - } - - return volumeByToken -} - -// calculateTokenUSDPrices calculates the USD price based on a base token (wugnot) price and token ratios. -func calculateTokenUSDPrices(tokenData *TokenData, baseTokenPrice float64) map[string]float64 { - tokenPrices := make(map[string]float64) - baseRatio := new(big.Float) - - // fund the base token ratio - for _, token := range tokenData.TokenRatio { - if token.TokenName == string(WUGNOT) { - ratio, _ := new(big.Float).SetString(token.Ratio) - baseRatio.Quo(ratio, big.NewFloat(math.Pow(2, 96))) - break - } - } - - // calculate token prices based on the base token price and ratios. - for _, token := range tokenData.TokenRatio { - if token.TokenName != string(WUGNOT) { - ratio, _ := new(big.Float).SetString(token.Ratio) - tokenRatio := new(big.Float).Quo(ratio, big.NewFloat(math.Pow(2, 96))) - tokenPrice := new(big.Float).Quo(baseRatio, tokenRatio) - - price, _ := tokenPrice.Float64() - tokenPrices[token.TokenName] = price * baseTokenPrice - } - } - - return tokenPrices -} - -func extractTrades(prices []TokenPrice) map[string][]TradeData { +func extractTrades(prices []TokenPrice, volumeByToken map[string]float64) map[string][]TradeData { trades := make(map[string][]TradeData) for _, price := range prices { - // calculatedVolume, err := strconv.ParseFloat(price.VolumeUSD24h, 64) - // if err != nil { - // fmt.Printf("Failed to parse volume for token %s: %v\n", price.Path, err) - // continue - // } usd, err := strconv.ParseFloat(price.USD, 64) if err != nil { fmt.Printf("failed to parse USD price for token %s: %v\n", price.Path, err) continue } + volume, ok := volumeByToken[price.Path] + if !ok { + fmt.Printf("volume not found for token %s\n", price.Path) + continue + } + trades[price.Path] = append(trades[price.Path], TradeData{ TokenName: price.Path, - // Volume: calculatedVolume, - - // hard coded because current calculated volume always be 0. - // therefore, we can't calculate the VWAP correctly. - // TODO: remove this hard coded value and use `calculatedVolume` instead. - Volume: 100, + Volume: volume, Ratio: usd, Timestamp: int(time.Now().Unix()), }) diff --git a/price_test.go b/price_test.go index 136ed68..8c9d4e8 100644 --- a/price_test.go +++ b/price_test.go @@ -121,74 +121,3 @@ func TestFetchTokenPricesLive(t *testing.T) { } } } - -func TestCalculateVolume(t *testing.T) { - t.Parallel() - mockTokenPrices := []TokenPrice{ - { - Path: "token1", - VolumeUSD24h: "1000.50", - }, - { - Path: "token2", - VolumeUSD24h: "2500.75", - }, - { - Path: "token3", - VolumeUSD24h: "500.25", - }, - } - - volumes := calculateVolume(mockTokenPrices) - - assert.Equal(t, 3, len(volumes)) - assert.InDelta(t, 1000.50, volumes["token1"], 0.001) - assert.InDelta(t, 2500.75, volumes["token2"], 0.001) - assert.InDelta(t, 500.25, volumes["token3"], 0.001) -} - -func TestCalculateTokenPrices(t *testing.T) { - t.Parallel() - mockTokenData := &TokenData{ - Status: struct { - Height int `json:"height"` - Timestamp int `json:"timestamp"` - }{ - Height: 61946, - Timestamp: 1715064552, - }, - TokenRatio: []struct { - TokenName string `json:"token"` - Ratio string `json:"ratio"` - }{ - { - TokenName: "gno.land/r/demo/wugnot", - Ratio: "79228162514264337593543950336", - }, - { - TokenName: "gno.land/r/demo/qux", - Ratio: "60536002769587966558221762891", - }, - { - TokenName: "gno.land/r/demo/foo", - Ratio: "121074204438706762251182081654", - }, - { - TokenName: "gno.land/r/demo/gns", - Ratio: "236174327852992866806677676716", - }, - }, - } - - baseTokenPrice := 1.0 - tokenPrices := calculateTokenUSDPrices(mockTokenData, baseTokenPrice) - - // 1 wugnot = 1.0 USD (base token) - // 1 qux = 1.308 USD - // 1 foo = 0.654 USD - // 1 gns = 0.335 USD - - assert.InDelta(t, 1.3087775685458196, tokenPrices["gno.land/r/demo/qux"], 1e-5) - assert.InDelta(t, 0.6543768995349725, tokenPrices["gno.land/r/demo/foo"], 1e-5) - assert.InDelta(t, 0.3354647528142011, tokenPrices["gno.land/r/demo/gns"], 1e-5) -} diff --git a/store_test.go b/store_test.go index 7719fd1..ceffa5d 100644 --- a/store_test.go +++ b/store_test.go @@ -13,7 +13,7 @@ func TestVWAPStorage(t *testing.T) { trades := generateMockTrades() for _, trade := range trades { - VWAP([]TradeData{trade}) + calculateVWAP([]TradeData{trade}) } expectedVWAPData := map[string][]VWAPData{ diff --git a/volume.go b/volume.go new file mode 100644 index 0000000..2d40e76 --- /dev/null +++ b/volume.go @@ -0,0 +1,22 @@ +package vwap + +import ( + "fmt" + "strconv" +) + +// calculateVolume calculates the total volume in the USD for each token. +func calculateVolume(prices []TokenPrice) map[string]float64 { + volumeByToken := make(map[string]float64) + + for _, price := range prices { + volume, err := strconv.ParseFloat(price.VolumeUSD24h, 64) + if err != nil { + fmt.Printf("failed to parse volume for token %s: %v\n", price.Path, err) + } + + volumeByToken[price.Path] = volume + } + + return volumeByToken +} diff --git a/volume_test.go b/volume_test.go new file mode 100644 index 0000000..4b6c4b9 --- /dev/null +++ b/volume_test.go @@ -0,0 +1,25 @@ +package vwap + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalculateVolume(t *testing.T) { + // Test case 1: Valid token prices + prices := []TokenPrice{ + {Path: "TOKEN1", VolumeUSD24h: "1000.50"}, + {Path: "TOKEN2", VolumeUSD24h: "500.25"}, + } + volumeByToken := calculateVolume(prices) + assert.Equal(t, 1000.50, volumeByToken["TOKEN1"]) + assert.Equal(t, 500.25, volumeByToken["TOKEN2"]) + + // Test case 2: Invalid volume format + prices = []TokenPrice{ + {Path: "TOKEN3", VolumeUSD24h: "invalid"}, + } + volumeByToken = calculateVolume(prices) + assert.Equal(t, 0.0, volumeByToken["TOKEN3"]) +} diff --git a/vwap.go b/vwap.go index 2028587..1dfef0a 100644 --- a/vwap.go +++ b/vwap.go @@ -1,8 +1,8 @@ package vwap -import "fmt" - -const priceEndpoint = "http://dev.api.gnoswap.io/v1/tokens/prices" +import ( + "fmt" +) // Token name type TokenIdentifier string @@ -32,9 +32,30 @@ func init() { lastPrices = make(map[string]float64) } -// VWAP calculates the Volume Weighted Average Price (VWAP) for the given set of trades. +func VWAP() (map[string]float64, error) { + prices, err := fetchTokenPrices(priceEndpoint) + if err != nil { + return nil, err + } + + volumeByToken := calculateVolume(prices) + trades := extractTrades(prices, volumeByToken) + vwapResults := make(map[string]float64) + + for tokenName, tradeData := range trades { + vwap, err := calculateVWAP(tradeData) + if err != nil { + return nil, err + } + vwapResults[tokenName] = vwap + } + + return vwapResults, nil +} + +// calculateVWAP calculates the Volume Weighted Average Price (calculateVWAP) for the given set of trades. // It returns the last price if there are no trades. -func VWAP(trades []TradeData) (float64, error) { +func calculateVWAP(trades []TradeData) (float64, error) { var numerator, denominator float64 if len(trades) == 0 { @@ -50,7 +71,7 @@ func VWAP(trades []TradeData) (float64, error) { if denominator == 0 { lastPrice, ok := lastPrices[trades[0].TokenName] if !ok { - return 0, fmt.Errorf("no last price available for token %s", trades[0].TokenName) + return 0, nil } return lastPrice, nil } @@ -62,23 +83,3 @@ func VWAP(trades []TradeData) (float64, error) { return vwap, nil } - -func FetchAndCalculateVWAP() (map[string]float64, error) { - prices, err := fetchTokenPrices(priceEndpoint) - if err != nil { - return nil, err - } - - trades := extractTrades(prices) - vwapResults := make(map[string]float64) - - for tokenName, tradeData := range trades { - vwap, err := VWAP(tradeData) - if err != nil { - return nil, err - } - vwapResults[tokenName] = vwap - } - - return vwapResults, nil -} diff --git a/vwap_test.go b/vwap_test.go index 7af7160..0f4c50f 100644 --- a/vwap_test.go +++ b/vwap_test.go @@ -18,7 +18,7 @@ func TestVWAPWithNoTrades(t *testing.T) { } expectedVWAP := 1.5 - actualVWAP, err := VWAP(trades) + actualVWAP, err := calculateVWAP(trades) assert.Nil(t, err, "Unexpected error") assert.Equal(t, expectedVWAP, actualVWAP, "VWAP calculation with no trades is incorrect") @@ -48,7 +48,7 @@ func TestVWAPWith10MinuteInterval(t *testing.T) { expectedVWAP := calculateExpectedVWAP(intervalTrades) expectedVWAPs = append(expectedVWAPs, expectedVWAP) - actualVWAP, err := VWAP(intervalTrades) + actualVWAP, err := calculateVWAP(intervalTrades) assert.Nil(t, err, "Unexpected error") actualVWAPs = append(actualVWAPs, actualVWAP) @@ -62,7 +62,7 @@ func TestVWAPWith10MinuteInterval(t *testing.T) { expectedVWAP := calculateExpectedVWAP(intervalTrades) expectedVWAPs = append(expectedVWAPs, expectedVWAP) - actualVWAP, err := VWAP(intervalTrades) + actualVWAP, err := calculateVWAP(intervalTrades) assert.Nil(t, err, "Unexpected error") actualVWAPs = append(actualVWAPs, actualVWAP)