From bced6323efa4a495e4a51e8679b7a0d9668ae120 Mon Sep 17 00:00:00 2001 From: larscom Date: Sun, 3 Dec 2023 11:18:22 +0100 Subject: [PATCH] add markets endpoint --- examples/http/market/main.go | 16 +++++++ httpc/client.go | 83 ++++++++++++++++++++++-------------- httpc/httpclient.go | 36 ++++++++++++++++ httpc/httpclientauth.go | 8 ++-- jsond/market.go | 80 ++++++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 35 deletions(-) create mode 100644 examples/http/market/main.go create mode 100644 jsond/market.go diff --git a/examples/http/market/main.go b/examples/http/market/main.go new file mode 100644 index 0000000..98145aa --- /dev/null +++ b/examples/http/market/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "log" + + "github.com/larscom/go-bitvavo/v2" +) + +func main() { + client := bitvavo.NewHttpClient() + markets, err := client.GetMarkets() + if err != nil { + log.Fatal(err) + } + log.Println("Markets", markets) +} diff --git a/httpc/client.go b/httpc/client.go index 78945ae..fea1a75 100644 --- a/httpc/client.go +++ b/httpc/client.go @@ -27,8 +27,7 @@ func httpGet[T any]( logDebug func(message string, args ...any), config *authConfig, ) (T, error) { - reqUrl := util.IfOrElse(len(params) > 0, func() string { return fmt.Sprintf("%s?%s", url, params.Encode()) }, url) - req, _ := http.NewRequest("GET", reqUrl, nil) + req, _ := http.NewRequest("GET", createRequestUrl(url, params), nil) return httpDo[T](req, updateRateLimit, updateRateLimitResetAt, logDebug, config) } @@ -47,8 +46,7 @@ func httpPost[T any]( return body, err } - reqUrl := util.IfOrElse(len(params) > 0, func() string { return fmt.Sprintf("%s?%s", url, params.Encode()) }, url) - req, _ := http.NewRequest("POST", reqUrl, bytes.NewBuffer(payload)) + req, _ := http.NewRequest("POST", createRequestUrl(url, params), bytes.NewBuffer(payload)) return httpDo[T](req, updateRateLimit, updateRateLimitResetAt, logDebug, config) } @@ -61,46 +59,30 @@ func httpDo[T any]( ) (T, error) { logDebug("executing request", "method", request.Method, "url", request.URL.String()) - var data T + var empty T if err := applyHeaders(request, config); err != nil { - return data, err + return empty, err } response, err := client.Do(request) if err != nil { - return data, err + return empty, err } + defer response.Body.Close() - for key, value := range response.Header { - if key == headerRatelimit { - if len(value) == 0 { - return data, fmt.Errorf("header: %s didn't contain a value", headerRatelimit) - } - updateRateLimit(util.MustInt64(value[0])) - } - if key == headerRatelimitResetAt { - if len(value) == 0 { - return data, fmt.Errorf("header: %s didn't contain a value", headerRatelimitResetAt) - } - updateRateLimitResetAt(time.UnixMilli(util.MustInt64(value[0]))) - } + if err := updateRateLimits(response, updateRateLimit, updateRateLimitResetAt); err != nil { + return empty, err } if response.StatusCode > http.StatusIMUsed { - bytes, err := io.ReadAll(response.Body) - if err != nil { - return data, err - } - - var apiError *jsond.BitvavoErr - if err := json.Unmarshal(bytes, &apiError); err != nil { - return data, fmt.Errorf("did not get OK response, code=%d, body=%s", response.StatusCode, string(bytes)) - } - return data, apiError + return empty, unwrapErr(response) } - defer response.Body.Close() + return unwrapBody[T](response) +} +func unwrapBody[T any](response *http.Response) (T, error) { + var data T bytes, err := io.ReadAll(response.Body) if err != nil { return data, err @@ -113,6 +95,41 @@ func httpDo[T any]( return data, nil } +func unwrapErr(response *http.Response) error { + bytes, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + var bitvavoErr *jsond.BitvavoErr + if err := json.Unmarshal(bytes, &bitvavoErr); err != nil { + return fmt.Errorf("did not get OK response, code=%d, body=%s", response.StatusCode, string(bytes)) + } + return bitvavoErr +} + +func updateRateLimits( + response *http.Response, + updateRateLimit func(ratelimit int64), + updateRateLimitResetAt func(resetAt time.Time), +) error { + for key, value := range response.Header { + if key == headerRatelimit { + if len(value) == 0 { + return fmt.Errorf("header: %s didn't contain a value", headerRatelimit) + } + updateRateLimit(util.MustInt64(value[0])) + } + if key == headerRatelimitResetAt { + if len(value) == 0 { + return fmt.Errorf("header: %s didn't contain a value", headerRatelimitResetAt) + } + updateRateLimitResetAt(time.UnixMilli(util.MustInt64(value[0]))) + } + } + return nil +} + func applyHeaders(request *http.Request, config *authConfig) error { if config == nil { return nil @@ -137,3 +154,7 @@ func applyHeaders(request *http.Request, config *authConfig) error { return nil } + +func createRequestUrl(url string, params url.Values) string { + return util.IfOrElse(len(params) > 0, func() string { return fmt.Sprintf("%s?%s", url, params.Encode()) }, url) +} diff --git a/httpc/httpclient.go b/httpc/httpclient.go index b670c22..590b214 100644 --- a/httpc/httpclient.go +++ b/httpc/httpclient.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/larscom/go-bitvavo/v2/jsond" "github.com/larscom/go-bitvavo/v2/log" "github.com/larscom/go-bitvavo/v2/util" ) @@ -38,12 +39,22 @@ type HttpClient interface { GetTime() (int64, error) // GetRateLimit returns the remaining rate limit. + // // Default value: -1 GetRateLimit() int64 // GetRateLimitResetAt returns the time (local time) when the counter resets. + // // Default value: time.Now() GetRateLimitResetAt() time.Time + + // GetMarkets returns the available markets with their status (trading,halted,auction) and + // available order types. + GetMarkets() ([]jsond.Market, error) + + // GetMarkets returns the available markets with their status (trading,halted,auction) and + // available order types for a single market (e.g: ETH-EUR) + GetMarket(market string) (jsond.Market, error) } type Option func(*httpClient) @@ -126,6 +137,31 @@ func (c *httpClient) GetTime() (int64, error) { return int64(resp["time"]), nil } +func (c *httpClient) GetMarkets() ([]jsond.Market, error) { + return httpGet[[]jsond.Market]( + fmt.Sprintf("%s/markets", httpUrl), + make(url.Values), + c.updateRateLimit, + c.updateRateLimitResetAt, + c.logDebug, + nil, + ) +} + +func (c *httpClient) GetMarket(market string) (jsond.Market, error) { + params := make(url.Values) + params.Add("market", market) + + return httpGet[jsond.Market]( + fmt.Sprintf("%s/markets", httpUrl), + params, + c.updateRateLimit, + c.updateRateLimitResetAt, + c.logDebug, + nil, + ) +} + func (c *httpClient) updateRateLimit(ratelimit int64) { c.mu.Lock() defer c.mu.Unlock() diff --git a/httpc/httpclientauth.go b/httpc/httpclientauth.go index a7df4b9..3192d22 100644 --- a/httpc/httpclientauth.go +++ b/httpc/httpclientauth.go @@ -4,14 +4,14 @@ import ( "fmt" "time" - neturl "net/url" + "net/url" "github.com/larscom/go-bitvavo/v2/jsond" ) type HttpClientAuth interface { // GetBalance returns the balance on the account. - // Optionally provide the symbol to filter for. + // Optionally provide the symbol to filter for in uppercase (e.g: ETH) GetBalance(symbol ...string) ([]jsond.Balance, error) // GetAccount returns trading volume and fees for account. @@ -46,7 +46,7 @@ func newHttpClientAuth( } func (c *httpClientAuth) GetBalance(symbol ...string) ([]jsond.Balance, error) { - params := make(neturl.Values) + params := make(url.Values) if len(symbol) > 0 { params.Add("symbol", symbol[0]) } @@ -63,7 +63,7 @@ func (c *httpClientAuth) GetBalance(symbol ...string) ([]jsond.Balance, error) { func (c *httpClientAuth) GetAccount() (jsond.Account, error) { return httpGet[jsond.Account]( fmt.Sprintf("%s/account", httpUrl), - make(neturl.Values), + make(url.Values), c.updateRateLimit, c.updateRateLimitResetAt, c.logDebug, diff --git a/jsond/market.go b/jsond/market.go new file mode 100644 index 0000000..447a861 --- /dev/null +++ b/jsond/market.go @@ -0,0 +1,80 @@ +package jsond + +import ( + "github.com/goccy/go-json" + "github.com/larscom/go-bitvavo/v2/util" +) + +type Market struct { + // The market itself + Market string `json:"market"` + + // Enum: "trading" | "halted" | "auction" + Status string `json:"status"` + + // Base currency, found on the left side of the dash in market. + Base string `json:"base"` + + // Quote currency, found on the right side of the dash in market. + Quote string `json:"quote"` + + // Price precision determines how many significant digits are allowed. The rationale behind this is that for higher amounts, smaller price increments are less relevant. + // Examples of valid prices for precision 5 are: 100010, 11313, 7500.10, 7500.20, 500.12, 0.0012345. + // Examples of precision 6 are: 11313.1, 7500.11, 7500.25, 500.123, 0.00123456. + PricePrecision int64 `json:"pricePrecision"` + + // The minimum amount in quote currency (amountQuote or amount * price) for valid orders. + MinOrderInBaseAsset float64 `json:"minOrderInBaseAsset"` + + // The minimum amount in base currency for valid orders. + MinOrderInQuoteAsset float64 `json:"minOrderInQuoteAsset"` + + // // The maximum amount in quote currency (amountQuote or amount * price) for valid orders. + MaxOrderInBaseAsset float64 `json:"maxOrderInBaseAsset"` + + // The maximum amount in base currency for valid orders. + MaxOrderInQuoteAsset float64 `json:"maxOrderInQuoteAsset"` + + // Allowed order types for this market. + OrderTypes []string `json:"orderTypes"` +} + +func (m *Market) UnmarshalJSON(bytes []byte) error { + var j map[string]any + + err := json.Unmarshal(bytes, &j) + if err != nil { + return err + } + + var ( + market = j["market"].(string) + status = j["status"].(string) + base = j["base"].(string) + quote = j["quote"].(string) + pricePrecision = j["pricePrecision"].(float64) + minOrderInBaseAsset = j["minOrderInBaseAsset"].(string) + minOrderInQuoteAsset = j["minOrderInQuoteAsset"].(string) + maxOrderInBaseAsset = j["maxOrderInBaseAsset"].(string) + maxOrderInQuoteAsset = j["maxOrderInQuoteAsset"].(string) + orderTypesAny = j["orderTypes"].([]any) + ) + + orderTypes := make([]string, len(orderTypesAny)) + for i := 0; i < len(orderTypesAny); i++ { + orderTypes[i] = orderTypesAny[i].(string) + } + + m.Market = market + m.Status = status + m.Base = base + m.Quote = quote + m.PricePrecision = int64(pricePrecision) + m.MinOrderInBaseAsset = util.MustFloat64(minOrderInBaseAsset) + m.MinOrderInQuoteAsset = util.MustFloat64(minOrderInQuoteAsset) + m.MaxOrderInBaseAsset = util.MustFloat64(maxOrderInBaseAsset) + m.MaxOrderInQuoteAsset = util.MustFloat64(maxOrderInQuoteAsset) + m.OrderTypes = orderTypes + + return nil +}