mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* Exchanges: Remove example BespokeGenerateMessageID * Okx: Replace conn.RequestIDGenerator with MesssageID Continued overall direction to remove the closed-loop of e => conn => e roundtrip for message ids * Exchanges: Add MessageSequence This method removes the either/or nature of message id generation. We don't tie the message ids to connections, or to anything. Consumers just call whichever they want, or even combine them as they want. Anything more complicated will need a separate installation anyway * GateIO: Split usage of MessageID and MessageSequence * Binance: Switch to UUID message IDs * Kraken: Switch to e.MessageSequence * Kucoin: Switch to MessageID * HitBTC: Switch to UUIDv7 for ws message ID * Bybit: Switch to UUIDv7 for ws message ID * Bitfinex: Switch to UUIDv7 and MessageSequence Tested CID - It accepts 53 bits only for an int, so MessageSequence makes sense. Can't use MessageID * Websocket: Remove now unused MessageID function Moved all MessageID usage into funcs and onto base methods, to remove the closed loop of message IDs * Docs: Update guidance for message signatures
856 lines
24 KiB
Go
856 lines
24 KiB
Go
package hitbtc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/Masterminds/sprig/v3"
|
|
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/kline"
|
|
"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 (
|
|
hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
|
|
rpcVersion = "2.0"
|
|
errAuthFailed = 1002
|
|
)
|
|
|
|
var subscriptionNames = map[string]string{
|
|
subscription.TickerChannel: "Ticker",
|
|
subscription.OrderbookChannel: "Orderbook",
|
|
subscription.CandlesChannel: "Candles",
|
|
subscription.AllTradesChannel: "Trades",
|
|
subscription.MyAccountChannel: "Reports",
|
|
}
|
|
|
|
var defaultSubscriptions = subscription.List{
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel, Interval: kline.ThirtyMin, Levels: 100},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel, Levels: 100},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.MyAccountChannel, Authenticated: true},
|
|
}
|
|
|
|
// WsConnect starts a new connection with the websocket API
|
|
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()
|
|
|
|
if e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
err = e.wsLogin(ctx)
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", e.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (e *Exchange) wsReadData() {
|
|
defer e.Websocket.Wg.Done()
|
|
|
|
for {
|
|
resp := e.Websocket.Conn.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
|
|
err := e.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Exchange) wsGetTableName(respRaw []byte) (string, error) {
|
|
var init capture
|
|
err := json.Unmarshal(respRaw, &init)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if init.Error.Code == errAuthFailed {
|
|
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
if init.ID != "" {
|
|
if e.Websocket.Match.IncomingWithData(init.ID, respRaw) {
|
|
return "", nil
|
|
}
|
|
}
|
|
if init.Error.Message != "" || init.Error.Code != 0 {
|
|
return "", fmt.Errorf("code: %d, Message: %s",
|
|
init.Error.Code,
|
|
init.Error.Message)
|
|
}
|
|
if _, ok := init.Result.(bool); ok {
|
|
return "", nil
|
|
}
|
|
if init.Method != "" {
|
|
return init.Method, nil
|
|
}
|
|
switch resultType := init.Result.(type) {
|
|
case map[string]any:
|
|
if reportType, ok := resultType["reportType"].(string); ok {
|
|
return reportType, nil
|
|
}
|
|
// check for ids - means it was a specific request
|
|
// and can't go through normal processing
|
|
if responseID, ok := resultType["id"].(string); ok {
|
|
if responseID != "" {
|
|
return "", nil
|
|
}
|
|
}
|
|
case []any:
|
|
if len(resultType) == 0 {
|
|
e.Websocket.DataHandler <- fmt.Sprintf("No data returned. ID: %v", init.ID)
|
|
return "", nil
|
|
}
|
|
|
|
data, ok := resultType[0].(map[string]any)
|
|
if !ok {
|
|
return "", errors.New("unable to type assert data")
|
|
}
|
|
if _, ok := data["clientOrderId"]; ok {
|
|
return "order", nil
|
|
} else if _, ok := data["available"]; ok {
|
|
return "trading", nil
|
|
}
|
|
}
|
|
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
|
|
return "", nil
|
|
}
|
|
|
|
func (e *Exchange) wsHandleData(respRaw []byte) error {
|
|
name, err := e.wsGetTableName(respRaw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch name {
|
|
case "":
|
|
return nil
|
|
case "ticker":
|
|
var wsTicker WsTicker
|
|
err := json.Unmarshal(respRaw, &wsTicker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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(wsTicker.Params.Symbol,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
e.Websocket.DataHandler <- &ticker.Price{
|
|
ExchangeName: e.Name,
|
|
Open: wsTicker.Params.Open,
|
|
Volume: wsTicker.Params.Volume,
|
|
QuoteVolume: wsTicker.Params.VolumeQuote,
|
|
High: wsTicker.Params.High,
|
|
Low: wsTicker.Params.Low,
|
|
Bid: wsTicker.Params.Bid,
|
|
Ask: wsTicker.Params.Ask,
|
|
Last: wsTicker.Params.Last,
|
|
LastUpdated: wsTicker.Params.Timestamp,
|
|
AssetType: asset.Spot,
|
|
Pair: p,
|
|
}
|
|
case "snapshotOrderbook":
|
|
var obSnapshot WsOrderbook
|
|
err := json.Unmarshal(respRaw, &obSnapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = e.WsProcessOrderbookSnapshot(&obSnapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "updateOrderbook":
|
|
var obUpdate WsOrderbook
|
|
err := json.Unmarshal(respRaw, &obUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = e.WsProcessOrderbookUpdate(&obUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "snapshotTrades", "updateTrades":
|
|
if !e.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
var tradeSnapshot WsTrade
|
|
err := json.Unmarshal(respRaw, &tradeSnapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var trades []trade.Data
|
|
p, err := currency.NewPairFromString(tradeSnapshot.Params.Symbol)
|
|
if err != nil {
|
|
return &order.ClassificationError{
|
|
Exchange: e.Name,
|
|
Err: err,
|
|
}
|
|
}
|
|
for i := range tradeSnapshot.Params.Data {
|
|
side, err := order.StringToOrderSide(tradeSnapshot.Params.Data[i].Side)
|
|
if err != nil {
|
|
return &order.ClassificationError{
|
|
Exchange: e.Name,
|
|
Err: err,
|
|
}
|
|
}
|
|
trades = append(trades, trade.Data{
|
|
Timestamp: tradeSnapshot.Params.Data[i].Timestamp,
|
|
Exchange: e.Name,
|
|
CurrencyPair: p,
|
|
AssetType: asset.Spot,
|
|
Price: tradeSnapshot.Params.Data[i].Price,
|
|
Amount: tradeSnapshot.Params.Data[i].Quantity,
|
|
Side: side,
|
|
TID: strconv.FormatInt(tradeSnapshot.Params.Data[i].ID, 10),
|
|
})
|
|
}
|
|
return trade.AddTradesToBuffer(trades...)
|
|
case "activeOrders":
|
|
var o wsActiveOrdersResponse
|
|
err := json.Unmarshal(respRaw, &o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range o.Params {
|
|
err = e.wsHandleOrderData(&o.Params[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "trading":
|
|
var trades WsGetTradingBalanceResponse
|
|
err := json.Unmarshal(respRaw, &trades)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- trades
|
|
case "report":
|
|
var o wsReportResponse
|
|
err := json.Unmarshal(respRaw, &o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = e.wsHandleOrderData(&o.OrderData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "order":
|
|
var o wsActiveOrderRequestResponse
|
|
err := json.Unmarshal(respRaw, &o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range o.OrderData {
|
|
err = e.wsHandleOrderData(&o.OrderData[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "replaced", "canceled", "new":
|
|
var o wsOrderResponse
|
|
err := json.Unmarshal(respRaw, &o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = e.wsHandleOrderData(&o.OrderData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache
|
|
func (e *Exchange) WsProcessOrderbookSnapshot(ob *WsOrderbook) error {
|
|
if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 {
|
|
return errors.New("no orderbooks to process")
|
|
}
|
|
|
|
newOrderBook := orderbook.Book{
|
|
Bids: make(orderbook.Levels, len(ob.Params.Bid)),
|
|
Asks: make(orderbook.Levels, len(ob.Params.Ask)),
|
|
}
|
|
for i := range ob.Params.Bid {
|
|
newOrderBook.Bids[i] = orderbook.Level{
|
|
Amount: ob.Params.Bid[i].Size,
|
|
Price: ob.Params.Bid[i].Price,
|
|
}
|
|
}
|
|
for i := range ob.Params.Ask {
|
|
newOrderBook.Asks[i] = orderbook.Level{
|
|
Amount: ob.Params.Ask[i].Size,
|
|
Price: ob.Params.Ask[i].Price,
|
|
}
|
|
}
|
|
|
|
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(ob.Params.Symbol,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
return err
|
|
}
|
|
|
|
newOrderBook.Asset = asset.Spot
|
|
newOrderBook.Pair = p
|
|
newOrderBook.Exchange = e.Name
|
|
newOrderBook.ValidateOrderbook = e.ValidateOrderbook
|
|
newOrderBook.LastUpdated = ob.Params.Timestamp
|
|
|
|
return e.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
|
}
|
|
|
|
func (e *Exchange) wsHandleOrderData(o *wsOrderData) error {
|
|
var trades []order.TradeHistory
|
|
if o.TradeID > 0 {
|
|
trades = append(trades, order.TradeHistory{
|
|
Price: o.TradePrice,
|
|
Amount: o.TradeQuantity,
|
|
Fee: o.TradeFee,
|
|
Exchange: e.Name,
|
|
TID: strconv.FormatFloat(o.TradeID, 'f', -1, 64),
|
|
Timestamp: o.UpdatedAt,
|
|
})
|
|
}
|
|
oType, err := order.StringToOrderType(o.Type)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: o.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
o.Status = strings.Replace(o.Status, "canceled", "cancelled", 1)
|
|
oStatus, err := order.StringToOrderStatus(o.Status)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: o.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
oSide, err := order.StringToOrderSide(o.Side)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: o.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
p, err := currency.NewPairFromString(o.Symbol)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: e.Name,
|
|
OrderID: o.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
var a asset.Item
|
|
a, err = e.GetPairAssetType(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- &order.Detail{
|
|
Price: o.Price,
|
|
Amount: o.Quantity,
|
|
ExecutedAmount: o.CumQuantity,
|
|
RemainingAmount: o.Quantity - o.CumQuantity,
|
|
Exchange: e.Name,
|
|
OrderID: o.ID,
|
|
Type: oType,
|
|
Side: oSide,
|
|
Status: oStatus,
|
|
AssetType: a,
|
|
Date: o.CreatedAt,
|
|
LastUpdated: o.UpdatedAt,
|
|
Pair: p,
|
|
Trades: trades,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsProcessOrderbookUpdate updates a local cache
|
|
func (e *Exchange) WsProcessOrderbookUpdate(update *WsOrderbook) error {
|
|
if len(update.Params.Bid) == 0 && len(update.Params.Ask) == 0 {
|
|
// Periodically HitBTC sends empty updates which includes a sequence
|
|
// can return this as nil.
|
|
return nil
|
|
}
|
|
|
|
bids := make(orderbook.Levels, len(update.Params.Bid))
|
|
for i := range update.Params.Bid {
|
|
bids[i] = orderbook.Level{
|
|
Price: update.Params.Bid[i].Price,
|
|
Amount: update.Params.Bid[i].Size,
|
|
}
|
|
}
|
|
|
|
asks := make(orderbook.Levels, len(update.Params.Ask))
|
|
for i := range update.Params.Ask {
|
|
asks[i] = orderbook.Level{
|
|
Price: update.Params.Ask[i].Price,
|
|
Amount: update.Params.Ask[i].Size,
|
|
}
|
|
}
|
|
|
|
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(update.Params.Symbol,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.Websocket.Orderbook.Update(&orderbook.Update{
|
|
Asks: asks,
|
|
Bids: bids,
|
|
Pair: p,
|
|
UpdateID: update.Params.Sequence,
|
|
Asset: asset.Spot,
|
|
UpdateTime: update.Params.Timestamp,
|
|
})
|
|
}
|
|
|
|
// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature
|
|
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
|
|
return e.Features.Subscriptions.ExpandTemplates(e)
|
|
}
|
|
|
|
// GetSubscriptionTemplate returns a subscription channel template
|
|
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
|
|
return template.New("master.tmpl").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
|
|
"subToReq": subToReq,
|
|
"isSymbolChannel": isSymbolChannel,
|
|
}).Parse(subTplText)
|
|
}
|
|
|
|
const (
|
|
subscribeOp = "subscribe"
|
|
unsubscribeOp = "unsubscribe"
|
|
)
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (e *Exchange) Subscribe(subs subscription.List) error {
|
|
ctx := context.TODO()
|
|
return e.ParallelChanOp(ctx, subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, subscribeOp, subs) }, 1)
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (e *Exchange) Unsubscribe(subs subscription.List) error {
|
|
ctx := context.TODO()
|
|
return e.ParallelChanOp(ctx, subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, unsubscribeOp, subs) }, 1)
|
|
}
|
|
|
|
func (e *Exchange) manageSubs(ctx context.Context, op string, subs subscription.List) error {
|
|
var errs error
|
|
subs, errs = subs.ExpandTemplates(e)
|
|
for _, s := range subs {
|
|
r := WsRequest{
|
|
JSONRPCVersion: rpcVersion,
|
|
ID: e.MessageID(),
|
|
}
|
|
if err := json.Unmarshal([]byte(s.QualifiedChannel), &r); err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
continue
|
|
}
|
|
r.Method = op + r.Method
|
|
err := e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, r) // v2 api does not return an ID with errors, so we don't use ReturnResponse
|
|
if err == nil {
|
|
if op == subscribeOp {
|
|
err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, s)
|
|
} else {
|
|
err = e.Websocket.RemoveSubscriptions(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) wsLogin(ctx context.Context) error {
|
|
if !e.IsWebsocketAuthenticationSupported() {
|
|
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", e.Name)
|
|
}
|
|
creds, err := e.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
n := strconv.FormatInt(time.Now().Unix(), 10)
|
|
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(n), []byte(creds.Secret))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := WsLoginRequest{
|
|
Method: "login",
|
|
Params: WsLoginData{
|
|
Algo: "HS256",
|
|
PKey: creds.Key,
|
|
Nonce: n,
|
|
Signature: hex.EncodeToString(hmac),
|
|
},
|
|
ID: e.MessageID(),
|
|
}
|
|
|
|
err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, req)
|
|
if err != nil {
|
|
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wsPlaceOrder sends a websocket message to submit an order
|
|
func (e *Exchange) wsPlaceOrder(ctx context.Context, pair currency.Pair, side string, price, quantity float64) (*WsSubmitOrderSuccessResponse, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
|
|
}
|
|
|
|
id := e.MessageID()
|
|
fPair, err := e.FormatExchangeCurrency(pair, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := WsSubmitOrderRequest{
|
|
Method: "newOrder",
|
|
Params: WsSubmitOrderRequestData{
|
|
ClientOrderID: id,
|
|
Symbol: fPair.String(),
|
|
Side: strings.ToLower(side),
|
|
Price: price,
|
|
Quantity: quantity,
|
|
},
|
|
ID: id,
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, id, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsSubmitOrderSuccessResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsCancelOrder sends a websocket message to cancel an order
|
|
func (e *Exchange) wsCancelOrder(ctx context.Context, clientOrderID string) (*WsCancelOrderResponse, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
|
|
}
|
|
req := WsCancelOrderRequest{
|
|
Method: "cancelOrder",
|
|
Params: WsCancelOrderRequestData{
|
|
ClientOrderID: clientOrderID,
|
|
},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsCancelOrderResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsReplaceOrder sends a websocket message to replace an order
|
|
func (e *Exchange) wsReplaceOrder(ctx context.Context, clientOrderID string, quantity, price float64) (*WsReplaceOrderResponse, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
|
|
}
|
|
req := WsReplaceOrderRequest{
|
|
Method: "cancelReplaceOrder",
|
|
Params: WsReplaceOrderRequestData{
|
|
ClientOrderID: clientOrderID,
|
|
RequestClientID: strconv.FormatInt(time.Now().Unix(), 10),
|
|
Quantity: quantity,
|
|
Price: price,
|
|
},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsReplaceOrderResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsGetActiveOrders sends a websocket message to get all active orders
|
|
func (e *Exchange) wsGetActiveOrders(ctx context.Context) (*wsActiveOrdersResponse, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated, cannot get active orders", e.Name)
|
|
}
|
|
req := WsReplaceOrderRequest{
|
|
Method: "getOrders",
|
|
Params: WsReplaceOrderRequestData{},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response wsActiveOrdersResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsGetTradingBalance sends a websocket message to get trading balance
|
|
func (e *Exchange) wsGetTradingBalance(ctx context.Context) (*WsGetTradingBalanceResponse, error) {
|
|
if !e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
|
|
}
|
|
req := WsReplaceOrderRequest{
|
|
Method: "getTradingBalance",
|
|
Params: WsReplaceOrderRequestData{},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsGetTradingBalanceResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsGetCurrencies sends a websocket message to get trading balance
|
|
func (e *Exchange) wsGetCurrencies(ctx context.Context, currencyItem currency.Code) (*WsGetCurrenciesResponse, error) {
|
|
req := WsGetCurrenciesRequest{
|
|
Method: "getCurrency",
|
|
Params: WsGetCurrenciesRequestParameters{
|
|
Currency: currencyItem,
|
|
},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsGetCurrenciesResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsGetSymbols sends a websocket message to get trading balance
|
|
func (e *Exchange) wsGetSymbols(ctx context.Context, c currency.Pair) (*WsGetSymbolsResponse, error) {
|
|
fPair, err := e.FormatExchangeCurrency(c, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := WsGetSymbolsRequest{
|
|
Method: "getSymbol",
|
|
Params: WsGetSymbolsRequestParameters{
|
|
Symbol: fPair.String(),
|
|
},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsGetSymbolsResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// wsGetSymbols sends a websocket message to get trading balance
|
|
func (e *Exchange) wsGetTrades(ctx context.Context, c currency.Pair, limit int64, sort, by string) (*WsGetTradesResponse, error) {
|
|
fPair, err := e.FormatExchangeCurrency(c, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := WsGetTradesRequest{
|
|
Method: "getTrades",
|
|
Params: WsGetTradesRequestParameters{
|
|
Symbol: fPair.String(),
|
|
Limit: limit,
|
|
Sort: sort,
|
|
By: by,
|
|
},
|
|
ID: e.MessageID(),
|
|
}
|
|
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
var response WsGetTradesResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %v", e.Name, err)
|
|
}
|
|
if response.Error.Code > 0 || response.Error.Message != "" {
|
|
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// subToReq returns the subscription as a map to populate WsRequest
|
|
func subToReq(s *subscription.Subscription, maybePair ...currency.Pair) *WsRequest {
|
|
name, ok := subscriptionNames[s.Channel]
|
|
if !ok {
|
|
panic(fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel))
|
|
}
|
|
r := &WsRequest{
|
|
Method: name,
|
|
}
|
|
if len(maybePair) != 0 {
|
|
r.Params = &WsParams{
|
|
Symbol: maybePair[0].String(),
|
|
Limit: s.Levels,
|
|
}
|
|
if s.Interval != 0 {
|
|
var err error
|
|
if r.Params.Period, err = formatExchangeKlineInterval(s.Interval); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
} else if s.Levels != 0 {
|
|
r.Params = &WsParams{
|
|
Limit: s.Levels,
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// isSymbolChannel returns if the channel expects receive a symbol
|
|
func isSymbolChannel(s *subscription.Subscription) bool {
|
|
return s.Channel != subscription.MyAccountChannel
|
|
}
|
|
|
|
const subTplText = `
|
|
{{- if isSymbolChannel $.S }}
|
|
{{ range $asset, $pairs := $.AssetPairs }}
|
|
{{- range $p := $pairs -}}
|
|
{{- subToReq $.S $p | mustToJson }}
|
|
{{ $.PairSeparator }}
|
|
{{- end }}
|
|
{{ $.AssetSeparator }}
|
|
{{- end }}
|
|
{{- else }}
|
|
{{- subToReq $.S | mustToJson }}
|
|
{{- end }}
|
|
`
|