Skip to content

Commit

Permalink
remove hard coded volume in vwap
Browse files Browse the repository at this point in the history
  • Loading branch information
notJoon committed May 16, 2024
1 parent 831d9c7 commit 322279d
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 188 deletions.
43 changes: 17 additions & 26 deletions main/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package main

import (
"fmt"
"log"
"sort"
"time"

"github.com/gnoswap-labs/vwap"
Expand All @@ -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("--------------------")
}
}
72 changes: 11 additions & 61 deletions price.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()),
})
Expand Down
71 changes: 0 additions & 71 deletions price_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestVWAPStorage(t *testing.T) {
trades := generateMockTrades()

for _, trade := range trades {
VWAP([]TradeData{trade})
calculateVWAP([]TradeData{trade})

Check failure on line 16 in store_test.go

View workflow job for this annotation

GitHub Actions / test-and-lint

Error return value is not checked (errcheck)
}

expectedVWAPData := map[string][]VWAPData{
Expand Down
22 changes: 22 additions & 0 deletions volume.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 25 additions & 0 deletions volume_test.go
Original file line number Diff line number Diff line change
@@ -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"])
}
53 changes: 27 additions & 26 deletions vwap.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
}
Loading

0 comments on commit 322279d

Please sign in to comment.