exchanges: Remove FTX implementation and fix minor test issues (#1100)

* exchanges: Start removal of FTX

* Get tests happy again

* okx: improve logic and add basic coverage

* Fix linterino

* Round 2 plus rm useless assignment in test

* Fix exchange_wrapper_issues test error

* Fix nitters

* Address nitters
This commit is contained in:
Adrian Gallagher
2023-01-25 16:40:04 +11:00
committed by GitHub
parent 05558aabfb
commit c785ae73a7
57 changed files with 211 additions and 9661 deletions

View File

@@ -237,18 +237,18 @@ func (bi *Binanceus) GetAggregateTrades(ctx context.Context, agg *AggregatedTrad
if agg.FromID != 0 {
params.Set("fromId", strconv.FormatInt(agg.FromID, 10))
}
startTime := time.UnixMilli(int64(agg.StartTime))
endTime := time.UnixMilli(int64(agg.EndTime))
startTime := time.UnixMilli(agg.StartTime)
endTime := time.UnixMilli(agg.EndTime)
if (endTime.UnixNano() - startTime.UnixNano()) >= int64(time.Hour) {
endTime = startTime.Add(time.Minute * 59)
}
if !startTime.IsZero() && startTime.Unix() != 0 {
params.Set("startTime", strconv.Itoa(int(agg.StartTime)))
params.Set("startTime", strconv.FormatInt(agg.StartTime, 10))
}
if !endTime.IsZero() && endTime.Unix() != 0 {
params.Set("endTime", strconv.Itoa(int(agg.EndTime)))
params.Set("endTime", strconv.FormatInt(agg.EndTime, 10))
}
needBatch = needBatch || (!startTime.IsZero() && !endTime.IsZero() && endTime.Sub(startTime) > time.Hour)
if needBatch {
@@ -277,8 +277,8 @@ func (bi *Binanceus) batchAggregateTrades(ctx context.Context, arg *AggregatedTr
// Extend from the default of 500
params.Set("limit", "1000")
}
startTime := time.UnixMilli(int64(arg.StartTime))
endTime := time.UnixMilli(int64(arg.EndTime))
startTime := time.UnixMilli(arg.StartTime)
endTime := time.UnixMilli(arg.EndTime)
var fromID int64
if arg.FromID > 0 {
fromID = arg.FromID
@@ -292,8 +292,8 @@ func (bi *Binanceus) batchAggregateTrades(ctx context.Context, arg *AggregatedTr
// All requests returned empty
return nil, nil
}
params.Set("startTime", strconv.Itoa(int(startTime.UnixMilli())))
params.Set("endTime", strconv.Itoa(int(startTime.Add(increment).UnixMilli())))
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
params.Set("endTime", strconv.FormatInt(startTime.Add(increment).UnixMilli(), 10))
path := common.EncodeURLValues(aggregatedTrades, params)
err := bi.SendHTTPRequest(ctx,
exchange.RestSpotSupplementary, path, spotDefaultRate, &resp)

View File

@@ -129,8 +129,8 @@ type AggregatedTradeRequestParams struct {
// The first trade to retrieve
FromID int64
// The API seems to accept (start and end time) or FromID and no other combinations
StartTime uint64
EndTime uint64
StartTime int64
EndTime int64
// Default 500; max 1000.
Limit int
}

View File

@@ -539,8 +539,8 @@ func (bi *Binanceus) GetHistoricTrades(ctx context.Context, p currency.Pair,
assetType asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) {
req := AggregatedTradeRequestParams{
Symbol: p,
StartTime: uint64(timestampStart.UnixMilli()),
EndTime: uint64(timestampEnd.UnixMilli()),
StartTime: timestampStart.UnixMilli(),
EndTime: timestampEnd.UnixMilli(),
}
trades, err := bi.GetAggregateTrades(ctx, &req)
if err != nil {

View File

@@ -2173,12 +2173,18 @@ func TestUpdateTicker(t *testing.T) {
t.Error(err)
}
pair2, err := currency.NewPairFromString("BTCUSD-Z22")
// Futures update dynamically, so fetch the available tradable futures for this test
availPairs, err := b.FetchTradablePairs(context.Background(), asset.Futures)
if err != nil {
t.Fatal(err)
}
_, err = b.UpdateTicker(context.Background(), pair2, asset.Futures)
// Needs to be set before calling extractCurrencyPair
if err = b.SetPairs(availPairs, asset.Futures, true); err != nil {
t.Fatal(err)
}
_, err = b.UpdateTicker(context.Background(), availPairs[0], asset.Futures)
if err != nil {
t.Error(err)
}

View File

@@ -1439,7 +1439,7 @@ func (b *Base) ScaleCollateral(context.Context, *order.CollateralCalculator) (*o
}
// CalculateTotalCollateral takes in n collateral calculators to determine an overall
// standing in a singular currency. See FTX's implementation
// standing in a singular currency
func (b *Base) CalculateTotalCollateral(ctx context.Context, calculator *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error) {
return nil, common.ErrNotYetImplemented
}
@@ -1450,7 +1450,7 @@ func (b *Base) GetCollateralCurrencyForContract(a asset.Item, cp currency.Pair)
}
// GetCurrencyForRealisedPNL returns where to put realised PNL
// example 1: FTX PNL is paid out in USD to your spot wallet
// example 1: Bybit universal margin PNL is paid out in USD to your spot wallet
// example 2: Binance coin margined futures pays returns using the same currency eg BTC
func (b *Base) GetCurrencyForRealisedPNL(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented

View File

@@ -1,133 +0,0 @@
# GoCryptoTrader package Ftx
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/ftx)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This ftx package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## FTX Exchange
### Current Features
+ REST Support
+ Websocket Support
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
+ Individual package example below:
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### How to do REST public/private calls
+ If enabled via "configuration".json file the exchange will be added to the
IBotExchange array in the ```go var bot Bot``` and you will only be able to use
the wrapper interface functions for accessing exchange data. View routines.go
for an example of integration usage with GoCryptoTrader. Rudimentary example
below:
main.go
```go
var f exchange.IBotExchange
for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "FTX" {
f = bot.Exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := f.FetchTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := f.FetchOrderbook()
if err != nil {
// Handle error
}
// Private calls - wrapper functions - make sure your APIKEY and APISECRET are
// set and AuthenticatedAPISupport is set to true
// Fetches current account information
accountInfo, err := f.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := f.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := f.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := f.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := f.Trade(...)
if err != nil {
// Handle error
}
```
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,624 +0,0 @@
package ftx
import (
"context"
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
ftxWSURL = "wss://ftx.com/ws/"
ftxWebsocketTimer = 13 * time.Second
wsTicker = "ticker"
wsTrades = "trades"
wsOrderbook = "orderbook"
wsMarkets = "markets"
wsFills = "fills"
wsOrders = "orders"
wsUpdate = "update"
wsPartial = "partial"
subscribe = "subscribe"
unsubscribe = "unsubscribe"
)
var obSuccess = make(map[currency.Pair]bool)
// WsConnect connects to a websocket feed
func (f *FTX) WsConnect() error {
if !f.Websocket.IsEnabled() || !f.IsEnabled() {
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := f.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
f.Websocket.Conn.SetupPingHandler(stream.PingHandler{
MessageType: websocket.PingMessage,
Delay: ftxWebsocketTimer,
})
if f.Verbose {
log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", f.Name)
}
f.Websocket.Wg.Add(1)
go f.wsReadData()
if f.IsWebsocketAuthenticationSupported() {
err = f.WsAuth(context.TODO())
if err != nil {
f.Websocket.DataHandler <- err
f.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
return nil
}
// WsAuth sends an authentication message to receive auth data
func (f *FTX) WsAuth(ctx context.Context) error {
creds, err := f.GetCredentials(ctx)
if err != nil {
return err
}
intNonce := time.Now().UnixMilli()
strNonce := strconv.FormatInt(intNonce, 10)
hmac, err := crypto.GetHMAC(
crypto.HashSHA256,
[]byte(strNonce+"websocket_login"),
[]byte(creds.Secret),
)
if err != nil {
return err
}
sign := crypto.HexEncodeToString(hmac)
req := Authenticate{Operation: "login",
Args: AuthenticationData{
Key: creds.Key,
Sign: sign,
Time: intNonce,
SubAccount: creds.SubAccount,
},
}
return f.Websocket.Conn.SendJSONMessage(req)
}
// Subscribe sends a websocket message to receive data from the channel
func (f *FTX) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var errs common.Errors
channels:
for i := range channelsToSubscribe {
var sub WsSub
sub.Channel = channelsToSubscribe[i].Channel
sub.Operation = subscribe
switch channelsToSubscribe[i].Channel {
case wsFills, wsOrders, wsMarkets:
default:
a, err := f.GetPairAssetType(channelsToSubscribe[i].Currency)
if err != nil {
errs = append(errs, err)
continue channels
}
formattedPair, err := f.FormatExchangeCurrency(channelsToSubscribe[i].Currency, a)
if err != nil {
errs = append(errs, err)
continue channels
}
sub.Market = formattedPair.String()
}
err := f.Websocket.Conn.SendJSONMessage(sub)
if err != nil {
errs = append(errs, err)
continue
}
f.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
}
if errs != nil {
return errs
}
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (f *FTX) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var errs common.Errors
channels:
for i := range channelsToUnsubscribe {
var unSub WsSub
unSub.Operation = unsubscribe
unSub.Channel = channelsToUnsubscribe[i].Channel
switch channelsToUnsubscribe[i].Channel {
case wsFills, wsOrders, wsMarkets:
default:
a, err := f.GetPairAssetType(channelsToUnsubscribe[i].Currency)
if err != nil {
errs = append(errs, err)
continue channels
}
formattedPair, err := f.FormatExchangeCurrency(channelsToUnsubscribe[i].Currency, a)
if err != nil {
errs = append(errs, err)
continue channels
}
unSub.Market = formattedPair.String()
}
err := f.Websocket.Conn.SendJSONMessage(unSub)
if err != nil {
errs = append(errs, err)
continue
}
f.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
}
if errs != nil {
return errs
}
return nil
}
// GenerateDefaultSubscriptions generates default subscription
func (f *FTX) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: wsMarkets,
})
var channels = []string{wsTicker, wsTrades, wsOrderbook}
assets := f.GetAssetTypes(true)
for a := range assets {
pairs, err := f.GetEnabledPairs(assets[a])
if err != nil {
return nil, err
}
for z := range pairs {
newPair := currency.NewPairWithDelimiter(pairs[z].Base.String(),
pairs[z].Quote.String(),
"-")
for x := range channels {
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channels[x],
Currency: newPair,
Asset: assets[a],
})
}
}
}
if f.IsWebsocketAuthenticationSupported() {
var authchan = []string{wsOrders, wsFills}
for x := range authchan {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: authchan[x],
})
}
}
return subscriptions, nil
}
// wsReadData gets and passes on websocket messages for processing
func (f *FTX) wsReadData() {
defer f.Websocket.Wg.Done()
for {
select {
case <-f.Websocket.ShutdownC:
return
default:
resp := f.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := f.wsHandleData(resp.Raw)
if err != nil {
f.Websocket.DataHandler <- err
}
}
}
}
func timestampFromFloat64(ts float64) time.Time {
secs := int64(ts)
nsecs := int64((ts - float64(secs)) * 1e9)
return time.Unix(secs, nsecs).UTC()
}
func (f *FTX) wsHandleData(respRaw []byte) error {
var result map[string]interface{}
err := json.Unmarshal(respRaw, &result)
if err != nil {
return err
}
switch result["type"] {
case wsUpdate:
var p currency.Pair
var a asset.Item
market, ok := result["market"]
if ok {
p, err = currency.NewPairFromString(market.(string))
if err != nil {
return err
}
a, err = f.GetPairAssetType(p)
if err != nil {
return err
}
}
switch result["channel"] {
case wsTicker:
var resultData WsTickerDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
f.Websocket.DataHandler <- &ticker.Price{
ExchangeName: f.Name,
Bid: resultData.Ticker.Bid,
BidSize: resultData.Ticker.BidSize,
Ask: resultData.Ticker.Ask,
AskSize: resultData.Ticker.AskSize,
Last: resultData.Ticker.Last,
LastUpdated: timestampFromFloat64(resultData.Ticker.Time),
Pair: p,
AssetType: a,
}
case wsOrderbook:
var resultData WsOrderbookDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
if len(resultData.OBData.Asks) == 0 && len(resultData.OBData.Bids) == 0 {
return nil
}
err = f.WsProcessUpdateOB(&resultData.OBData, p, a)
if err != nil {
err2 := f.wsResubToOB(p)
if err2 != nil {
f.Websocket.DataHandler <- err2
}
return err
}
case wsTrades:
saveTradeData := f.IsSaveTradeDataEnabled()
if !saveTradeData &&
!f.IsTradeFeedEnabled() {
return nil
}
var resultData WsTradeDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
var trades []trade.Data
for z := range resultData.TradeData {
var oSide order.Side
oSide, err = order.StringToOrderSide(resultData.TradeData[z].Side)
if err != nil {
f.Websocket.DataHandler <- order.ClassificationError{
Exchange: f.Name,
Err: err,
}
}
trades = append(trades, trade.Data{
Timestamp: resultData.TradeData[z].Time,
CurrencyPair: p,
AssetType: a,
Exchange: f.Name,
Price: resultData.TradeData[z].Price,
Amount: resultData.TradeData[z].Size,
Side: oSide,
TID: strconv.FormatInt(resultData.TradeData[z].ID, 10),
})
}
return f.Websocket.Trade.Update(saveTradeData, trades...)
case wsOrders:
var resultData WsOrderDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
var pair currency.Pair
pair, err = currency.NewPairFromString(resultData.OrderData.Market)
if err != nil {
return err
}
var assetType asset.Item
assetType, err = f.GetPairAssetType(pair)
if err != nil {
return err
}
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(context.TODO(),
resultData.OrderData.Side,
resultData.OrderData.Status,
resultData.OrderData.OrderType,
resultData.OrderData.Size,
resultData.OrderData.FilledSize,
resultData.OrderData.AvgFillPrice)
if err != nil {
return err
}
var resp order.Detail
resp.PostOnly = resultData.OrderData.PostOnly
resp.Price = resultData.OrderData.Price
resp.Amount = resultData.OrderData.Size
resp.AverageExecutedPrice = resultData.OrderData.AvgFillPrice
resp.ExecutedAmount = resultData.OrderData.FilledSize
resp.RemainingAmount = resultData.OrderData.Size - resultData.OrderData.FilledSize
resp.Cost = resp.AverageExecutedPrice * resultData.OrderData.FilledSize
// Fee: orderVars.Fee is incorrect.
resp.Exchange = f.Name
resp.OrderID = strconv.FormatInt(resultData.OrderData.ID, 10)
resp.ClientOrderID = resultData.OrderData.ClientID
resp.Type = orderVars.OrderType
resp.Side = orderVars.Side
resp.Status = orderVars.Status
resp.AssetType = assetType
resp.Date = resultData.OrderData.CreatedAt
// There's no current timestamp, so this is the best we can get.
resp.LastUpdated = resultData.OrderData.CreatedAt
resp.Pair = pair
f.Websocket.DataHandler <- &resp
case wsFills:
if !f.IsFillsFeedEnabled() {
return nil
}
var resultData WsFillsDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
var side order.Side
side, err = order.StringToOrderSide(resultData.FillsData.Side)
if err != nil {
f.Websocket.DataHandler <- order.ClassificationError{
Exchange: f.Name,
Err: err,
}
}
p, err = currency.NewPairFromString(resultData.FillsData.Market)
if err != nil {
return err
}
a, err = f.GetPairAssetType(p)
if err != nil {
return err
}
return f.Websocket.Fills.Update(fill.Data{
ID: strconv.FormatInt(resultData.FillsData.ID, 10),
Timestamp: resultData.FillsData.Time,
Exchange: f.Name,
AssetType: a,
CurrencyPair: p,
Side: side,
OrderID: strconv.FormatInt(resultData.FillsData.OrderID, 10),
TradeID: strconv.FormatInt(resultData.FillsData.TradeID, 10),
Price: resultData.FillsData.Price,
Amount: resultData.FillsData.Size,
})
default:
f.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: f.Name + stream.UnhandledMessage + string(respRaw)}
}
case wsPartial:
switch result["channel"] {
case "orderbook":
var p currency.Pair
var a asset.Item
market, ok := result["market"]
if ok {
p, err = currency.NewPairFromString(market.(string))
if err != nil {
return err
}
a, err = f.GetPairAssetType(p)
if err != nil {
return err
}
}
var resultData WsOrderbookDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
err = f.WsProcessPartialOB(&resultData.OBData, p, a)
if err != nil {
err2 := f.wsResubToOB(p)
if err2 != nil {
f.Websocket.DataHandler <- err2
}
return err
}
// reset obchecksum failure blockage for pair
delete(obSuccess, p)
case wsMarkets:
var resultData WSMarkets
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
f.Websocket.DataHandler <- resultData.Data
}
case "error":
f.Websocket.DataHandler <- stream.UnhandledMessageWarning{
Message: f.Name + stream.UnhandledMessage + string(respRaw),
}
}
return nil
}
// WsProcessUpdateOB processes an update on the orderbook
func (f *FTX) WsProcessUpdateOB(data *WsOrderbookData, p currency.Pair, a asset.Item) error {
update := orderbook.Update{
Asset: a,
Pair: p,
Bids: make([]orderbook.Item, len(data.Bids)),
Asks: make([]orderbook.Item, len(data.Asks)),
UpdateTime: timestampFromFloat64(data.Time),
}
for x := range data.Bids {
update.Bids[x] = orderbook.Item{
Price: data.Bids[x][0],
Amount: data.Bids[x][1],
}
}
for x := range data.Asks {
update.Asks[x] = orderbook.Item{
Price: data.Asks[x][0],
Amount: data.Asks[x][1],
}
}
err := f.Websocket.Orderbook.Update(&update)
if err != nil {
return err
}
updatedOb, err := f.Websocket.Orderbook.GetOrderbook(p, a)
if err != nil {
return err
}
checksum := f.CalcUpdateOBChecksum(updatedOb)
if checksum != data.Checksum {
log.Warnf(log.ExchangeSys, "%s checksum failure for item %s",
f.Name,
p)
return errors.New("checksum failed")
}
return nil
}
func (f *FTX) wsResubToOB(p currency.Pair) error {
if ok := obSuccess[p]; ok {
return nil
}
obSuccess[p] = true
channelToResubscribe := &stream.ChannelSubscription{
Channel: wsOrderbook,
Currency: p,
}
err := f.Websocket.ResubscribeToChannel(channelToResubscribe)
if err != nil {
return fmt.Errorf("%s resubscribe to orderbook failure %s", f.Name, err)
}
return nil
}
// WsProcessPartialOB creates an OB from websocket data
func (f *FTX) WsProcessPartialOB(data *WsOrderbookData, p currency.Pair, a asset.Item) error {
signedChecksum := f.CalcPartialOBChecksum(data)
if signedChecksum != data.Checksum {
return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid",
f.Name,
a,
p)
}
bids := make(orderbook.Items, len(data.Bids))
asks := make(orderbook.Items, len(data.Asks))
for x := range data.Bids {
bids[x] = orderbook.Item{
Price: data.Bids[x][0],
Amount: data.Bids[x][1],
}
}
for x := range data.Asks {
asks[x] = orderbook.Item{
Price: data.Asks[x][0],
Amount: data.Asks[x][1],
}
}
newOrderBook := orderbook.Base{
Asks: asks,
Bids: bids,
Asset: a,
LastUpdated: timestampFromFloat64(data.Time),
Pair: p,
Exchange: f.Name,
VerifyOrderbook: f.CanVerifyOrderbook,
}
return f.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
}
// CalcPartialOBChecksum calculates checksum of partial OB data received from WS
func (f *FTX) CalcPartialOBChecksum(data *WsOrderbookData) int64 {
var checksum strings.Builder
var price, amount string
for i := 0; i < 100; i++ {
if len(data.Bids)-1 >= i {
price = checksumParseNumber(data.Bids[i][0])
amount = checksumParseNumber(data.Bids[i][1])
checksum.WriteString(price + ":" + amount + ":")
}
if len(data.Asks)-1 >= i {
price = checksumParseNumber(data.Asks[i][0])
amount = checksumParseNumber(data.Asks[i][1])
checksum.WriteString(price + ":" + amount + ":")
}
}
checksumStr := strings.TrimSuffix(checksum.String(), ":")
return int64(crc32.ChecksumIEEE([]byte(checksumStr)))
}
// CalcUpdateOBChecksum calculates checksum of update OB data received from WS
func (f *FTX) CalcUpdateOBChecksum(data *orderbook.Base) int64 {
var checksum strings.Builder
var price, amount string
for i := 0; i < 100; i++ {
if len(data.Bids)-1 >= i {
price = checksumParseNumber(data.Bids[i].Price)
amount = checksumParseNumber(data.Bids[i].Amount)
checksum.WriteString(price + ":" + amount + ":")
}
if len(data.Asks)-1 >= i {
price = checksumParseNumber(data.Asks[i].Price)
amount = checksumParseNumber(data.Asks[i].Amount)
checksum.WriteString(price + ":" + amount + ":")
}
}
checksumStr := strings.TrimSuffix(checksum.String(), ":")
return int64(crc32.ChecksumIEEE([]byte(checksumStr)))
}
func checksumParseNumber(num float64) string {
modifier := byte('f')
if num < 0.0001 {
modifier = 'e'
}
r := strconv.FormatFloat(num, modifier, -1, 64)
if strings.IndexByte(r, '.') == -1 && modifier != 'e' {
r += ".0"
}
return r
}

