Skip to content

Commit

Permalink
calculate VWAP with fetched data
Browse files Browse the repository at this point in the history
  • Loading branch information
notJoon committed May 10, 2024
1 parent 328b3dd commit 831d9c7
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 127 deletions.
43 changes: 43 additions & 0 deletions main/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"log"
"sort"
"time"

"github.com/gnoswap-labs/vwap"
)

// testing purposes

func main() {
interval := time.Minute
ticker := time.NewTicker(interval)
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])
}
}
65 changes: 56 additions & 9 deletions price.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package vwap

import (
"context"
"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 @@ -45,19 +46,35 @@ type APIResponse struct {
}

func fetchTokenPrices(endpoint string) ([]TokenPrice, error) {
resp, err := http.Get(endpoint)
client := &http.Client{} // Timeout is managed by context

// Create a context with a 30-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var apiResponse APIResponse
err = json.NewDecoder(resp.Body).Decode(&apiResponse)
resp, err := client.Do(req)
if err != nil {
log.Printf("Error making request: %v\n", err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
var apiResponse APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
log.Printf("Error decoding response: %v\n", err)
return nil, err
}
return apiResponse.Data, nil
}

return apiResponse.Data, nil
log.Printf("Received non-OK response: %d\n", resp.StatusCode)
return nil, fmt.Errorf("received non-OK status code: %d", resp.StatusCode)
}

// calculateVolume calculates the total volume in the USD for each token.
Expand All @@ -83,7 +100,7 @@ func calculateTokenUSDPrices(tokenData *TokenData, baseTokenPrice float64) map[s

// fund the base token ratio
for _, token := range tokenData.TokenRatio {
if token.TokenName == string(wugnot) {
if token.TokenName == string(WUGNOT) {
ratio, _ := new(big.Float).SetString(token.Ratio)
baseRatio.Quo(ratio, big.NewFloat(math.Pow(2, 96)))
break
Expand All @@ -92,7 +109,7 @@ func calculateTokenUSDPrices(tokenData *TokenData, baseTokenPrice float64) map[s

// calculate token prices based on the base token price and ratios.
for _, token := range tokenData.TokenRatio {
if token.TokenName != string(wugnot) {
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)
Expand All @@ -104,3 +121,33 @@ func calculateTokenUSDPrices(tokenData *TokenData, baseTokenPrice float64) map[s

return tokenPrices
}

func extractTrades(prices []TokenPrice) 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
}

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,
Ratio: usd,
Timestamp: int(time.Now().Unix()),
})
}

return trades
}
5 changes: 5 additions & 0 deletions price_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ func TestCalculateTokenPrices(t *testing.T) {
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)
Expand Down
1 change: 0 additions & 1 deletion store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type VWAPData struct {
var vwapDataMap map[string][]VWAPData

func init() {
lastPrices = make(map[string]float64)
vwapDataMap = make(map[string][]VWAPData)
}

Expand Down
88 changes: 35 additions & 53 deletions vwap.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package vwap

import (
"fmt"
"math/big"
)
import "fmt"

const priceEndpoint = "http://dev.api.gnoswap.io/v1/tokens/prices"

// Token name
type TokenIdentifier string

const (
wugnot TokenIdentifier = "gno.land/r/demo/wugnot" // base token (ratio = 1)
foo TokenIdentifier = "gno.land/r/demo/foo"
bar TokenIdentifier = "gno.land/r/demo/bar"
baz TokenIdentifier = "gno.land/r/demo/baz"
qux TokenIdentifier = "gno.land/r/demo/qux"
gns TokenIdentifier = "gno.land/r/demo/gns"
WUGNOT TokenIdentifier = "gno.land/r/demo/wugnot" // base token (ratio = 1)
FOO TokenIdentifier = "gno.land/r/demo/foo"
BAR TokenIdentifier = "gno.land/r/demo/bar"
BAZ TokenIdentifier = "gno.land/r/demo/baz"
QUX TokenIdentifier = "gno.land/r/demo/qux"
GNS TokenIdentifier = "gno.land/r/demo/gns"
)

// TradeData represents the data for a single trade.
Expand All @@ -29,74 +28,57 @@ type TradeData struct {
// This value will be used to show the last price if the token is not traded.
var lastPrices map[string]float64

func init() {
lastPrices = make(map[string]float64)
}

// VWAP calculates the Volume Weighted Average Price (VWAP) for the given set of trades.
// It returns the last price if there are no trades.
func VWAP(trades []TradeData) float64 {
func VWAP(trades []TradeData) (float64, error) {
var numerator, denominator float64

if len(trades) == 0 {
return 0, fmt.Errorf("no trades found")
}

for _, trade := range trades {
numerator += trade.Volume * trade.Ratio
denominator += trade.Volume
}

// return last price if there is no trade
if denominator == 0 {
return lastPrices[trades[0].TokenName]
lastPrice, ok := lastPrices[trades[0].TokenName]
if !ok {
return 0, fmt.Errorf("no last price available for token %s", trades[0].TokenName)
}
return lastPrice, nil
}

vwap := numerator / denominator
lastPrices[trades[0].TokenName] = vwap // save the last price

store(trades[0].TokenName, vwap, trades[0].Timestamp)

return vwap
return vwap, nil
}

// updateTrades retrieves and updates the trade data from RPC API.
// It returns the updated trades and any error that occurred during the process.
func updateTrades(jsonStr string) ([]TradeData, error) {
data, err := unmarshalResponseData(jsonStr)
func FetchAndCalculateVWAP() (map[string]float64, error) {
prices, err := fetchTokenPrices(priceEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response data: %v", err)
return nil, err
}

tradableTokens := []TokenIdentifier{wugnot, foo, bar, baz, qux, gns}

var trades []TradeData
for _, r := range data.Response {
ratio, ok := new(big.Int).SetString(r.Ratio, 10)
if !ok {
return nil, fmt.Errorf("error converting ratio to big.Int")
}

dvisor := new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)
result := new(big.Float).Quo(new(big.Float).SetInt(ratio), new(big.Float).SetInt(dvisor))
floatRatio, _ := result.Float64()

// TODO; remove testing logic
// testing purpose
// TODO: Get volume data by using API
tokenName := TokenIdentifier(r.Token)
if contains(tradableTokens, tokenName) {
trade := TradeData{
TokenName: string(tokenName),
Volume: 100,
Ratio: floatRatio,
Timestamp: data.Stat.Timestamp,
}
trades = append(trades, trade)
}
}

return trades, nil
}
trades := extractTrades(prices)
vwapResults := make(map[string]float64)

func contains(tokens []TokenIdentifier, name TokenIdentifier) bool {
for _, t := range tokens {
if t == name {
return true
for tokenName, tradeData := range trades {
vwap, err := VWAP(tradeData)
if err != nil {
return nil, err
}
vwapResults[tokenName] = vwap
}

return false
return vwapResults, nil
}
Loading

0 comments on commit 831d9c7

Please sign in to comment.