Files
gocryptotrader/exchanges/bybit/bybit.go
Jaydeep Rajpurohit 247da918a8 exchanges: Add ByBit support (#887)
* few fixes and add ratelimiter

* adds test

* revert configtest.json changes

* configtest updated

* WIP: adds public endpoint support

* WIP: adds public endpoint support

* adds public endpoint support

* WIP: adds auth. endpoint support

* adds test for auth. endpoint

* fixes

* adds auth. endpoint support

* WIP: ws support

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* Testing

* Complete WS spot testing

* adds support for ws events

* minor change

* WIP: adds REST support for CoinMarginedFutures

* Fixes

* WIP: adds REST support for CoinMarginedFutures

* Fixes

* improvement in SPOT REST

* Typo fix

* WIP: add REST support for CMF Account API

* minor fixes

* WIP: add support for CMF conditional orders and few minor fixes

* complete support for CMF conditional orders

* adds support for public CMF endpoint

* adds support for CMF position API

* Complete REST CMF support

* WIP

* Testing REST CMF support

* Testing REST CMF support

* Testing REST CMF support completed

* WIP: add support for UMF

* completed non-auth UMF

* WIP: add support for REST Auth. UMF

* WIP: add support for REST Auth. UMF and some improvements

* WIP

* WIP

* WIP

* completed REST UMF

* renaming

* adds REST support for futures

* add testcases for UMF and some optimizations

* add testcases for futures

* Testing UMF, futures and its changes

* Fixes

* Fixes after testing

* WIP

* WIP

* WIP

* completed ws USDT futures support

* WIP: ws support for futures

* fixes in WS futures

* fixes in WS support

* roll back changes made for WS CMF, USDT and Futures

* fixes

* WIP

* WIP

* fixes

* Steps for new PR

* WIP

* WIP

* WIP

* WIP

* complete PR setup

* fixes for successfully running tests

* update in symbol for futures pair in test file

* WIP

* Fixes in test file and other minor fix

* fix testdata/configtest.json

* reset CONTRIBUTORS file

* review changes

* remove unwanted file

* remove redundant code

* improvisation

* adds comment for exported functions

* remove unwanted TODO and commented code

* fix

* improvisation

* fix

* defined errors

* improvisation

* improvisation

* improvisation

* updates test

* adds comment for exported types

* review changes

* review changes

* fix

* fixes

* Changes for making BYBIT compatible with existing code base

* Test file changes

* Changes for making BYBIT compatible with existing code base

* Changes for making BYBIT compatible with existing code base

* fix lint issues

* fix

* review changes

* review changes

* review changes

* review changes

* review changes

* review changes

* review changes

* review changes

* review changes

* review changes

* WIP

* add test cases for new API's

* minor improvements

* add missing API and their tests

* minor fixes

* add bybitTime

* add bybitTimeSec, bybitTimeMilliSec, bybitTimeNanoSec and necessary support

* fix GetTradeHistory function

* error handling

* test fixes

* add GetServerTime API

* adds GetHistoricCandlesExtended and review changes

* test fixes

* minor fix

* integrating CMF Bybit recent change log

* minor fixes

* adds extractCurrencyPair

* minor fixes

* minor fix

* review changes

* adds variable declaration of error

* review commit

* adds embeddable type in API response for all API and integrate it

* fixes

* adds authentication WS connection

* review changes

* review changes

* compatible changes

* adds asset to GetWithdrawalsHistory

* adds asset_type in rpc.proto

* adds asset argument in gctcli withdrawal request command

* improve error handling in exchange API error

* web socket fix

* review changes

* improvements

* improvements

* minor fix

* review changes

* fixing wrapper issues

* fixes

* fixes

* review changes

* add test cases

* fix for GetActiveOrders

* lint fixes

* fixes in websocket

* adds wrapper testcases

* adds wrapper testcases

* adds wrapper testcases

* fixes

* fix issue with GetHistoricCandlesExtended

* fix merge issues

* improving error reporting

* adds wrapper testcases and a minor fix

* gctrpc changes

* adds test cases
fixes in websocket

* review changes for ws

* review changes in WS

* fix gctrpc

* merge fixes

* review changes

* WIP

* updates pair in configs

* adds new asset USDCMarginedFutures

* adds URL const for USDCMarginedFutures

* adds API support

* minor fixes

* adds kline API

* minor fix

* adds API

* adds API

* adds API

* WIP

* WIP

* WIP

* adds support for USDC auth requests to SendAuthHTTPRequest

* adds SendUSDCAuthHTTPRequest

* run test and fix them

* rollback support added for Auth. USDC request inside SendAuthHTTPRequest

* adds API and test cases

* adds API and test cases

* adds APIs and test cases

* adds APIs

* adds rate limit for USDC

* adds USDCMarginedFutures to wrapper

* adds USDC testcases in wrapper and fix few issues

* minor test fixes

* minor test fixes

* fix lint issues

* WIP

* Merge changes

* minor fixes

* remove "else" and optimize

* review changes

* review changes

* review changes

* fix lint issue

* merge fix

* fix test

* fix templates and run them

* changes after merge

* review changes and improvements

* code improvement

* fixes with respect to changes in API response in documentation

* fixed review change in test

* adds check in CancelExistingOrder

* update exchange template

* review changes

* adds GetDepositAddress API

* WIP: adds GetOrderHistory

* complete GetOrderHistory

* fixes

* adds test case

* fixes and add WithdrawFund API

* WIP

* WIP

* updating all SendAuthHTTPRequest call

* adds WithdrawCryptocurrencyFunds

* update test cases

* fix lint issues

* fixes after merge

* adds GetAvailableTransferChains and few fixes

* minor fix in GetDepositAddress

* minor fix with WS ping/pong handling

* add ping handler for WS Auth.

* fix typo mistake

* update doc
2022-08-08 11:29:43 +10:00

999 lines
28 KiB
Go

package bybit
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// Bybit is the overarching type across this package
type Bybit struct {
exchange.Base
}
const (
bybitAPIURL = "https://api.bybit.com"
defaultRecvWindow = "5000" // 5000 milli second
sideBuy = "Buy"
sideSell = "Sell"
// Public endpoints
bybitSpotGetSymbols = "/spot/v1/symbols"
bybitOrderBook = "/spot/quote/v1/depth"
bybitMergedOrderBook = "/spot/quote/v1/depth/merged"
bybitRecentTrades = "/spot/quote/v1/trades"
bybitCandlestickChart = "/spot/quote/v1/kline"
bybit24HrsChange = "/spot/quote/v1/ticker/24hr"
bybitLastTradedPrice = "/spot/quote/v1/ticker/price"
bybitBestBidAskPrice = "/spot/quote/v1/ticker/book_ticker"
// Authenticated endpoints
bybitSpotOrder = "/spot/v1/order" // create, query, cancel
bybitFastCancelSpotOrder = "/spot/v1/order/fast"
bybitBatchCancelSpotOrder = "/spot/order/batch-cancel"
bybitFastBatchCancelSpotOrder = "/spot/order/batch-fast-cancel"
bybitOpenOrder = "/spot/v1/open-orders"
bybitPastOrder = "/spot/v1/history-orders"
bybitTradeHistory = "/spot/v1/myTrades"
bybitWalletBalance = "/spot/v1/account"
bybitServerTime = "/spot/v1/time"
// Account asset endpoint
bybitGetDepositAddress = "/asset/v1/private/deposit/address"
bybitWithdrawFund = "/asset/v1/private/withdraw"
)
// GetAllSpotPairs gets all pairs on the exchange
func (by *Bybit) GetAllSpotPairs(ctx context.Context) ([]PairData, error) {
resp := struct {
Data []PairData `json:"result"`
Error
}{}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestSpot, bybitSpotGetSymbols, publicSpotRate, &resp)
}
func processOB(ob [][2]string) ([]orderbook.Item, error) {
o := make([]orderbook.Item, len(ob))
for x := range ob {
var price, amount float64
amount, err := strconv.ParseFloat(ob[x][1], 64)
if err != nil {
return nil, err
}
price, err = strconv.ParseFloat(ob[x][0], 64)
if err != nil {
return nil, err
}
o[x] = orderbook.Item{
Price: price,
Amount: amount,
}
}
return o, nil
}
func constructOrderbook(o *orderbookResponse) (*Orderbook, error) {
var (
s Orderbook
err error
)
s.Bids, err = processOB(o.Data.Bids)
if err != nil {
return nil, err
}
s.Asks, err = processOB(o.Data.Asks)
if err != nil {
return nil, err
}
s.Time = o.Data.Time.Time()
return &s, err
}
// GetOrderBook gets orderbook for a given market with a given depth (default depth 100)
func (by *Bybit) GetOrderBook(ctx context.Context, symbol string, depth int64) (*Orderbook, error) {
var o orderbookResponse
strDepth := "100" // default depth
if depth > 0 && depth < 100 {
strDepth = strconv.FormatInt(depth, 10)
}
params := url.Values{}
params.Set("symbol", symbol)
params.Set("limit", strDepth)
path := common.EncodeURLValues(bybitOrderBook, params)
err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &o)
if err != nil {
return nil, err
}
return constructOrderbook(&o)
}
// GetMergedOrderBook gets orderbook for a given market with a given depth (default depth 100)
func (by *Bybit) GetMergedOrderBook(ctx context.Context, symbol string, scale, depth int64) (*Orderbook, error) {
var o orderbookResponse
params := url.Values{}
if scale > 0 {
params.Set("scale", strconv.FormatInt(scale, 10))
}
strDepth := "100" // default depth
if depth > 0 && depth <= 200 {
strDepth = strconv.FormatInt(depth, 10)
}
params.Set("symbol", symbol)
params.Set("limit", strDepth)
path := common.EncodeURLValues(bybitMergedOrderBook, params)
err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &o)
if err != nil {
return nil, err
}
return constructOrderbook(&o)
}
// GetTrades gets recent trades from the exchange
func (by *Bybit) GetTrades(ctx context.Context, symbol string, limit int64) ([]TradeItem, error) {
resp := struct {
Data []struct {
Price float64 `json:"price,string"`
Time bybitTimeMilliSec `json:"time"`
Quantity float64 `json:"qty,string"`
IsBuyerMaker bool `json:"isBuyerMaker"`
} `json:"result"`
Error
}{}
params := url.Values{}
params.Set("symbol", symbol)
strLimit := "60" // default limit
if limit > 0 && limit < 60 {
strLimit = strconv.FormatInt(limit, 10)
}
params.Set("limit", strLimit)
path := common.EncodeURLValues(bybitRecentTrades, params)
err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
if err != nil {
return nil, err
}
trades := make([]TradeItem, len(resp.Data))
for x := range resp.Data {
var tradeSide string
if resp.Data[x].IsBuyerMaker {
tradeSide = order.Buy.String()
} else {
tradeSide = order.Sell.String()
}
trades[x] = TradeItem{
CurrencyPair: symbol,
Price: resp.Data[x].Price,
Side: tradeSide,
Volume: resp.Data[x].Quantity,
Time: resp.Data[x].Time.Time(),
}
}
return trades, nil
}
// GetKlines data returns the kline data for a specific symbol. Limitation: It only returns latest 3500 candles irrespective of interval passed
func (by *Bybit) GetKlines(ctx context.Context, symbol, period string, limit int64, start, end time.Time) ([]KlineItem, error) {
resp := struct {
Data [][]interface{} `json:"result"`
Error
}{}
v := url.Values{}
v.Add("symbol", symbol)
v.Add("interval", period)
if !start.IsZero() {
v.Add("startTime", strconv.FormatInt(start.UnixMilli(), 10))
}
if !end.IsZero() {
v.Add("endTime", strconv.FormatInt(end.UnixMilli(), 10))
}
if limit <= 0 || limit > 1000 {
limit = 1000
}
v.Add("limit", strconv.FormatInt(limit, 10))
path := common.EncodeURLValues(bybitCandlestickChart, v)
if err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp); err != nil {
return nil, err
}
klines := make([]KlineItem, len(resp.Data))
for x := range resp.Data {
if len(resp.Data[x]) != 11 {
return klines, fmt.Errorf("%v GetKlines: invalid response, array length not as expected, check api docs for updates", by.Name)
}
var kline KlineItem
var err error
startTime, ok := resp.Data[x][0].(float64)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for StartTime", by.Name, errTypeAssert)
}
kline.StartTime = time.UnixMilli(int64(startTime))
open, ok := resp.Data[x][1].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for Open", by.Name, errTypeAssert)
}
kline.Open, err = strconv.ParseFloat(open, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for Open", by.Name, errStrParsing)
}
high, ok := resp.Data[x][2].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for High", by.Name, errTypeAssert)
}
kline.High, err = strconv.ParseFloat(high, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for High", by.Name, errStrParsing)
}
low, ok := resp.Data[x][3].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for Low", by.Name, errTypeAssert)
}
kline.Low, err = strconv.ParseFloat(low, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for Low", by.Name, errStrParsing)
}
c, ok := resp.Data[x][4].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for Close", by.Name, errTypeAssert)
}
kline.Close, err = strconv.ParseFloat(c, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for Close", by.Name, errStrParsing)
}
volume, ok := resp.Data[x][5].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for Volume", by.Name, errTypeAssert)
}
kline.Volume, err = strconv.ParseFloat(volume, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for Volume", by.Name, errStrParsing)
}
endTime, ok := resp.Data[x][6].(float64)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for EndTime", by.Name, errTypeAssert)
}
kline.EndTime = time.UnixMilli(int64(endTime))
quoteAssetVolume, ok := resp.Data[x][7].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for QuoteAssetVolume", by.Name, errTypeAssert)
}
kline.QuoteAssetVolume, err = strconv.ParseFloat(quoteAssetVolume, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for QuoteAssetVolume", by.Name, errStrParsing)
}
tradesCount, ok := resp.Data[x][8].(float64)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for TradesCount", by.Name, errTypeAssert)
}
kline.TradesCount = int64(tradesCount)
takerBaseVolume, ok := resp.Data[x][9].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for TakerBaseVolume", by.Name, errTypeAssert)
}
kline.TakerBaseVolume, err = strconv.ParseFloat(takerBaseVolume, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for TakerBaseVolume", by.Name, errStrParsing)
}
takerQuoteVolume, ok := resp.Data[x][10].(string)
if !ok {
return klines, fmt.Errorf("%v GetKlines: %w for TakerQuoteVolume", by.Name, errTypeAssert)
}
kline.TakerQuoteVolume, err = strconv.ParseFloat(takerQuoteVolume, 64)
if err != nil {
return klines, fmt.Errorf("%v GetKlines: %w for TakerQuoteVolume", by.Name, errStrParsing)
}
klines[x] = kline
}
return klines, nil
}
// Get24HrsChange returns price change statistics for the last 24 hours
// If symbol not passed then it will return price change statistics for all pairs
func (by *Bybit) Get24HrsChange(ctx context.Context, symbol string) ([]PriceChangeStats, error) {
type priceChangeStats struct {
Time bybitTimeMilliSec `json:"time"`
Symbol string `json:"symbol"`
BestBidPrice float64 `json:"bestBidPrice,string"`
BestAskPrice float64 `json:"bestAskPrice,string"`
LastPrice float64 `json:"lastPrice,string"`
OpenPrice float64 `json:"openPrice,string"`
HighPrice float64 `json:"highPrice,string"`
LowPrice float64 `json:"lowPrice,string"`
Volume float64 `json:"volume,string"`
QuoteVolume float64 `json:"quoteVolume,string"`
}
var stats []PriceChangeStats
if symbol != "" {
resp := struct {
Data priceChangeStats `json:"result"`
Error
}{}
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(bybit24HrsChange, params)
err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
if err != nil {
return nil, err
}
stats = append(stats, PriceChangeStats{
resp.Data.Time.Time(),
resp.Data.Symbol,
resp.Data.BestAskPrice,
resp.Data.BestAskPrice,
resp.Data.LastPrice,
resp.Data.OpenPrice,
resp.Data.HighPrice,
resp.Data.LowPrice,
resp.Data.Volume,
resp.Data.QuoteVolume,
})
} else {
resp := struct {
Data []priceChangeStats `json:"result"`
Error
}{}
err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybit24HrsChange, publicSpotRate, &resp)
if err != nil {
return nil, err
}
for x := range resp.Data {
stats = append(stats, PriceChangeStats{
resp.Data[x].Time.Time(),
resp.Data[x].Symbol,
resp.Data[x].BestAskPrice,
resp.Data[x].BestAskPrice,
resp.Data[x].LastPrice,
resp.Data[x].OpenPrice,
resp.Data[x].HighPrice,
resp.Data[x].LowPrice,
resp.Data[x].Volume,
resp.Data[x].QuoteVolume,
})
}
}
return stats, nil
}
// GetLastTradedPrice returns last trading price
// If symbol not passed then it will return last trading price for all pairs
func (by *Bybit) GetLastTradedPrice(ctx context.Context, symbol string) ([]LastTradePrice, error) {
var lastTradePrices []LastTradePrice
if symbol != "" {
resp := struct {
Data LastTradePrice `json:"result"`
Error
}{}
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(bybitLastTradedPrice, params)
err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
if err != nil {
return nil, err
}
lastTradePrices = append(lastTradePrices, LastTradePrice{
resp.Data.Symbol,
resp.Data.Price,
})
} else {
resp := struct {
Data []LastTradePrice `json:"result"`
Error
}{}
err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybitLastTradedPrice, publicSpotRate, &resp)
if err != nil {
return nil, err
}
for x := range resp.Data {
lastTradePrices = append(lastTradePrices, LastTradePrice{
resp.Data[x].Symbol,
resp.Data[x].Price,
})
}
}
return lastTradePrices, nil
}
// GetBestBidAskPrice returns best BID and ASK price
// If symbol not passed then it will return best BID and ASK price for all pairs
func (by *Bybit) GetBestBidAskPrice(ctx context.Context, symbol string) ([]TickerData, error) {
type bestTicker struct {
Symbol string `json:"symbol"`
BidPrice float64 `json:"bidPrice,string"`
BidQuantity float64 `json:"bidQty,string"`
AskPrice float64 `json:"askPrice,string"`
AskQuantity float64 `json:"askQty,string"`
Time bybitTimeMilliSec `json:"time"`
}
var tickers []TickerData
if symbol != "" {
resp := struct {
Data bestTicker `json:"result"`
Error
}{}
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(bybitBestBidAskPrice, params)
err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
if err != nil {
return nil, err
}
tickers = append(tickers, TickerData{
resp.Data.Symbol,
resp.Data.BidPrice,
resp.Data.BidQuantity,
resp.Data.AskPrice,
resp.Data.AskQuantity,
resp.Data.Time.Time(),
})
} else {
resp := struct {
Data []bestTicker `json:"result"`
Error
}{}
err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybitBestBidAskPrice, publicSpotRate, &resp)
if err != nil {
return nil, err
}
for x := range resp.Data {
tickers = append(tickers, TickerData{
resp.Data[x].Symbol,
resp.Data[x].BidPrice,
resp.Data[x].BidQuantity,
resp.Data[x].AskPrice,
resp.Data[x].AskQuantity,
resp.Data[x].Time.Time(),
})
}
}
return tickers, nil
}
// CreatePostOrder create and post order
func (by *Bybit) CreatePostOrder(ctx context.Context, o *PlaceOrderRequest) (*PlaceOrderResponse, error) {
if o == nil {
return nil, errInvalidOrderRequest
}
params := url.Values{}
params.Set("symbol", o.Symbol)
params.Set("qty", strconv.FormatFloat(o.Quantity, 'f', -1, 64))
params.Set("side", o.Side)
params.Set("type", o.TradeType)
if o.TimeInForce != "" {
params.Set("timeInForce", o.TimeInForce)
}
if (o.TradeType == BybitRequestParamsOrderLimit || o.TradeType == BybitRequestParamsOrderLimitMaker) && o.Price == 0 {
return nil, errMissingPrice
}
if o.Price != 0 {
params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64))
}
if o.OrderLinkID != "" {
params.Set("orderLinkId", o.OrderLinkID)
}
resp := struct {
Data PlaceOrderResponse `json:"result"`
Error
}{}
return &resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, bybitSpotOrder, params, nil, &resp, privateSpotRate)
}
// QueryOrder returns order data based upon orderID or orderLinkID
func (by *Bybit) QueryOrder(ctx context.Context, orderID, orderLinkID string) (*QueryOrderResponse, error) {
if orderID == "" && orderLinkID == "" {
return nil, errOrderOrOrderLinkIDMissing
}
params := url.Values{}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
resp := struct {
Data QueryOrderResponse `json:"result"`
Error
}{}
return &resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitSpotOrder, params, nil, &resp, privateSpotRate)
}
// CancelExistingOrder cancels existing order based upon orderID or orderLinkID
func (by *Bybit) CancelExistingOrder(ctx context.Context, orderID, orderLinkID string) (*CancelOrderResponse, error) {
if orderID == "" && orderLinkID == "" {
return nil, errOrderOrOrderLinkIDMissing
}
params := url.Values{}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
resp := struct {
Data CancelOrderResponse `json:"result"`
Error
}{}
err := by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitSpotOrder, params, nil, &resp, privateSpotRate)
if err != nil {
return nil, err
}
// In case open order is cancelled, this endpoint return status as NEW whereas if we try to cancel a already cancelled order then it's status is returned as CANCELED without any error. So this check is added to prevent this obscurity.
if resp.Data.Status == "CANCELED" {
return nil, fmt.Errorf("%s order already cancelled", resp.Data.OrderID)
}
return &resp.Data, nil
}
// FastCancelExistingOrder cancels existing order based upon orderID or orderLinkID
func (by *Bybit) FastCancelExistingOrder(ctx context.Context, symbol, orderID, orderLinkID string) (bool, error) {
resp := struct {
Data struct {
IsCancelled bool `json:"isCancelled"`
} `json:"result"`
Error
}{}
if orderID == "" && orderLinkID == "" {
return resp.Data.IsCancelled, errOrderOrOrderLinkIDMissing
}
params := url.Values{}
if symbol == "" {
return resp.Data.IsCancelled, errSymbolMissing
}
params.Set("symbolId", symbol)
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
return resp.Data.IsCancelled, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitFastCancelSpotOrder, params, nil, &resp, privateSpotRate)
}
// BatchCancelOrder cancels orders in batch based upon symbol, side or orderType
func (by *Bybit) BatchCancelOrder(ctx context.Context, symbol, side, orderTypes string) (bool, error) {
params := url.Values{}
if symbol != "" {
params.Set("symbol", symbol)
}
if side != "" {
params.Set("side", side)
}
if orderTypes != "" {
params.Set("orderTypes", orderTypes)
}
resp := struct {
Result struct {
Success bool `json:"success"`
} `json:"result"`
Error
}{}
return resp.Result.Success, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitBatchCancelSpotOrder, params, nil, &resp, privateSpotRate)
}
// BatchFastCancelOrder cancels orders in batch based upon symbol, side or orderType
func (by *Bybit) BatchFastCancelOrder(ctx context.Context, symbol, side, orderTypes string) (bool, error) {
params := url.Values{}
if symbol != "" {
params.Set("symbol", symbol)
}
if side != "" {
params.Set("side", side)
}
if orderTypes != "" {
params.Set("orderTypes", orderTypes)
}
resp := struct {
Result struct {
Success bool `json:"success"`
} `json:"result"`
Error
}{}
return resp.Result.Success, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitFastBatchCancelSpotOrder, params, nil, &resp, privateSpotRate)
}
// BatchCancelOrderByIDs cancels orders in batch based on comma separated order id's
func (by *Bybit) BatchCancelOrderByIDs(ctx context.Context, orderIDs []string) (bool, error) {
params := url.Values{}
if len(orderIDs) == 0 {
return false, errEmptyOrderIDs
}
params.Set("orderIds", strings.Join(orderIDs, ","))
resp := struct {
Result struct {
Success bool `json:"success"`
} `json:"result"`
Error
}{}
return resp.Result.Success, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitFastBatchCancelSpotOrder, params, nil, &resp, privateSpotRate)
}
// ListOpenOrders returns all open orders
func (by *Bybit) ListOpenOrders(ctx context.Context, symbol, orderID string, limit int64) ([]QueryOrderResponse, error) {
params := url.Values{}
if symbol != "" {
params.Set("symbol", symbol)
}
if orderID != "" {
params.Set("orderId", orderID)
}
if limit != 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
Data []QueryOrderResponse `json:"result"`
Error
}{}
return resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitOpenOrder, params, nil, &resp, privateSpotRate)
}
// GetPastOrders returns all past orders from history
func (by *Bybit) GetPastOrders(ctx context.Context, symbol, orderID string, limit int64, startTime, endTime time.Time) ([]QueryOrderResponse, error) {
params := url.Values{}
if symbol != "" {
params.Set("symbol", symbol)
}
if orderID != "" {
params.Set("orderId", orderID)
}
if limit != 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
resp := struct {
Data []QueryOrderResponse `json:"result"`
Error
}{}
return resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitPastOrder, params, nil, &resp, privateSpotRate)
}
// GetTradeHistory returns user trades
func (by *Bybit) GetTradeHistory(ctx context.Context, limit int64, symbol, fromID, toID, orderID string, startTime, endTime time.Time) ([]HistoricalTrade, error) {
params := url.Values{}
if symbol != "" {
params.Set("symbol", symbol)
}
if limit != 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if fromID != "" {
params.Set("fromTicketId", fromID)
}
if toID != "" {
params.Set("toTicketId", toID)
}
if orderID != "" {
params.Set("orderId", orderID)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
resp := struct {
Data []HistoricalTrade `json:"result"`
Error
}{}
return resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitTradeHistory, params, nil, &resp, privateSpotRate)
}
// GetWalletBalance returns user wallet balance
func (by *Bybit) GetWalletBalance(ctx context.Context) ([]Balance, error) {
resp := struct {
Data struct {
Balances []Balance `json:"balances"`
} `json:"result"`
Error
}{}
return resp.Data.Balances, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitWalletBalance, url.Values{}, nil, &resp, privateSpotRate)
}
// GetSpotServerTime returns server time
func (by *Bybit) GetSpotServerTime(ctx context.Context) (time.Time, error) {
resp := struct {
Result struct {
ServerTime int64 `json:"serverTime"`
} `json:"result"`
Error
}{}
err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybitServerTime, publicSpotRate, &resp)
return time.UnixMilli(resp.Result.ServerTime), err
}
// GetDepositAddressForCurrency returns deposit wallet address based upon the coin.
func (by *Bybit) GetDepositAddressForCurrency(ctx context.Context, coin string) (DepositWalletInfo, error) {
resp := struct {
Result DepositWalletInfo `json:"result"`
Error
}{}
params := url.Values{}
if coin == "" {
return resp.Result, errInvalidCoin
}
params.Set("coin", strings.ToUpper(coin))
return resp.Result, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitGetDepositAddress, params, nil, &resp, publicSpotRate)
}
// WithdrawFund creates request for fund withdrawal.
func (by *Bybit) WithdrawFund(ctx context.Context, coin, chain, address, tag, amount string) (string, error) {
resp := struct {
Data struct {
ID string `json:"id"`
} `json:"result"`
Error
}{}
params := make(map[string]interface{})
params["coin"] = coin
params["chain"] = chain
params["address"] = address
params["amount"] = amount
if tag != "" {
params["tag"] = tag
}
return resp.Data.ID, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, bybitWithdrawFund, nil, params, &resp, privateSpotRate)
}
// SendHTTPRequest sends an unauthenticated request
func (by *Bybit) SendHTTPRequest(ctx context.Context, ePath exchange.URL, path string, f request.EndpointLimit, result UnmarshalTo) error {
endpointPath, err := by.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
err = by.SendPayload(ctx, f, func() (*request.Item, error) {
return &request.Item{
Method: http.MethodGet,
Path: endpointPath + path,
Result: result,
Verbose: by.Verbose,
HTTPDebugging: by.HTTPDebugging,
HTTPRecording: by.HTTPRecording}, nil
})
if err != nil {
return err
}
return result.GetError()
}
// SendAuthHTTPRequest sends an authenticated HTTP request
// If payload is non-nil then request is considered to be JSON
func (by *Bybit) SendAuthHTTPRequest(ctx context.Context, ePath exchange.URL, method, path string, params url.Values, jsonPayload map[string]interface{}, result UnmarshalTo, f request.EndpointLimit) error {
creds, err := by.GetCredentials(ctx)
if err != nil {
return err
}
if result == nil {
result = &Error{}
}
endpointPath, err := by.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
if params == nil && jsonPayload == nil {
params = url.Values{}
}
if jsonPayload != nil {
jsonPayload["recvWindow"] = defaultRecvWindow
} else if params.Get("recvWindow") == "" {
params.Set("recvWindow", defaultRecvWindow)
}
err = by.SendPayload(ctx, f, func() (*request.Item, error) {
var (
payload []byte
hmacSignedStr string
)
headers := make(map[string]string)
if jsonPayload != nil {
headers["Content-Type"] = "application/json"
jsonPayload["timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
jsonPayload["api_key"] = creds.Key
hmacSignedStr, err = getJSONRequestSignature(jsonPayload, creds.Secret)
if err != nil {
return nil, err
}
jsonPayload["sign"] = hmacSignedStr
payload, err = json.Marshal(jsonPayload)
if err != nil {
return nil, err
}
} else {
params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10))
params.Set("api_key", creds.Key)
hmacSignedStr, err = getSign(params.Encode(), creds.Secret)
if err != nil {
return nil, err
}
headers["Content-Type"] = "application/x-www-form-urlencoded"
switch method {
case http.MethodPost:
params.Set("sign", hmacSignedStr)
payload = []byte(params.Encode())
default:
path = common.EncodeURLValues(path, params)
path += "&sign=" + hmacSignedStr
}
}
return &request.Item{
Method: method,
Path: endpointPath + path,
Headers: headers,
Body: bytes.NewBuffer(payload),
Result: &result,
AuthRequest: true,
Verbose: by.Verbose,
HTTPDebugging: by.HTTPDebugging,
HTTPRecording: by.HTTPRecording}, nil
})
if err != nil {
return err
}
return result.GetError()
}
// Error defines all error information for each request
type Error struct {
ReturnCode int64 `json:"ret_code"`
ReturnMsg string `json:"ret_msg"`
ExtCode string `json:"ext_code"`
ExtMsg string `json:"ext_info"`
}
// GetError checks and returns an error if it is supplied.
func (e Error) GetError() error {
if e.ReturnCode != 0 && e.ReturnMsg != "" {
return errors.New(e.ReturnMsg)
}
if e.ExtCode != "" && e.ExtMsg != "" {
return errors.New(e.ExtMsg)
}
return nil
}
func getSide(side string) order.Side {
switch side {
case sideBuy:
return order.Buy
case sideSell:
return order.Sell
default:
return order.UnknownSide
}
}
func getTradeType(tradeType string) order.Type {
switch tradeType {
case BybitRequestParamsOrderLimit:
return order.Limit
case BybitRequestParamsOrderMarket:
return order.Market
case BybitRequestParamsOrderLimitMaker:
return order.Limit
default:
return order.UnknownType
}
}
func getOrderStatus(status string) order.Status {
switch status {
case "NEW":
return order.New
case "PARTIALLY_FILLED":
return order.PartiallyFilled
case "FILLED":
return order.Filled
case "CANCELED":
return order.Cancelled
case "PENDING_CANCEL":
return order.PendingCancel
case "PENDING_NEW":
return order.Pending
case "REJECTED":
return order.Rejected
default:
return order.UnknownStatus
}
}
func getJSONRequestSignature(payload map[string]interface{}, secret string) (string, error) {
payloadArr := make([]string, len(payload))
var i int
for p := range payload {
payloadArr[i] = p
i++
}
sort.Strings(payloadArr)
var signStr string
for _, key := range payloadArr {
if value, found := payload[key]; found {
if v, ok := value.(string); ok {
signStr += key + "=" + v + "&"
}
} else {
return "", errors.New("non-string payload parameter not expected")
}
}
return getSign(signStr[:len(signStr)-1], secret)
}
func getSign(sign, secret string) (string, error) {
hmacSigned, err := crypto.GetHMAC(crypto.HashSHA256, []byte(sign), []byte(secret))
if err != nil {
return "", err
}
return crypto.HexEncodeToString(hmacSigned), nil
}