View File

@@ -1,422 +0,0 @@
package ftx
import (
"fmt"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
func parseRaw(t *testing.T, input string) interface{} {
t.Helper()
pairs := currency.Pairs{
currency.Pair{
Base: currency.BTC,
Quote: currency.USDT,
},
}
dataC := make(chan interface{}, 1)
fills := fill.Fills{}
fills.Setup(true, dataC)
x := FTX{
exchange.Base{
Name: "FTX",
Features: exchange.Features{
Enabled: exchange.FeaturesEnabled{
FillsFeed: true,
},
},
CurrencyPairs: currency.PairsManager{
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: {
Available: pairs,
Enabled: pairs,
ConfigFormat: &currency.PairFormat{
Delimiter: "^",
Uppercase: true,
},
},
},
},
Websocket: &stream.Websocket{
DataHandler: dataC,
Fills: fills,
},
},
CollateralWeightHolder{},
}
if err := x.wsHandleData([]byte(input)); err != nil {
t.Fatal(err)
}
var ret interface{}
select {
case ret = <-x.Websocket.DataHandler:
default:
t.Error(fmt.Errorf("timed out waiting for channel data"))
}
return ret
}
func TestFTX_wsHandleData_Details(t *testing.T) {
const inputPartiallyCancelled = `{
"channel": "orders",
"type": "update",
"data": {
"id": 69350095302,
"clientId": "192ab87ae99970b79f624ef8bd783351",
"market": "BTC/USDT",
"type": "limit",
"side": "sell",
"price": 65536,
"size": 12,
"status": "closed",
"filledSize": 4,
"remainingSize": 8,
"reduceOnly": false,
"liquidation": false,
"avgFillPrice": 32768,
"postOnly": true,
"ioc": true,
"createdAt": "2021-08-08T10:35:02.649437+00:00"
}
}`
p := parseRaw(t, inputPartiallyCancelled)
x, ok := p.(*order.Detail)
if !ok {
t.Fatalf("have %T, want *order.Detail", p)
}
// "reduceOnly" and "liquidation" do not have corresponding fields in
// order.Detail.
if x.OrderID != "69350095302" ||
x.ClientOrderID != "192ab87ae99970b79f624ef8bd783351" ||
x.Pair.Base.Item.Symbol != "BTC" ||
x.Pair.Quote.Item.Symbol != "USDT" ||
x.Type != order.Limit ||
x.Side != order.Sell ||
x.Price != 65536 ||
x.Amount != 12 ||
x.Status != order.PartiallyCancelled ||
x.ExecutedAmount != 4 ||
x.RemainingAmount != 8 ||
x.AverageExecutedPrice != 32768 ||
!x.PostOnly ||
!x.Date.Equal(time.Unix(1628418902, 649437000).UTC()) {
t.Error("parsed values do not match")
}
const inputFilled = `{
"channel": "orders",
"type": "update",
"data": {
"id": 69350095302,
"clientId": "192ab87ae99970b79f624ef8bd783351",
"market": "BTC/USDT",
"type": "limit",
"side": "sell",
"price": 65536,
"size": 12,
"status": "closed",
"filledSize": 12,
"remainingSize": 0,
"reduceOnly": false,
"liquidation": false,
"avgFillPrice": 32768,
"postOnly": true,
"ioc": true,
"createdAt": "2021-08-08T10:35:02.649437+00:00"
}
}`
orderDetail, ok := parseRaw(t, inputFilled).(*order.Detail)
if !ok {
t.Error("unable to type asset order detail")
} else if orderDetail.Status != order.Filled {
t.Errorf("have %s, want %s", orderDetail.Status, order.Filled)
}
const inputCancelled = `{
"channel": "orders",
"type": "update",
"data": {
"id": 69350095302,
"clientId": "192ab87ae99970b79f624ef8bd783351",
"market": "BTC/USDT",
"type": "limit",
"side": "sell",
"price": 65536,
"size": 12,
"status": "closed",
"filledSize": 0,
"remainingSize": 12,
"reduceOnly": false,
"liquidation": false,
"avgFillPrice": 32768,
"postOnly": true,
"ioc": true,
"createdAt": "2021-08-08T10:35:02.649437+00:00"
}
}`
orderDetail, ok = parseRaw(t, inputCancelled).(*order.Detail)
if !ok {
t.Error("unable to type asset order detail")
} else if orderDetail.Status != order.Cancelled {
t.Errorf("have %s, want %s", orderDetail.Status, order.Cancelled)
}
}
func TestFTX_wsHandleData_wsFills(t *testing.T) {
const input = `{
"channel": "fills",
"type": "update",
"data": {
"id": 1234567890,
"market": "BTC-USDT",
"type": "order",
"side": "sell",
"price": 32768,
"size": 2,
"orderId": 23456789012,
"time": "2021-08-07T14:32:42.373010+00:00",
"tradeId": 3456789012,
"feeRate": 8,
"fee": 16,
"feeCurrency": "FTT",
"liquidity": "maker"
}
}`
p := parseRaw(t, input)
x, ok := p.([]fill.Data)
if !ok {
t.Fatalf("have %T, want []fill.Data", p)
}
if x[0].Exchange != "FTX" ||
x[0].ID != "1234567890" ||
x[0].OrderID != "23456789012" ||
x[0].CurrencyPair.Base.String() != "BTC" ||
x[0].CurrencyPair.Quote.String() != "USDT" ||
x[0].Side != order.Sell ||
x[0].TradeID != "3456789012" ||
x[0].Price != 32768 ||
x[0].Amount != 2 ||
!x[0].Timestamp.Equal(time.Unix(1628346762, 373010000).UTC()) {
t.Errorf("parsed values do not match, x: %#v", x)
}
}
func TestFTX_wsHandleData_Price(t *testing.T) {
const input = `{
"channel": "ticker",
"market": "BTC/USDT",
"type": "update",
"data": {
"bid": 16.0,
"ask": 32.0,
"bidSize": 64.0,
"askSize": 128.0,
"last": 256.0,
"time": 1073741824.0
}
}`
p := parseRaw(t, input)
x, ok := p.(*ticker.Price)
if !ok {
t.Fatalf("have %T, want *ticker.Price", p)
}
if x.AssetType != asset.Spot ||
!x.Pair.Equal(currency.NewPair(currency.BTC, currency.USDT)) ||
x.Bid != 16 ||
x.BidSize != 64 ||
x.Ask != 32 ||
x.AskSize != 128 ||
x.Last != 256 ||
!x.LastUpdated.Equal(time.Unix(1073741824, 0)) {
t.Error("parsed values do not match")
}
}
func TestParsingOrders(t *testing.T) {
t.Parallel()
data := []byte(`{
"channel": "fills",
"data": {
"id": 24852229,
"clientId": null,
"market": "XRP-PERP",
"type": "limit",
"side": "buy",
"size": 42353.0,
"price": 0.2977,
"reduceOnly": false,
"ioc": false,
"postOnly": false,
"status": "closed",
"filledSize": 0.0,
"remainingSize": 0.0,
"avgFillPrice": 0.2978
},
"type": "update"
}`)
if err := f.wsHandleData(data); err != nil {
t.Error(err)
}
}
func TestParsingWSTradesData(t *testing.T) {
t.Parallel()
data := []byte(`{
"channel": "trades",
"market": "BTC-PERP",
"type": "update",
"data": [
{
"id": 44200173,
"price": 9761.0,
"size": 0.0008,
"side": "buy",
"liquidation": false,
"time": "2020-05-15T01:10:04.369194+00:00"
}
]
}`)
if err := f.wsHandleData(data); err != nil {
t.Error(err)
}
}
func TestParsingWSTickerData(t *testing.T) {
t.Parallel()
data := []byte(`{
"channel": "ticker",
"market": "BTC-PERP",
"type": "update",
"data": {
"bid": 9760.5,
"ask": 9761.0,
"bidSize": 3.36,
"askSize": 71.8484,
"last": 9761.0,
"time": 1589505004.4237103
}
}`)
if err := f.wsHandleData(data); err != nil {
t.Error(err)
}
}
func TestParsingWSOrdersData(t *testing.T) {
t.Parallel()
data := []byte(`{
"channel": "orders",
"data": {
"id": 24852229,
"clientId": null,
"market": "BTC-PERP",
"type": "limit",
"side": "buy",
"size": 42353.0,
"price": 0.2977,
"reduceOnly": false,
"ioc": false,
"postOnly": false,
"status": "closed",
"filledSize": 0.0,
"remainingSize": 0.0,
"avgFillPrice": 0.2978
},
"type": "update"
}`)
if err := f.wsHandleData(data); err != nil {
t.Error(err)
}
}
func TestParsingMarketsData(t *testing.T) {
t.Parallel()
data := []byte(`{"channel": "markets",
"type": "partial",
"data": {
"ADA-0626": {
"name": "ADA-0626",
"enabled": true,
"priceIncrement": 5e-06,
"sizeIncrement": 1.0,
"type": "future",
"baseCurrency": null,
"quoteCurrency": null,
"restricted": false,
"underlying": "ADA",
"future": {
"name": "ADA-0626",
"underlying": "ADA",
"description": "Cardano June 2020 Futures",
"type": "future", "expiry": "2020-06-26T003:00:00+00:00",
"perpetual": false,
"expired": false,
"enabled": true,
"postOnly": false,
"imfFactor": 4e-05,
"underlyingDescription": "Cardano",
"expiryDescription": "June 2020",
"moveStart": null, "positionLimitWeight": 10.0,
"group": "quarterly"}}},
"action": "partial"
}`)
if err := f.wsHandleData(data); err != nil {
t.Error(err)
}
}
func TestParsingWSOBData(t *testing.T) {
data := []byte(`{"channel": "orderbook", "market": "BTC-PERP", "type": "partial", "data": {"time": 1589855831.4606245, "checksum": 225973019, "bids": [[9602.0, 3.2903], [9601.5, 3.11], [9601.0, 2.1356], [9600.5, 3.0991], [9600.0, 8.014], [9599.5, 4.1571], [9599.0, 79.1846], [9598.5, 3.099], [9598.0, 3.985], [9597.5, 3.999], [9597.0, 16.4335], [9596.5, 4.006], [9596.0, 3.2596], [9595.0, 6.334], [9594.0, 3.5685], [9593.0, 14.2717], [9592.5, 0.5], [9591.0, 2.181], [9590.5, 40.4246], [9590.0, 1.0], [9589.0, 1.357], [9588.5, 0.4738], [9587.5, 0.15], [9587.0, 16.811], [9586.5, 1.2], [9586.0, 0.2], [9585.5, 1.0], [9584.5, 0.002], [9584.0, 1.51], [9583.5, 0.01], [9583.0, 1.4], [9582.5, 0.1], [9582.0, 24.7921], [9581.0, 2.087], [9580.5, 2.0], [9580.0, 0.1], [9579.0, 1.1588], [9578.0, 0.9477], [9577.5, 22.216], [9576.0, 0.2], [9574.0, 22.0], [9573.5, 1.0], [9572.0, 0.203], [9570.0, 0.1026], [9565.5, 5.5332], [9565.0, 27.5243], [9563.5, 2.6], [9562.0, 0.0175], [9561.0, 2.0085], [9552.0, 1.6], [9550.5, 27.3399], [9550.0, 0.1046], [9548.0, 0.0175], [9544.0, 4.8197], [9542.5, 26.5754], [9542.0, 0.003], [9541.0, 0.0549], [9540.0, 0.1984], [9537.5, 0.0008], [9535.5, 0.0105], [9535.0, 1.514], [9534.5, 36.5858], [9532.5, 4.7798], [9531.0, 40.6564], [9525.0, 0.001], [9523.5, 1.6], [9522.0, 0.0894], [9521.0, 0.315], [9520.5, 5.4525], [9520.0, 0.07], [9518.0, 0.034], [9517.5, 4.0], [9513.0, 0.0175], [9512.5, 15.6016], [9512.0, 32.7882], [9511.5, 0.0482], [9510.5, 0.0482], [9510.0, 0.2999], [9509.0, 2.0], [9508.5, 0.0482], [9506.0, 0.0416], [9505.5, 0.0492], [9505.0, 0.2], [9502.5, 0.01], [9502.0, 0.01], [9501.5, 0.0592], [9501.0, 0.001], [9500.0, 3.4913], [9499.5, 39.8683], [9498.0, 4.6108], [9497.0, 0.0481], [9492.0, 41.3559], [9490.0, 1.1104], [9488.0, 0.0105], [9486.0, 5.4443], [9485.5, 0.0482], [9484.0, 4.0], [9482.0, 0.25], [9481.5, 2.0], [9481.0, 8.1572]], "asks": [[9602.5, 3.0], [9603.0, 2.8979], [9603.5, 54.49], [9604.0, 5.9982], [9604.5, 3.028], [9605.0, 4.657], [9606.5, 5.2512], [9607.0, 4.003], [9607.5, 4.011], [9608.0, 13.7505], [9608.5, 3.994], [9609.0, 2.974], [9609.5, 3.002], [9612.0, 10.298], [9612.5, 13.455], [9613.5, 3.013], [9614.0, 2.02], [9614.5, 3.359], [9615.0, 21.2429], [9616.0, 0.5], [9616.5, 0.01], [9617.0, 2.182], [9617.5, 23.0223], [9618.0, 0.0623], [9618.5, 1.5795], [9619.0, 0.3065], [9620.0, 3.9], [9621.0, 1.5], [9622.0, 1.5], [9622.5, 1.216], [9625.0, 1.0], [9625.5, 0.9477], [9626.0, 0.05], [9628.5, 1.1588], [9629.0, 1.4], [9630.0, 4.2332], [9630.5, 1.228], [9631.0, 1.5], [9631.5, 0.0104], [9632.5, 26.7529], [9633.0, 0.25], [9638.0, 1.0], [9640.0, 0.2], [9641.0, 1.001], [9642.0, 0.0175], [9643.0, 0.25], [9643.5, 1.6], [9644.0, 31.4166], [9646.5, 41.6609], [9649.5, 0.2], [9653.5, 1.5], [9656.5, 1.6], [9657.0, 0.2], [9658.0, 1.5], [9659.5, 4.7804], [9660.5, 43.3405], [9665.5, 40.6564], [9670.0, 0.1034], [9671.5, 4.9098], [9674.0, 0.25], [9678.0, 15.6016], [9678.5, 1.5], [9681.0, 34.9683], [9683.0, 0.2], [9683.5, 5.3845], [9684.5, 5.087], [9685.0, 0.1032], [9686.5, 0.0075], [9689.0, 1.6], [9691.0, 34.7472], [9692.0, 0.001], [9694.0, 0.5], [9695.0, 0.0109], [9696.5, 4.825], [9700.0, 1.0595], [9701.5, 2.0], [9702.0, 0.011], [9702.5, 0.01], [9706.0, 1.2], [9708.0, 0.0175], [9710.0, 39.153], [9712.0, 48.6163], [9712.5, 1.5], [9713.0, 8.1572], [9715.5, 0.5021], [9716.5, 2.0], [9719.0, 0.0245], [9721.0, 0.5], [9724.0, 0.251], [9726.0, 0.12], [9727.5, 0.5075], [9730.0, 0.015], [9732.0, 58.5394], [9733.0, 0.001], [9734.0, 20.0], [9743.0, 0.06], [9750.0, 9.5], [9755.0, 52.4404], [9757.0, 48.6121], [9764.0, 0.015]], "action": "partial"}}`)
err := f.wsHandleData(data)
if err != nil {
t.Error(err)
}
data = []byte(`{"channel": "orderbook", "market": "BTC-PERP", "type": "update", "data": {"time": 1589855831.5128105, "checksum": 365946911, "bids": [[9596.0, 4.2656], [9512.0, 32.7912]], "asks": [[9613.5, 4.012], [9702.0, 0.021]], "action": "update"}}`)
err = f.wsHandleData(data)
if err != nil {
t.Error(err)
}
}
func TestParsingWSOBData2(t *testing.T) {
t.Parallel()
data := []byte(`{"channel": "orderbook", "market": "PRIVBEAR/USD", "type": "partial", "data": {"time": 1593498757.0915809, "checksum": 87356415, "bids": [[1389.5, 5.1019], [1384.5, 16.6318], [1371.5, 23.5531], [1365.5, 23.3001], [1354.0, 26.758], [1352.5, 24.6891], [1337.5, 30.3091], [1333.5, 24.9583], [1323.0, 30.9597], [1302.0, 40.9241], [1282.5, 38.0319], [1272.5, 39.1436], [1084.5, 1.8934], [1080.0, 2.0595], [1075.0, 2.0527], [1069.0, 1.8077], [1053.5, 1.855], [1.0, 2.0]], "asks": [[1403.5, 6.8077], [1407.5, 17.6482], [1417.0, 14.6401], [1418.5, 22.6664], [1426.0, 20.3936], [1430.5, 34.2797], [1435.0, 30.6073], [1443.0, 20.2036], [1471.5, 35.5789], [1494.5, 29.2815], [1505.0, 30.9842], [1511.5, 39.4325], [1799.5, 1.7529], [1810.5, 2.0379], [1813.5, 2.0423], [1817.5, 2.0393], [1821.0, 1.7148], [86347.5, 9e-05], [94982.5, 0.0001], [104480.0, 0.0001], [114930.0, 0.00011], [126420.0, 0.00011], [139065.0, 0.00011], [152970.0, 0.00012], [168267.5, 0.00012], [185092.5, 0.00012], [223962.5, 0.00013], [246360.0, 0.00014], [270995.0, 0.00017], [1203602.5, 0.00013]], "action": "partial"}}`)
err := f.wsHandleData(data)
if err != nil {
t.Fatal(err)
}
data = []byte(`{"channel": "orderbook", "market": "DOGE-PERP", "type": "partial", "data": {"time": 1593395710.072698, "checksum": 2591057682, "bids": [[0.0023085, 507742.0], [0.002308, 7000.0], [0.0023075, 100000.0], [0.0023065, 324770.0], [0.002305, 46000.0], [0.0023035, 879600.0], [0.002303, 49000.0], [0.0023025, 1076421.0], [0.002296, 30511800.0], [0.002293, 3006300.0], [0.0022925, 1256349.0], [0.0022895, 11855700.0], [0.0022855, 1008960.0], [0.0022775, 1047578.0], [0.0022745, 3070200.0], [0.00227, 2939100.0], [0.002269, 1599711.0], [0.00226, 1671504.0], [0.00225, 1957119.0], [0.00224, 5225404.0], [0.0022395, 250.0], [0.002233, 2994000.0], [0.002229, 2336857.0], [0.002218, 2144227.0], [0.002205, 2101662.0], [0.0021985, 7406099.0], [0.0021915, 2470187.0], [0.0021775, 2690545.0], [0.0021755, 250.0], [0.002162, 2997201.0], [0.00215, 11464856.0], [0.002148, 16178857.0], [0.0021255, 11063510.0], [0.002119, 164239.0], [0.0020435, 19124572.0], [0.0020395, 18376430.0], [0.0020125, 1250.0], [0.0019655, 50.0], [0.001958, 97012.0], [0.001942, 50000.0], [0.001899, 50000.0], [0.001895, 1250.0], [0.001712, 2500.0], [0.0012075, 70190.0], [0.00112, 22321.0], [1.65e-05, 31889.0]], "asks": [[0.0023145, 359557.0], [0.0023155, 222497.0], [0.0023175, 40000.0], [0.002319, 879600.0], [0.0023195, 50000.0], [0.0023205, 1067334.0], [0.0023215, 45000.0], [0.002326, 33518100.0], [0.0023265, 1113997.0], [0.0023285, 1170756.0], [0.002331, 11855700.0], [0.002336, 1105442.0], [0.002344, 1244804.0], [0.002348, 3070200.0], [0.0023525, 1546561.0], [0.0023555, 2939100.0], [0.0023575, 2928000.0], [0.002362, 1509707.0], [0.0023725, 1786697.0], [0.002374, 5710.0], [0.0023795, 151098.0], [0.0023835, 1747428.0], [0.002385, 2994000.0], [0.002395, 1721532.0], [0.0024015, 5710.0], [0.002408, 2552142.0], [0.002422, 2188855.0], [0.002429, 5710.0], [0.0024295, 8441953.0], [0.002437, 2196750.0], [0.002445, 122574.0], [0.002454, 1974273.0], [0.0024565, 5710.0], [0.0024715, 2864643.0], [0.00248, 15238408.0], [0.002484, 5710.0], [0.002497, 16343646.0], [0.0025025, 12177084.0], [0.0025115, 5710.0], [0.002539, 5710.0], [0.002566, 16643688.0], [0.0025665, 5710.0], [0.002594, 5710.0], [0.002617, 50.0], [0.002623, 10.0], [0.0027685, 20825893.0], [0.003178, 50000.0], [0.003811, 68952.0], [0.0074, 41460.0]], "action": "partial"}}`)
err = f.wsHandleData(data)
if err != nil {
t.Error(err)
}
data = []byte(`{"channel": "orderbook", "market": "BTC-PERP", "type": "partial", "data": {"time": 1589855831.4606245, "checksum": 225973019, "bids": [[9602.0, 3.2903], [9601.5, 3.11], [9601.0, 2.1356], [9600.5, 3.0991], [9600.0, 8.014], [9599.5, 4.1571], [9599.0, 79.1846], [9598.5, 3.099], [9598.0, 3.985], [9597.5, 3.999], [9597.0, 16.4335], [9596.5, 4.006], [9596.0, 3.2596], [9595.0, 6.334], [9594.0, 3.5685], [9593.0, 14.2717], [9592.5, 0.5], [9591.0, 2.181], [9590.5, 40.4246], [9590.0, 1.0], [9589.0, 1.357], [9588.5, 0.4738], [9587.5, 0.15], [9587.0, 16.811], [9586.5, 1.2], [9586.0, 0.2], [9585.5, 1.0], [9584.5, 0.002], [9584.0, 1.51], [9583.5, 0.01], [9583.0, 1.4], [9582.5, 0.1], [9582.0, 24.7921], [9581.0, 2.087], [9580.5, 2.0], [9580.0, 0.1], [9579.0, 1.1588], [9578.0, 0.9477], [9577.5, 22.216], [9576.0, 0.2], [9574.0, 22.0], [9573.5, 1.0], [9572.0, 0.203], [9570.0, 0.1026], [9565.5, 5.5332], [9565.0, 27.5243], [9563.5, 2.6], [9562.0, 0.0175], [9561.0, 2.0085], [9552.0, 1.6], [9550.5, 27.3399], [9550.0, 0.1046], [9548.0, 0.0175], [9544.0, 4.8197], [9542.5, 26.5754], [9542.0, 0.003], [9541.0, 0.0549], [9540.0, 0.1984], [9537.5, 0.0008], [9535.5, 0.0105], [9535.0, 1.514], [9534.5, 36.5858], [9532.5, 4.7798], [9531.0, 40.6564], [9525.0, 0.001], [9523.5, 1.6], [9522.0, 0.0894], [9521.0, 0.315], [9520.5, 5.4525], [9520.0, 0.07], [9518.0, 0.034], [9517.5, 4.0], [9513.0, 0.0175], [9512.5, 15.6016], [9512.0, 32.7882], [9511.5, 0.0482], [9510.5, 0.0482], [9510.0, 0.2999], [9509.0, 2.0], [9508.5, 0.0482], [9506.0, 0.0416], [9505.5, 0.0492], [9505.0, 0.2], [9502.5, 0.01], [9502.0, 0.01], [9501.5, 0.0592], [9501.0, 0.001], [9500.0, 3.4913], [9499.5, 39.8683], [9498.0, 4.6108], [9497.0, 0.0481], [9492.0, 41.3559], [9490.0, 1.1104], [9488.0, 0.0105], [9486.0, 5.4443], [9485.5, 0.0482], [9484.0, 4.0], [9482.0, 0.25], [9481.5, 2.0], [9481.0, 8.1572]], "asks": [[9602.5, 3.0], [9603.0, 2.8979], [9603.5, 54.49], [9604.0, 5.9982], [9604.5, 3.028], [9605.0, 4.657], [9606.5, 5.2512], [9607.0, 4.003], [9607.5, 4.011], [9608.0, 13.7505], [9608.5, 3.994], [9609.0, 2.974], [9609.5, 3.002], [9612.0, 10.298], [9612.5, 13.455], [9613.5, 3.013], [9614.0, 2.02], [9614.5, 3.359], [9615.0, 21.2429], [9616.0, 0.5], [9616.5, 0.01], [9617.0, 2.182], [9617.5, 23.0223], [9618.0, 0.0623], [9618.5, 1.5795], [9619.0, 0.3065], [9620.0, 3.9], [9621.0, 1.5], [9622.0, 1.5], [9622.5, 1.216], [9625.0, 1.0], [9625.5, 0.9477], [9626.0, 0.05], [9628.5, 1.1588], [9629.0, 1.4], [9630.0, 4.2332], [9630.5, 1.228], [9631.0, 1.5], [9631.5, 0.0104], [9632.5, 26.7529], [9633.0, 0.25], [9638.0, 1.0], [9640.0, 0.2], [9641.0, 1.001], [9642.0, 0.0175], [9643.0, 0.25], [9643.5, 1.6], [9644.0, 31.4166], [9646.5, 41.6609], [9649.5, 0.2], [9653.5, 1.5], [9656.5, 1.6], [9657.0, 0.2], [9658.0, 1.5], [9659.5, 4.7804], [9660.5, 43.3405], [9665.5, 40.6564], [9670.0, 0.1034], [9671.5, 4.9098], [9674.0, 0.25], [9678.0, 15.6016], [9678.5, 1.5], [9681.0, 34.9683], [9683.0, 0.2], [9683.5, 5.3845], [9684.5, 5.087], [9685.0, 0.1032], [9686.5, 0.0075], [9689.0, 1.6], [9691.0, 34.7472], [9692.0, 0.001], [9694.0, 0.5], [9695.0, 0.0109], [9696.5, 4.825], [9700.0, 1.0595], [9701.5, 2.0], [9702.0, 0.011], [9702.5, 0.01], [9706.0, 1.2], [9708.0, 0.0175], [9710.0, 39.153], [9712.0, 48.6163], [9712.5, 1.5], [9713.0, 8.1572], [9715.5, 0.5021], [9716.5, 2.0], [9719.0, 0.0245], [9721.0, 0.5], [9724.0, 0.251], [9726.0, 0.12], [9727.5, 0.5075], [9730.0, 0.015], [9732.0, 58.5394], [9733.0, 0.001], [9734.0, 20.0], [9743.0, 0.06], [9750.0, 9.5], [9755.0, 52.4404], [9757.0, 48.6121], [9764.0, 0.015]], "action": "partial"}}`)
err = f.wsHandleData(data)
if err != nil {
t.Error(err)
}
data = []byte(`{"channel": "orderbook", "market": "BTC-PERP", "type": "update", "data": {"time": 1589855831.5128105, "checksum": 365946911, "bids": [[9596.0, 4.2656], [9512.0, 32.7912]], "asks": [[9613.5, 4.012], [9702.0, 0.021]], "action": "update"}}`)
err = f.wsHandleData(data)
if err != nil {
t.Error(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -530,19 +530,16 @@ type TraderSentimentIndexPositionData struct {
// LiquidationOrdersData stores data of liquidation orders
type LiquidationOrdersData struct {
Data struct {
Orders []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"buy"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt int64 `json:"created_at"`
} `json:"orders"`
TotalPage int64 `json:"totalPage"`
CurrentPage int64 `json:"current_page"`
TotalSize int64 `json:"total_size"`
Data []struct {
QueryID int64 `json:"query_id"`
ContractCode string `json:"contract_code"`
Symbol string `json:"symbol"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt int64 `json:"created_at"`
Amount float64 `json:"amount"`
} `json:"data"`
}

View File

@@ -35,7 +35,7 @@ const (
huobiSwapSystemStatus = "/swap-api/v1/swap_api_state"
huobiSwapSentimentAccountData = "/swap-api/v1/swap_elite_account_ratio"
huobiSwapSentimentPosition = "/swap-api/v1/swap_elite_position_ratio"
huobiSwapLiquidationOrders = "/swap-api/v1/swap_liquidation_orders"
huobiSwapLiquidationOrders = "/swap-api/v3/swap_liquidation_orders"
huobiSwapHistoricalFundingRate = "/swap-api/v1/swap_historical_funding_rate"
huobiPremiumIndexKlineData = "/index/market/history/swap_premium_index_kline"
huobiPredictedFundingRateData = "/index/market/history/swap_estimated_rate_kline"
@@ -318,28 +318,31 @@ func (h *HUOBI) GetTraderSentimentIndexPosition(ctx context.Context, code curren
}
// GetLiquidationOrders gets liquidation orders for a given perp
func (h *HUOBI) GetLiquidationOrders(ctx context.Context, code currency.Pair, tradeType string, pageIndex, pageSize, createDate int64) (LiquidationOrdersData, error) {
func (h *HUOBI) GetLiquidationOrders(ctx context.Context, contract currency.Pair, tradeType string, startTime, endTime int64, direction string, fromID int64) (LiquidationOrdersData, error) {
var resp LiquidationOrdersData
codeValue, err := h.FormatSymbol(code, asset.CoinMarginedFutures)
formattedContract, err := h.FormatSymbol(contract, asset.CoinMarginedFutures)
if err != nil {
return resp, err
}
if createDate != 7 && createDate != 90 {
return resp, fmt.Errorf("invalid createDate. 7 and 90 are the only supported values")
}
tType, ok := validTradeTypes[tradeType]
if !ok {
return resp, fmt.Errorf("invalid trade type")
}
params := url.Values{}
params.Set("contract_code", codeValue)
params.Set("create_date", strconv.FormatInt(createDate, 10))
params.Set("contract", formattedContract)
params.Set("trade_type", strconv.FormatInt(tType, 10))
if pageIndex != 0 {
params.Set("page_index", strconv.FormatInt(pageIndex, 10))
if startTime != 0 {
params.Set("start_time", strconv.FormatInt(startTime, 10))
}
if pageSize != 0 {
params.Set("page_size", strconv.FormatInt(pageIndex, 10))
if endTime != 0 {
params.Set("end_time", strconv.FormatInt(startTime, 10))
}
if direction != "" {
params.Set("direct", direction)
}
if fromID != 0 {
params.Set("from_id", strconv.FormatInt(fromID, 10))
}
path := common.EncodeURLValues(huobiSwapLiquidationOrders, params)
return resp, h.SendHTTPRequest(ctx, exchange.RestFutures, path, &resp)

View File

@@ -40,7 +40,7 @@ const (
fSystemStatus = "/api/v1/contract_api_state"
fTopAccountsSentiment = "/api/v1/contract_elite_account_ratio"
fTopPositionsSentiment = "/api/v1/contract_elite_position_ratio"
fLiquidationOrders = "/api/v1/contract_liquidation_orders"
fLiquidationOrders = "/api/v3/contract_liquidation_orders"
fIndexKline = "/index/market/history/index"
fBasisData = "/index/market/history/basis"
@@ -403,24 +403,27 @@ func (h *HUOBI) FQueryTopPositionsRatio(ctx context.Context, symbol, period stri
}
// FLiquidationOrders gets liquidation orders for futures contracts
func (h *HUOBI) FLiquidationOrders(ctx context.Context, symbol, tradeType string, pageIndex, pageSize, createDate int64) (FLiquidationOrdersInfo, error) {
var resp FLiquidationOrdersInfo
params := url.Values{}
params.Set("symbol", symbol)
if createDate != 7 && createDate != 90 {
return resp, fmt.Errorf("invalid createDate. 7 and 90 are the only supported values")
}
params.Set("create_date", strconv.FormatInt(createDate, 10))
func (h *HUOBI) FLiquidationOrders(ctx context.Context, symbol currency.Code, tradeType string, startTime, endTime int64, direction string, fromID int64) (LiquidationOrdersData, error) {
var resp LiquidationOrdersData
tType, ok := validTradeTypes[tradeType]
if !ok {
return resp, fmt.Errorf("invalid trade type")
}
params := url.Values{}
params.Set("symbol", symbol.String())
params.Set("trade_type", strconv.FormatInt(tType, 10))
if pageIndex != 0 {
params.Set("page_index", strconv.FormatInt(pageIndex, 10))
if startTime != 0 {
params.Set("start_time", strconv.FormatInt(startTime, 10))
}
if pageSize != 0 {
params.Set("page_size", strconv.FormatInt(pageIndex, 10))
if endTime != 0 {
params.Set("end_time", strconv.FormatInt(startTime, 10))
}
if direction != "" {
params.Set("direct", direction)
}
if fromID != 0 {
params.Set("from_id", strconv.FormatInt(fromID, 10))
}
path := common.EncodeURLValues(fLiquidationOrders, params)
return resp, h.SendHTTPRequest(ctx, exchange.RestFutures, path, &resp)

View File

@@ -264,8 +264,7 @@ func TestFQueryTopPositionsRatio(t *testing.T) {
func TestFLiquidationOrders(t *testing.T) {
t.Parallel()
_, err := h.FLiquidationOrders(context.Background(), "BTC", "filled", 0, 0, 7)
if err != nil {
if _, err := h.FLiquidationOrders(context.Background(), currency.BTC, "filled", 0, 0, "", 0); err != nil {
t.Error(err)
}
}
@@ -1002,8 +1001,8 @@ func TestGetLiquidationOrders(t *testing.T) {
if err != nil {
t.Error(err)
}
_, err = h.GetLiquidationOrders(context.Background(), cp, "closed", 0, 0, 7)
if err != nil {
if _, err = h.GetLiquidationOrders(context.Background(), cp, "closed", 0, 0, "", 0); err != nil {
t.Error(err)
}
}

View File

@@ -270,6 +270,8 @@ func durationToWord(in Interval) string {
return "oneday"
case ThreeDay:
return "threeday"
case FiveDay:
return "fiveday"
case FifteenDay:
return "fifteenday"
case OneWeek:

View File

@@ -214,6 +214,10 @@ func TestDurationToWord(t *testing.T) {
"ThreeDay",
ThreeDay,
},
{
"FiveDay",
FiveDay,
},
{
"FifteenDay",
FifteenDay,
@@ -337,6 +341,11 @@ func TestTotalCandlesPerInterval(t *testing.T) {
ThreeDay,
121,
},
{
"FiveDay",
FiveDay,
73,
},
{
"FifteenDay",
FifteenDay,

View File

@@ -28,6 +28,7 @@ const (
OneDay = 24 * OneHour
TwoDay = 2 * OneDay
ThreeDay = 3 * OneDay
FiveDay = 5 * OneDay
SevenDay = 7 * OneDay
FifteenDay = 15 * OneDay
OneWeek = 7 * OneDay

View File

@@ -836,7 +836,7 @@ func (ok *Okx) PlaceTWAPOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoO
if arg.PriceLimit <= 0 {
return nil, errInvalidPriceLimit
}
if ok.GetIntervalEnum(arg.TimeInterval) == "" {
if ok.GetIntervalEnum(arg.TimeInterval, true) == "" {
return nil, errMissingIntervalValue
}
return ok.PlaceAlgoOrder(ctx, arg)
@@ -3075,7 +3075,7 @@ func (ok *Okx) GetOrderBookDepth(ctx context.Context, instrumentID string, depth
}
// GetIntervalEnum allowed interval params by Okx Exchange
func (ok *Okx) GetIntervalEnum(interval kline.Interval) string {
func (ok *Okx) GetIntervalEnum(interval kline.Interval, appendUTC bool) string {
switch interval {
case kline.OneMin:
return "1m"
@@ -3093,31 +3093,41 @@ func (ok *Okx) GetIntervalEnum(interval kline.Interval) string {
return "2H"
case kline.FourHour:
return "4H"
case kline.SixHour: // NOTE: Cases here and below force UTC return instead of hong Kong time.
return "6Hutc"
case kline.EightHour:
return "8Hutc"
case kline.TwelveHour:
return "12Hutc"
case kline.OneDay:
return "1Dutc"
case kline.TwoDay:
return "2Dutc"
case kline.ThreeDay:
return "3Dutc"
case kline.OneWeek:
return "1Wutc"
case kline.OneMonth:
return "1Mutc"
case kline.ThreeMonth:
return "3Mutc"
case kline.SixMonth:
return "6Mutc"
case kline.OneYear:
return "1Yutc"
default:
return ""
}
duration := ""
switch interval {
case kline.SixHour: // NOTE: Cases here and below can either be local Hong Kong time or UTC time.
duration = "6H"
case kline.TwelveHour:
duration = "12H"
case kline.OneDay:
duration = "1D"
case kline.TwoDay:
duration = "2D"
case kline.ThreeDay:
duration = "3D"
case kline.FiveDay:
duration = "5D"
case kline.OneWeek:
duration = "1W"
case kline.OneMonth:
duration = "1M"
case kline.ThreeMonth:
duration = "3M"
case kline.SixMonth:
duration = "6M"
case kline.OneYear:
duration = "1Y"
default:
return duration
}
if appendUTC {
duration += "utc"
}
return duration
}
// GetCandlesticks Retrieve the candlestick charts. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar.
@@ -3160,7 +3170,7 @@ func (ok *Okx) GetCandlestickData(ctx context.Context, instrumentID string, inte
if !after.IsZero() {
params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
}
bar := ok.GetIntervalEnum(interval)
bar := ok.GetIntervalEnum(interval, true)
if bar != "" {
params.Set("bar", bar)
}
@@ -3719,7 +3729,7 @@ func (ok *Okx) GetTakerVolume(ctx context.Context, currency, instrumentType stri
return nil, errInvalidInstrumentType
}
params.Set("instType", strings.ToUpper(instrumentType))
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -3773,7 +3783,7 @@ func (ok *Okx) GetMarginLendingRatio(ctx context.Context, currency string, begin
if !end.IsZero() {
params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10))
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -3813,7 +3823,7 @@ func (ok *Okx) GetLongShortRatio(ctx context.Context, currency string, begin, en
if !end.IsZero() {
params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10))
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -3857,7 +3867,7 @@ func (ok *Okx) GetContractsOpenInterestAndVolume(
if !end.IsZero() {
params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10))
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -3901,7 +3911,7 @@ func (ok *Okx) GetOptionsOpenInterestAndVolume(ctx context.Context, currency str
if currency != "" {
params.Set("ccy", currency)
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -3945,7 +3955,7 @@ func (ok *Okx) GetPutCallRatio(ctx context.Context, currency string,
if currency != "" {
params.Set("ccy", currency)
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -3984,7 +3994,7 @@ func (ok *Okx) GetOpenInterestAndVolumeExpiry(ctx context.Context, currency stri
if currency != "" {
params.Set("ccy", currency)
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -4068,7 +4078,7 @@ func (ok *Okx) GetOpenInterestAndVolumeStrike(ctx context.Context, currency stri
if currency != "" {
params.Set("ccy", currency)
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}
@@ -4128,7 +4138,7 @@ func (ok *Okx) GetTakerFlow(ctx context.Context, currency string, period kline.I
if currency != "" {
params.Set("ccy", currency)
}
interval := ok.GetIntervalEnum(period)
interval := ok.GetIntervalEnum(period, false)
if interval != "" {
params.Set("period", interval)
}

View File

@@ -370,7 +370,7 @@ func TestGetSupportCoins(t *testing.T) {
func TestGetTakerVolume(t *testing.T) {
t.Parallel()
if _, err := ok.GetTakerVolume(context.Background(), "BTC", "SPOT", time.Time{}, time.Time{}, kline.FiveMin); err != nil {
if _, err := ok.GetTakerVolume(context.Background(), "BTC", "SPOT", time.Time{}, time.Time{}, kline.OneDay); err != nil {
t.Error("Okx GetTakerVolume() error", err)
}
}
@@ -383,7 +383,7 @@ func TestGetMarginLendingRatio(t *testing.T) {
func TestGetLongShortRatio(t *testing.T) {
t.Parallel()
if _, err := ok.GetLongShortRatio(context.Background(), "BTC", time.Time{}, time.Time{}, kline.FiveMin); err != nil {
if _, err := ok.GetLongShortRatio(context.Background(), "BTC", time.Time{}, time.Time{}, kline.OneDay); err != nil {
t.Error("Okx GetLongShortRatio() error", err)
}
}
@@ -3193,3 +3193,30 @@ func TestGuessAssetTypeFromInstrumentID(t *testing.T) {
t.Error("unexpected result")
}
}
func TestGetIntervalEnum(t *testing.T) {
t.Parallel()
tests := []struct {
Description string
Interval kline.Interval
Expected string
AppendUTC bool
}{
{Description: "4hr with UTC", Interval: kline.FourHour, Expected: "4H", AppendUTC: true},
{Description: "6H without UTC", Interval: kline.SixHour, Expected: "6H"},
{Description: "6H with UTC", Interval: kline.SixHour, Expected: "6Hutc", AppendUTC: true},
{Description: "Unsupported interval with UTC", Expected: "", AppendUTC: true},
}
for x := range tests {
tt := tests[x]
t.Run(tt.Description, func(t *testing.T) {
t.Parallel()
if r := ok.GetIntervalEnum(tt.Interval, tt.AppendUTC); r != tt.Expected {
t.Errorf("%s: received: %s but expected: %s", tt.Description, r, tt.Expected)
}
})
}
}

View File

@@ -142,7 +142,9 @@ func (ok *Okx) SetDefaults() {
kline.SixHour,
kline.TwelveHour,
kline.OneDay,
kline.TwoDay,
kline.ThreeDay,
kline.FiveDay,
kline.OneWeek,
kline.OneMonth,
kline.ThreeMonth,

View File

@@ -87,7 +87,7 @@ type CollateralByPosition struct {
// CollateralByCurrency individual collateral contribution
// along with what the potentially scaled collateral
// currency it is represented as
// eg in FTX ScaledCurrency is USD
// eg in Bybit ScaledCurrency is USDC
type CollateralByCurrency struct {
Currency currency.Code
SkipContribution bool
@@ -222,7 +222,7 @@ type TotalCollateralCalculator struct {
// CollateralCalculator is used to determine
// the size of collateral holdings for an exchange
// eg on FTX, the collateral is scaled depending on what
// eg on Bybit, the collateral is scaled depending on what
// currency it is
type CollateralCalculator struct {
CalculateOffline bool

View File

@@ -28,7 +28,6 @@ var Exchanges = []string{
"coinbasepro",
"coinut",
"exmo",
"ftx",
"gateio",
"gemini",
"hitbtc",

View File

@@ -74,7 +74,6 @@ _b in this context is an `IBotExchange` implemented struct_
| CoinbasePro | Yes | Yes | No|
| COINUT | Yes | Yes | No |
| Exmo | Yes | NA | No |
| FTX | Yes | Yes | Yes |
| GateIO | Yes | Yes | No |
| Gemini | Yes | Yes | Yes |
| HitBTC | Yes | Yes | Yes |