mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* Added TimeInForce type and updated related files * Linter issue fix and minor coinbasepro type update * Bitrex consts update * added unit test and minor changes in bittrex * Unit tests update * Fix minor linter issues * Update TestStringToTimeInForce unit test * Exchange test template change * A different approach * fix conflict with gateio timeInForce * minor exchange template update * Minor fix to test_files template * Update order tests * Complete updating the order unit tests * Updating exchange wrapper and test template files * update kucoin and deribit wrapper to match the time in force change * minor comment update * fix time-in-force related test errors * linter issue fix * ADD_NEW_EXCHANGE documentation update * time in force constants, functions and unit tests update * shift tif policies to TimeInForce * Update time-in-force, related functions, and unit tests * fix linter issue and time-in-force processing * added a good till crossing tif value * order type fix and fix related tim-in-force entries * update time-in-force unmarshaling and unit test * consistency guideline added * fix time-in-force error in gateio * linter issue fix * update based on review comments * add unit test and fix missing issues * minor fix and added benchmark unit test * change GTT to GTC for limit * fix linter issue * added time-in-force value to place order param * fix minor issues based on review comment and move tif code to separate files * update on exchanges linked to time-in-force * resolve missing review comments * minor linter issues fix * added time-in-force handler and update timeInForce parametered endpoint * minor fixes based on review * nits fix * update based on review * linter fix * rm getTimeInForce func and minor change to time-in-force * minor change * update based on review comments * wrappers and time-in-force calling approach * minor change * update gateio string to timeInForce conversion and unit test * update exchange template * update wrapper template file * policy comments, and template files update * rename all exchange types name to Exchange * update on template files and template generation * templates and generation code and other updates * linter issue fix * added subscriptions and websocket templates * update ADD_NEW_EXCHANGE.md with recent binance functions and implementations * rename template files and update unit tests * minor template and unit test fix * rename templates and fix on unit tests * update on template files and documentation * removed unnecessary tag fix and update templates * fix Add_NEW_EXCHANGE.md doc file * formatting, comments, and error checks update on template files * rename exchange receivers to e and ex for consistency * rename unit test exchange receiver and minor updates * linter issues fix * fix deribit issue and minor style update * fix test issues caused by receiver change * raname local variables exchange declaration variables * update templates comments * update templates and related comments * renamed ex to e * update template comments * toggle WS to false to improve coverage * template comments update * added test coverage to Ws enabled and minor changes --------- Co-authored-by: Samuel Reid <43227667+cranktakular@users.noreply.github.com>
950 lines
25 KiB
Go
950 lines
25 KiB
Go
package coinut
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/buger/jsonparser"
|
|
gws "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/encoding/json"
|
|
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
const (
|
|
coinutWebsocketURL = "wss://wsapi.coinut.com"
|
|
coinutWebsocketRateLimit = 30
|
|
)
|
|
|
|
var channels map[string]chan []byte
|
|
|
|
// NOTE for speed considerations
|
|
// wss://wsapi-as.coinut.com
|
|
// wss://wsapi-na.coinut.com
|
|
// wss://wsapi-eu.coinut.com
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (e *Exchange) WsConnect() error {
|
|
ctx := context.TODO()
|
|
if !e.Websocket.IsEnabled() || !e.IsEnabled() {
|
|
return websocket.ErrWebsocketNotEnabled
|
|
}
|
|
var dialer gws.Dialer
|
|
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
e.Websocket.Wg.Add(1)
|
|
go e.wsReadData(ctx)
|
|
|
|
if !e.instrumentMap.IsLoaded() {
|
|
_, err = e.WsGetInstruments(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if e.IsWebsocketAuthenticationSupported() {
|
|
if err = e.wsAuthenticate(ctx); err != nil {
|
|
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
log.Errorln(log.WebsocketMgr, e.Name+" "+err.Error())
|
|
}
|
|
}
|
|
|
|
// define bi-directional communication
|
|
channels = make(map[string]chan []byte)
|
|
channels["hb"] = make(chan []byte, 1)
|
|
|
|
return nil
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (e *Exchange) wsReadData(ctx context.Context) {
|
|
defer e.Websocket.Wg.Done()
|
|
|
|
for {
|
|
resp := e.Websocket.Conn.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
|
|
if strings.HasPrefix(string(resp.Raw), "[") {
|
|
var incoming []wsResponse
|
|
err := json.Unmarshal(resp.Raw, &incoming)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
for i := range incoming {
|
|
if incoming[i].Nonce > 0 {
|
|
if e.Websocket.Match.IncomingWithData(incoming[i].Nonce, resp.Raw) {
|
|
break
|
|
}
|
|
}
|
|
var individualJSON []byte
|
|
individualJSON, err = json.Marshal(incoming[i])
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
err = e.wsHandleData(ctx, individualJSON)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
} else {
|
|
var incoming wsResponse
|
|
err := json.Unmarshal(resp.Raw, &incoming)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
err = e.wsHandleData(ctx, resp.Raw)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Exchange) wsHandleData(_ context.Context, respRaw []byte) error {
|
|
if strings.HasPrefix(string(respRaw), "[") {
|
|
var orders []wsOrderContainer
|
|
err := json.Unmarshal(respRaw, &orders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range orders {
|
|
o, err2 := e.parseOrderContainer(&orders[i])
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
e.Websocket.DataHandler <- o
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var incoming wsResponse
|
|
err := json.Unmarshal(respRaw, &incoming)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.Websocket.Match.IncomingWithData(incoming.Nonce, respRaw) {
|
|
return nil
|
|
}
|
|
|
|
format, err := e.GetPairFormat(asset.Spot, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch incoming.Reply {
|
|
case "hb":
|
|
channels["hb"] <- respRaw
|
|
case "user_balance":
|
|
var userBalance WsUserBalanceResponse
|
|
err := json.Unmarshal(respRaw, &userBalance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "user_open_orders":
|
|
var openOrders WsUserOpenOrdersResponse
|
|
err := json.Unmarshal(respRaw, &openOrders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "cancel_order":
|
|
var cancel WsCancelOrderResponse
|
|
err := json.Unmarshal(respRaw, &cancel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- &order.Detail{
|
|
Exchange: e.Name,
|
|
OrderID: strconv.FormatInt(cancel.OrderID, 10),
|
|
Status: order.Cancelled,
|
|
LastUpdated: time.Now(),
|
|
AssetType: asset.Spot,
|
|
}
|
|
case "cancel_orders":
|
|
var cancels WsCancelOrdersResponse
|
|
err := json.Unmarshal(respRaw, &cancels)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range cancels.Results {
|
|
e.Websocket.DataHandler <- &order.Detail{
|
|
Exchange: e.Name,
|
|
OrderID: strconv.FormatInt(cancels.Results[i].OrderID, 10),
|
|
Status: order.Cancelled,
|
|
LastUpdated: time.Now(),
|
|
AssetType: asset.Spot,
|
|
}
|
|
}
|
|
case "trade_history":
|
|
var trades WsTradeHistoryResponse
|
|
err := json.Unmarshal(respRaw, &trades)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "inst_list":
|
|
var instList wsInstList
|
|
err := json.Unmarshal(respRaw, &instList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range instList.Spot {
|
|
for _, v2 := range v {
|
|
e.instrumentMap.Seed(k, v2.InstrumentID)
|
|
}
|
|
}
|
|
case "inst_tick":
|
|
var wsTicker WsTicker
|
|
err := json.Unmarshal(respRaw, &wsTicker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pairs, err := e.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currencyPair := e.instrumentMap.LookupInstrument(wsTicker.InstID)
|
|
p, err := currency.NewPairFromFormattedPairs(currencyPair,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
e.Websocket.DataHandler <- &ticker.Price{
|
|
ExchangeName: e.Name,
|
|
Volume: wsTicker.Volume24,
|
|
QuoteVolume: wsTicker.Volume24Quote,
|
|
Bid: wsTicker.HighestBuy,
|
|
Ask: wsTicker.LowestSell,
|
|
High: wsTicker.High24,
|
|
Low: wsTicker.Low24,
|
|
Last: wsTicker.Last,
|
|
LastUpdated: wsTicker.Timestamp.Time(),
|
|
AssetType: asset.Spot,
|
|
Pair: p,
|
|
}
|
|
case "inst_order_book":
|
|
var orderbookSnapshot WsOrderbookSnapshot
|
|
err := json.Unmarshal(respRaw, &orderbookSnapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = e.WsProcessOrderbookSnapshot(&orderbookSnapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "inst_order_book_update":
|
|
var orderbookUpdate WsOrderbookUpdate
|
|
err := json.Unmarshal(respRaw, &orderbookUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = e.WsProcessOrderbookUpdate(&orderbookUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "inst_trade":
|
|
if !e.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
var tradeSnap WsTradeSnapshot
|
|
err := json.Unmarshal(respRaw, &tradeSnap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var trades []trade.Data
|
|
for i := range tradeSnap.Trades {
|
|
pairs, err := e.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currencyPair := e.instrumentMap.LookupInstrument(tradeSnap.InstrumentID)
|
|
p, err := currency.NewPairFromFormattedPairs(currencyPair,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tSide, err := order.StringToOrderSide(tradeSnap.Trades[i].Side)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
trades = append(trades, trade.Data{
|
|
Timestamp: tradeSnap.Trades[i].Timestamp.Time(),
|
|
CurrencyPair: p,
|
|
AssetType: asset.Spot,
|
|
Exchange: e.Name,
|
|
Price: tradeSnap.Trades[i].Price,
|
|
Side: tSide,
|
|
Amount: tradeSnap.Trades[i].Quantity,
|
|
TID: strconv.FormatInt(tradeSnap.Trades[i].TransID, 10),
|
|
})
|
|
}
|
|
return trade.AddTradesToBuffer(trades...)
|
|
case "inst_trade_update":
|
|
if !e.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
var tradeUpdate WsTradeUpdate
|
|
err := json.Unmarshal(respRaw, &tradeUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pairs, err := e.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currencyPair := e.instrumentMap.LookupInstrument(tradeUpdate.InstID)
|
|
p, err := currency.NewPairFromFormattedPairs(currencyPair,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tSide, err := order.StringToOrderSide(tradeUpdate.Side)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
return trade.AddTradesToBuffer(trade.Data{
|
|
Timestamp: tradeUpdate.Timestamp.Time(),
|
|
CurrencyPair: p,
|
|
AssetType: asset.Spot,
|
|
Exchange: e.Name,
|
|
Price: tradeUpdate.Price,
|
|
Side: tSide,
|
|
Amount: tradeUpdate.Quantity,
|
|
TID: strconv.FormatInt(tradeUpdate.TransID, 10),
|
|
})
|
|
case "order_filled", "order_rejected", "order_accepted":
|
|
var orderContainer wsOrderContainer
|
|
err := json.Unmarshal(respRaw, &orderContainer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o, err := e.parseOrderContainer(&orderContainer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- o
|
|
default:
|
|
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stringToOrderStatus(status string, quantity float64) (order.Status, error) {
|
|
switch status {
|
|
case "order_accepted":
|
|
return order.Active, nil
|
|
case "order_filled":
|
|
if quantity > 0 {
|
|
return order.PartiallyFilled, nil
|
|
}
|
|
return order.Filled, nil
|
|
case "order_rejected":
|
|
return order.Rejected, nil
|
|
default:
|
|
return order.UnknownStatus, errors.New(status + " not recognised as order status")
|
|
}
|
|
}
|
|
|
|
func (e *Exchange) parseOrderContainer(oContainer *wsOrderContainer) (*order.Detail, error) {
|
|
var oSide order.Side
|
|
var oStatus order.Status
|
|
var err error
|
|
orderID := strconv.FormatInt(oContainer.OrderID, 10)
|
|
if oContainer.Side != "" {
|
|
oSide, err = order.StringToOrderSide(oContainer.Side)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
} else if oContainer.Order.Side != "" {
|
|
oSide, err = order.StringToOrderSide(oContainer.Order.Side)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
}
|
|
|
|
oStatus, err = stringToOrderStatus(oContainer.Reply, oContainer.OpenQuantity)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
if oContainer.Status[0] != "OK" {
|
|
return nil, fmt.Errorf("%s - Order rejected: %v", e.Name, oContainer.Status)
|
|
}
|
|
if len(oContainer.Reasons) > 0 {
|
|
return nil, fmt.Errorf("%s - Order rejected: %v", e.Name, oContainer.Reasons)
|
|
}
|
|
|
|
o := &order.Detail{
|
|
Price: oContainer.Price,
|
|
Amount: oContainer.Quantity,
|
|
ExecutedAmount: oContainer.FillQuantity,
|
|
RemainingAmount: oContainer.OpenQuantity,
|
|
Exchange: e.Name,
|
|
OrderID: orderID,
|
|
Side: oSide,
|
|
Status: oStatus,
|
|
Date: oContainer.Timestamp.Time(),
|
|
Trades: nil,
|
|
}
|
|
if oContainer.Reply == "order_filled" {
|
|
o.Side, err = order.StringToOrderSide(oContainer.Order.Side)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
o.RemainingAmount = oContainer.Order.OpenQuantity
|
|
o.Amount = oContainer.Order.Quantity
|
|
o.OrderID = strconv.FormatInt(oContainer.Order.OrderID, 10)
|
|
o.LastUpdated = oContainer.Timestamp.Time()
|
|
o.Pair, o.AssetType, err = e.GetRequestFormattedPairAndAssetType(e.instrumentMap.LookupInstrument(oContainer.Order.InstrumentID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o.Trades = []order.TradeHistory{
|
|
{
|
|
Price: oContainer.FillPrice,
|
|
Amount: oContainer.FillQuantity,
|
|
Exchange: e.Name,
|
|
TID: strconv.FormatInt(oContainer.TransactionID, 10),
|
|
Side: oSide,
|
|
Timestamp: oContainer.Timestamp.Time(),
|
|
},
|
|
}
|
|
} else {
|
|
o.Pair, o.AssetType, err = e.GetRequestFormattedPairAndAssetType(e.instrumentMap.LookupInstrument(oContainer.InstrumentID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
// WsGetInstruments fetches instrument list and propagates a local cache
|
|
func (e *Exchange) WsGetInstruments(ctx context.Context) (Instruments, error) {
|
|
var list Instruments
|
|
req := wsRequest{
|
|
Request: "inst_list",
|
|
SecurityType: strings.ToUpper(asset.Spot.String()),
|
|
Nonce: getNonce(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.Nonce, req)
|
|
if err != nil {
|
|
return list, err
|
|
}
|
|
err = json.Unmarshal(resp, &list)
|
|
if err != nil {
|
|
return list, err
|
|
}
|
|
for curr, data := range list.Instruments {
|
|
e.instrumentMap.Seed(curr, data[0].InstrumentID)
|
|
}
|
|
if len(e.instrumentMap.GetInstrumentIDs()) == 0 {
|
|
return list, errors.New("instrument list failed to populate")
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// WsProcessOrderbookSnapshot processes the orderbook snapshot
|
|
func (e *Exchange) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error {
|
|
bids := make([]orderbook.Level, len(ob.Buy))
|
|
for i := range ob.Buy {
|
|
bids[i] = orderbook.Level{
|
|
Amount: ob.Buy[i].Volume,
|
|
Price: ob.Buy[i].Price,
|
|
}
|
|
}
|
|
|
|
asks := make([]orderbook.Level, len(ob.Sell))
|
|
for i := range ob.Sell {
|
|
asks[i] = orderbook.Level{
|
|
Amount: ob.Sell[i].Volume,
|
|
Price: ob.Sell[i].Price,
|
|
}
|
|
}
|
|
|
|
var newOrderBook orderbook.Book
|
|
newOrderBook.Asks = asks
|
|
newOrderBook.Bids = bids
|
|
newOrderBook.ValidateOrderbook = e.ValidateOrderbook
|
|
|
|
pairs, err := e.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
format, err := e.GetPairFormat(asset.Spot, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newOrderBook.Pair, err = currency.NewPairFromFormattedPairs(
|
|
e.instrumentMap.LookupInstrument(ob.InstID),
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newOrderBook.Asset = asset.Spot
|
|
newOrderBook.Exchange = e.Name
|
|
newOrderBook.LastUpdated = time.Now() // No time sent
|
|
|
|
return e.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
|
}
|
|
|
|
// WsProcessOrderbookUpdate process an orderbook update
|
|
func (e *Exchange) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error {
|
|
pairs, err := e.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
format, err := e.GetPairFormat(asset.Spot, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p, err := currency.NewPairFromFormattedPairs(
|
|
e.instrumentMap.LookupInstrument(update.InstID),
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bufferUpdate := &orderbook.Update{
|
|
Pair: p,
|
|
UpdateID: update.TransID,
|
|
Asset: asset.Spot,
|
|
UpdateTime: time.Now(), // No time sent
|
|
}
|
|
if strings.EqualFold(update.Side, order.Buy.Lower()) {
|
|
bufferUpdate.Bids = []orderbook.Level{{Price: update.Price, Amount: update.Volume}}
|
|
} else {
|
|
bufferUpdate.Asks = []orderbook.Level{{Price: update.Price, Amount: update.Volume}}
|
|
}
|
|
return e.Websocket.Orderbook.Update(bufferUpdate)
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (e *Exchange) GenerateDefaultSubscriptions() (subscription.List, error) {
|
|
channels := []string{"inst_tick", "inst_order_book", "inst_trade"}
|
|
var subscriptions subscription.List
|
|
enabledPairs, err := e.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range channels {
|
|
for j := range enabledPairs {
|
|
subscriptions = append(subscriptions, &subscription.Subscription{
|
|
Channel: channels[i],
|
|
Pairs: currency.Pairs{enabledPairs[j]},
|
|
Asset: asset.Spot,
|
|
})
|
|
}
|
|
}
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (e *Exchange) Subscribe(subs subscription.List) error {
|
|
ctx := context.TODO()
|
|
var errs error
|
|
for _, s := range subs {
|
|
if len(s.Pairs) != 1 {
|
|
return subscription.ErrNotSinglePair
|
|
}
|
|
fPair, err := e.FormatExchangeCurrency(s.Pairs[0], asset.Spot)
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
continue
|
|
}
|
|
|
|
subscribe := wsRequest{
|
|
Request: s.Channel,
|
|
InstrumentID: e.instrumentMap.LookupID(fPair.String()),
|
|
Subscribe: true,
|
|
Nonce: getNonce(),
|
|
}
|
|
err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, subscribe)
|
|
if err == nil {
|
|
err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, s)
|
|
}
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (e *Exchange) Unsubscribe(channelToUnsubscribe subscription.List) error {
|
|
ctx := context.TODO()
|
|
var errs error
|
|
for _, s := range channelToUnsubscribe {
|
|
if len(s.Pairs) != 1 {
|
|
return subscription.ErrNotSinglePair
|
|
}
|
|
fPair, err := e.FormatExchangeCurrency(s.Pairs[0], asset.Spot)
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
continue
|
|
}
|
|
|
|
subscribe := wsRequest{
|
|
Request: s.Channel,
|
|
InstrumentID: e.instrumentMap.LookupID(fPair.String()),
|
|
Subscribe: false,
|
|
Nonce: getNonce(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, subscribe.Nonce, subscribe)
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
continue
|
|
}
|
|
var response map[string]any
|
|
err = json.Unmarshal(resp, &response)
|
|
if err == nil {
|
|
val, ok := response["status"].([]any)
|
|
switch {
|
|
case !ok:
|
|
err = common.GetTypeAssertError("[]any", response["status"])
|
|
case len(val) == 0, val[0] != "OK":
|
|
err = common.AppendError(errs, fmt.Errorf("%v unsubscribe failed for channel %v", e.Name, s.Channel))
|
|
default:
|
|
err = e.Websocket.RemoveSubscriptions(e.Websocket.Conn, s)
|
|
}
|
|
}
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func (e *Exchange) wsAuthenticate(ctx context.Context) error {
|
|
creds, err := e.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r := WsLoginReq{
|
|
Request: "login",
|
|
Username: creds.ClientID,
|
|
Nonce: getNonce(),
|
|
Timestamp: time.Now().Unix(),
|
|
}
|
|
payload := creds.ClientID + "|" + strconv.FormatInt(r.Timestamp, 10) + "|" + strconv.FormatInt(r.Nonce, 10)
|
|
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(creds.Key))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Hmac = hex.EncodeToString(hmac)
|
|
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, r.Nonce, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
respKey, err := jsonparser.GetUnsafeString(resp, "api_key")
|
|
if err != nil || respKey != creds.Key {
|
|
return errors.New("failed to authenticate")
|
|
}
|
|
|
|
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsGetAccountBalance(ctx context.Context) (*UserBalance, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authorised to submit order", e.Name)
|
|
}
|
|
accBalance := wsRequest{
|
|
Request: "user_balance",
|
|
Nonce: getNonce(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, accBalance.Nonce, accBalance)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var response UserBalance
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response.Status[0] != "OK" {
|
|
return &response, fmt.Errorf("%v get account balance failed", e.Name)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
func (e *Exchange) wsSubmitOrder(ctx context.Context, o *WsSubmitOrderParameters) (*order.Detail, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authorised to submit order", e.Name)
|
|
}
|
|
|
|
curr, err := e.FormatExchangeCurrency(o.Currency, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var orderSubmissionRequest WsSubmitOrderRequest
|
|
orderSubmissionRequest.Request = "new_order"
|
|
orderSubmissionRequest.Nonce = getNonce()
|
|
orderSubmissionRequest.InstrumentID = e.instrumentMap.LookupID(curr.String())
|
|
orderSubmissionRequest.Quantity = o.Amount
|
|
orderSubmissionRequest.Price = o.Price
|
|
orderSubmissionRequest.Side = o.Side.String()
|
|
|
|
if o.OrderID > 0 {
|
|
orderSubmissionRequest.OrderID = o.OrderID
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, orderSubmissionRequest.Nonce, orderSubmissionRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incoming wsOrderContainer
|
|
err = json.Unmarshal(resp, &incoming)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var ord *order.Detail
|
|
ord, err = e.parseOrderContainer(&incoming)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ord, nil
|
|
}
|
|
|
|
func (e *Exchange) wsSubmitOrders(ctx context.Context, orders []WsSubmitOrderParameters) ([]order.Detail, []error) {
|
|
var errs []error
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
errs = append(errs, fmt.Errorf("%v not authorised to submit orders",
|
|
e.Name))
|
|
return nil, errs
|
|
}
|
|
orderRequest := WsSubmitOrdersRequest{}
|
|
for i := range orders {
|
|
curr, err := e.FormatExchangeCurrency(orders[i].Currency, asset.Spot)
|
|
if err != nil {
|
|
return nil, []error{err}
|
|
}
|
|
|
|
orderRequest.Orders = append(orderRequest.Orders,
|
|
WsSubmitOrdersRequestData{
|
|
Quantity: orders[i].Amount,
|
|
Price: orders[i].Price,
|
|
Side: orders[i].Side.String(),
|
|
InstrumentID: e.instrumentMap.LookupID(curr.String()),
|
|
ClientOrderID: i + 1,
|
|
})
|
|
}
|
|
|
|
orderRequest.Nonce = getNonce()
|
|
orderRequest.Request = "new_orders"
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, orderRequest.Nonce, orderRequest)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
return nil, errs
|
|
}
|
|
var incoming []wsOrderContainer
|
|
err = json.Unmarshal(resp, &incoming)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
return nil, errs
|
|
}
|
|
|
|
ordersResponse := make([]order.Detail, 0, len(incoming))
|
|
for i := range incoming {
|
|
o, err := e.parseOrderContainer(&incoming[i])
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
ordersResponse = append(ordersResponse, *o)
|
|
}
|
|
|
|
return ordersResponse, errs
|
|
}
|
|
|
|
func (e *Exchange) wsGetOpenOrders(ctx context.Context, curr string) (*WsUserOpenOrdersResponse, error) {
|
|
var response *WsUserOpenOrdersResponse
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return response, fmt.Errorf("%v not authorised to get open orders",
|
|
e.Name)
|
|
}
|
|
var openOrdersRequest WsGetOpenOrdersRequest
|
|
openOrdersRequest.Request = "user_open_orders"
|
|
openOrdersRequest.Nonce = getNonce()
|
|
openOrdersRequest.InstrumentID = e.instrumentMap.LookupID(curr)
|
|
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, openOrdersRequest.Nonce, openOrdersRequest)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
if response.Status[0] != "OK" {
|
|
return response, fmt.Errorf("%v get open orders failed for currency %v",
|
|
e.Name,
|
|
curr)
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (e *Exchange) wsCancelOrder(ctx context.Context, cancellation *WsCancelOrderParameters) (*CancelOrdersResponse, error) {
|
|
var response *CancelOrdersResponse
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return response, fmt.Errorf("%v not authorised to cancel order", e.Name)
|
|
}
|
|
|
|
curr, err := e.FormatExchangeCurrency(cancellation.Currency, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cancellationRequest WsCancelOrderRequest
|
|
cancellationRequest.Request = "cancel_order"
|
|
cancellationRequest.InstrumentID = e.instrumentMap.LookupID(curr.String())
|
|
cancellationRequest.OrderID = cancellation.OrderID
|
|
cancellationRequest.Nonce = getNonce()
|
|
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, cancellationRequest.Nonce, cancellationRequest)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
if response.Status[0] != "OK" {
|
|
return response, fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v",
|
|
e.Name,
|
|
cancellation.Currency,
|
|
cancellation.OrderID,
|
|
response.Status[0])
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (e *Exchange) wsCancelOrders(ctx context.Context, cancellations []WsCancelOrderParameters) (*CancelOrdersResponse, error) {
|
|
var err error
|
|
var response *CancelOrdersResponse
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, err
|
|
}
|
|
var cancelOrderRequest WsCancelOrdersRequest
|
|
for i := range cancellations {
|
|
var curr currency.Pair
|
|
curr, err = e.FormatExchangeCurrency(cancellations[i].Currency,
|
|
asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cancelOrderRequest.Entries = append(cancelOrderRequest.Entries,
|
|
WsCancelOrdersRequestEntry{
|
|
InstID: e.instrumentMap.LookupID(curr.String()),
|
|
OrderID: cancellations[i].OrderID,
|
|
})
|
|
}
|
|
|
|
cancelOrderRequest.Request = "cancel_orders"
|
|
cancelOrderRequest.Nonce = getNonce()
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, cancelOrderRequest.Nonce, cancelOrderRequest)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
return response, err
|
|
}
|
|
|
|
func (e *Exchange) wsGetTradeHistory(ctx context.Context, p currency.Pair, start, limit int64) (*WsTradeHistoryResponse, error) {
|
|
var response *WsTradeHistoryResponse
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return response, fmt.Errorf("%v not authorised to get trade history",
|
|
e.Name)
|
|
}
|
|
|
|
curr, err := e.FormatExchangeCurrency(p, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var req WsTradeHistoryRequest
|
|
req.Request = "trade_history"
|
|
req.InstID = e.instrumentMap.LookupID(curr.String())
|
|
req.Nonce = getNonce()
|
|
req.Start = start
|
|
req.Limit = limit
|
|
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.Nonce, req)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
if response.Status[0] != "OK" {
|
|
return response, fmt.Errorf("%v get trade history failed for %v", e.Name, req)
|
|
}
|
|
return response, nil
|
|
}
|