mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-20 07:26:46 +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>
796 lines
23 KiB
Go
796 lines
23 KiB
Go
package bybit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
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/account"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
bybitWebsocketTimer = 20 * time.Second
|
|
|
|
// Public v5 channels
|
|
chanOrderbook = "orderbook"
|
|
chanPublicTrade = "publicTrade"
|
|
chanPublicTicker = "tickers"
|
|
chanKline = "kline"
|
|
chanLiquidation = "liquidation"
|
|
chanLeverageTokenKline = "kline_lt"
|
|
chanLeverageTokenTicker = "tickers_lt"
|
|
chanLeverageTokenNav = "lt"
|
|
|
|
// Private v5 channels
|
|
chanPositions = "position"
|
|
chanExecution = "execution"
|
|
chanOrder = "order"
|
|
chanWallet = "wallet"
|
|
chanGreeks = "greeks"
|
|
chanDCP = "dcp"
|
|
|
|
spotPublic = "wss://stream.bybit.com/v5/public/spot"
|
|
linearPublic = "wss://stream.bybit.com/v5/public/linear" // USDT, USDC perpetual & USDC Futures
|
|
inversePublic = "wss://stream.bybit.com/v5/public/inverse" // Inverse contract
|
|
optionPublic = "wss://stream.bybit.com/v5/public/option" // USDC Option
|
|
|
|
// Main-net private
|
|
websocketPrivate = "wss://stream.bybit.com/v5/private"
|
|
)
|
|
|
|
var defaultSubscriptions = subscription.List{
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel, Levels: 50},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel},
|
|
{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel, Interval: kline.OneHour},
|
|
{Enabled: true, Asset: asset.Spot, Authenticated: true, Channel: subscription.MyOrdersChannel},
|
|
{Enabled: true, Asset: asset.Spot, Authenticated: true, Channel: subscription.MyWalletChannel},
|
|
{Enabled: true, Asset: asset.Spot, Authenticated: true, Channel: subscription.MyTradesChannel},
|
|
}
|
|
|
|
var subscriptionNames = map[string]string{
|
|
subscription.TickerChannel: chanPublicTicker,
|
|
subscription.OrderbookChannel: chanOrderbook,
|
|
subscription.AllTradesChannel: chanPublicTrade,
|
|
subscription.MyOrdersChannel: chanOrder,
|
|
subscription.MyTradesChannel: chanExecution,
|
|
subscription.MyWalletChannel: chanWallet,
|
|
subscription.CandlesChannel: chanKline,
|
|
}
|
|
|
|
// WsConnect connects to a websocket feed
|
|
func (e *Exchange) WsConnect() error {
|
|
ctx := context.TODO()
|
|
if !e.Websocket.IsEnabled() || !e.IsEnabled() || !e.IsAssetWebsocketSupported(asset.Spot) {
|
|
return websocket.ErrWebsocketNotEnabled
|
|
}
|
|
var dialer gws.Dialer
|
|
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.Conn.SetupPingHandler(request.Unset, websocket.PingHandler{
|
|
MessageType: gws.TextMessage,
|
|
Message: []byte(`{"op": "ping"}`),
|
|
Delay: bybitWebsocketTimer,
|
|
})
|
|
|
|
e.Websocket.Wg.Add(1)
|
|
go e.wsReadData(ctx, asset.Spot, e.Websocket.Conn)
|
|
if e.Websocket.CanUseAuthenticatedEndpoints() {
|
|
err = e.WsAuth(ctx)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsAuth sends an authentication message to receive auth data
|
|
func (e *Exchange) WsAuth(ctx context.Context) error {
|
|
creds, err := e.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var dialer gws.Dialer
|
|
if err := e.Websocket.AuthConn.Dial(ctx, &dialer, http.Header{}); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.Websocket.AuthConn.SetupPingHandler(request.Unset, websocket.PingHandler{
|
|
MessageType: gws.TextMessage,
|
|
Message: []byte(`{"op":"ping"}`),
|
|
Delay: bybitWebsocketTimer,
|
|
})
|
|
|
|
e.Websocket.Wg.Add(1)
|
|
go e.wsReadData(ctx, asset.Spot, e.Websocket.AuthConn)
|
|
|
|
intNonce := time.Now().Add(time.Hour * 6).UnixMilli()
|
|
strNonce := strconv.FormatInt(intNonce, 10)
|
|
hmac, err := crypto.GetHMAC(
|
|
crypto.HashSHA256,
|
|
[]byte("GET/realtime"+strNonce),
|
|
[]byte(creds.Secret),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sign := hex.EncodeToString(hmac)
|
|
req := Authenticate{
|
|
RequestID: strconv.FormatInt(e.Websocket.AuthConn.GenerateMessageID(false), 10),
|
|
Operation: "auth",
|
|
Args: []any{creds.Key, intNonce, sign},
|
|
}
|
|
resp, err := e.Websocket.AuthConn.SendMessageReturnResponse(ctx, request.Unset, req.RequestID, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var response SubscriptionResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !response.Success {
|
|
return fmt.Errorf("%s with request ID %s msg: %s", response.Operation, response.RequestID, response.RetMsg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (e *Exchange) Subscribe(channelsToSubscribe subscription.List) error {
|
|
ctx := context.TODO()
|
|
return e.handleSpotSubscription(ctx, "subscribe", channelsToSubscribe)
|
|
}
|
|
|
|
func (e *Exchange) handleSubscriptions(operation string, subs subscription.List) (args []SubscriptionArgument, err error) {
|
|
subs, err = subs.ExpandTemplates(e)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, list := range []subscription.List{subs.Public(), subs.Private()} {
|
|
for _, b := range common.Batch(list, 10) {
|
|
args = append(args, SubscriptionArgument{
|
|
auth: b[0].Authenticated,
|
|
Operation: operation,
|
|
RequestID: strconv.FormatInt(e.Websocket.Conn.GenerateMessageID(false), 10),
|
|
Arguments: b.QualifiedChannels(),
|
|
associatedSubs: b,
|
|
})
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (e *Exchange) Unsubscribe(channelsToUnsubscribe subscription.List) error {
|
|
ctx := context.TODO()
|
|
return e.handleSpotSubscription(ctx, "unsubscribe", channelsToUnsubscribe)
|
|
}
|
|
|
|
func (e *Exchange) handleSpotSubscription(ctx context.Context, operation string, channelsToSubscribe subscription.List) error {
|
|
payloads, err := e.handleSubscriptions(operation, channelsToSubscribe)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for a := range payloads {
|
|
var response []byte
|
|
if payloads[a].auth {
|
|
response, err = e.Websocket.AuthConn.SendMessageReturnResponse(ctx, request.Unset, payloads[a].RequestID, payloads[a])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
response, err = e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, payloads[a].RequestID, payloads[a])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var resp SubscriptionResponse
|
|
err = json.Unmarshal(response, &resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !resp.Success {
|
|
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.RetMsg)
|
|
}
|
|
|
|
var conn websocket.Connection
|
|
if payloads[a].auth {
|
|
conn = e.Websocket.AuthConn
|
|
} else {
|
|
conn = e.Websocket.Conn
|
|
}
|
|
|
|
if operation == "unsubscribe" {
|
|
err = e.Websocket.RemoveSubscriptions(conn, payloads[a].associatedSubs...)
|
|
} else {
|
|
err = e.Websocket.AddSubscriptions(conn, payloads[a].associatedSubs...)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// generateSubscriptions generates default subscription
|
|
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(template.FuncMap{
|
|
"channelName": channelName,
|
|
"isSymbolChannel": isSymbolChannel,
|
|
"intervalToString": intervalToString,
|
|
"getCategoryName": getCategoryName,
|
|
"isCategorisedChannel": isCategorisedChannel,
|
|
}).Parse(subTplText)
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (e *Exchange) wsReadData(ctx context.Context, assetType asset.Item, ws websocket.Connection) {
|
|
defer e.Websocket.Wg.Done()
|
|
for {
|
|
select {
|
|
case <-e.Websocket.ShutdownC:
|
|
return
|
|
default:
|
|
resp := ws.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
err := e.wsHandleData(ctx, assetType, resp.Raw)
|
|
if err != nil {
|
|
e.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Exchange) wsHandleData(ctx context.Context, assetType asset.Item, respRaw []byte) error {
|
|
var result WebsocketResponse
|
|
err := json.Unmarshal(respRaw, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.Topic == "" {
|
|
switch result.Operation {
|
|
case "subscribe", "unsubscribe", "auth":
|
|
if result.RequestID != "" {
|
|
if !e.Websocket.Match.IncomingWithData(result.RequestID, respRaw) {
|
|
return fmt.Errorf("could not match subscription with id %s data %s", result.RequestID, respRaw)
|
|
}
|
|
}
|
|
case "ping", "pong":
|
|
default:
|
|
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{
|
|
Message: string(respRaw),
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
topicSplit := strings.Split(result.Topic, ".")
|
|
if len(topicSplit) == 0 {
|
|
return errInvalidPushData
|
|
}
|
|
switch topicSplit[0] {
|
|
case chanOrderbook:
|
|
return e.wsProcessOrderbook(assetType, &result)
|
|
case chanPublicTrade:
|
|
return e.wsProcessPublicTrade(assetType, &result)
|
|
case chanPublicTicker:
|
|
return e.wsProcessPublicTicker(assetType, &result)
|
|
case chanKline:
|
|
return e.wsProcessKline(assetType, &result, topicSplit)
|
|
case chanLiquidation:
|
|
return e.wsProcessLiquidation(&result)
|
|
case chanLeverageTokenKline:
|
|
return e.wsProcessLeverageTokenKline(assetType, &result, topicSplit)
|
|
case chanLeverageTokenTicker:
|
|
return e.wsProcessLeverageTokenTicker(assetType, &result)
|
|
case chanLeverageTokenNav:
|
|
return e.wsLeverageTokenNav(&result)
|
|
case chanPositions:
|
|
return e.wsProcessPosition(&result)
|
|
case chanExecution:
|
|
return e.wsProcessExecution(asset.Spot, &result)
|
|
case chanOrder:
|
|
return e.wsProcessOrder(asset.Spot, &result)
|
|
case chanWallet:
|
|
return e.wsProcessWalletPushData(ctx, asset.Spot, respRaw)
|
|
case chanGreeks:
|
|
return e.wsProcessGreeks(respRaw)
|
|
case chanDCP:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unhandled stream data %s", string(respRaw))
|
|
}
|
|
|
|
func (e *Exchange) wsProcessGreeks(resp []byte) error {
|
|
var result GreeksResponse
|
|
err := json.Unmarshal(resp, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- &result
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessWalletPushData(ctx context.Context, assetType asset.Item, resp []byte) error {
|
|
var result WebsocketWallet
|
|
err := json.Unmarshal(resp, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
creds, err := e.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var changes []account.Change
|
|
for x := range result.Data {
|
|
for y := range result.Data[x].Coin {
|
|
changes = append(changes, account.Change{
|
|
AssetType: assetType,
|
|
Balance: &account.Balance{
|
|
Currency: currency.NewCode(result.Data[x].Coin[y].Coin),
|
|
Total: result.Data[x].Coin[y].WalletBalance.Float64(),
|
|
Free: result.Data[x].Coin[y].WalletBalance.Float64(),
|
|
UpdatedAt: result.CreationTime.Time(),
|
|
},
|
|
})
|
|
}
|
|
}
|
|
e.Websocket.DataHandler <- changes
|
|
return account.ProcessChange(e.Name, changes, creds)
|
|
}
|
|
|
|
// wsProcessOrder the order stream to see changes to your orders in real-time.
|
|
func (e *Exchange) wsProcessOrder(assetType asset.Item, resp *WebsocketResponse) error {
|
|
var result WsOrders
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
execution := make([]order.Detail, len(result))
|
|
for x := range result {
|
|
cp, err := e.MatchSymbolWithAvailablePairs(result[x].Symbol, assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
orderType, err := order.StringToOrderType(result[x].OrderType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
side, err := order.StringToOrderSide(result[x].Side)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
execution[x] = order.Detail{
|
|
Amount: result[x].Qty.Float64(),
|
|
Exchange: e.Name,
|
|
OrderID: result[x].OrderID,
|
|
ClientOrderID: result[x].OrderLinkID,
|
|
Side: side,
|
|
Type: orderType,
|
|
Pair: cp,
|
|
Cost: result[x].CumExecQty.Float64() * result[x].AvgPrice.Float64(),
|
|
AssetType: assetType,
|
|
Status: StringToOrderStatus(result[x].OrderStatus),
|
|
Price: result[x].Price.Float64(),
|
|
ExecutedAmount: result[x].CumExecQty.Float64(),
|
|
Date: result[x].CreatedTime.Time(),
|
|
LastUpdated: result[x].UpdatedTime.Time(),
|
|
}
|
|
}
|
|
e.Websocket.DataHandler <- execution
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessExecution(assetType asset.Item, resp *WebsocketResponse) error {
|
|
var result WsExecutions
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
executions := make([]fill.Data, len(result))
|
|
for x := range result {
|
|
cp, err := e.MatchSymbolWithAvailablePairs(result[x].Symbol, assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
side, err := order.StringToOrderSide(result[x].Side)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
executions[x] = fill.Data{
|
|
ID: result[x].ExecID,
|
|
Timestamp: result[x].ExecTime.Time(),
|
|
Exchange: e.Name,
|
|
AssetType: assetType,
|
|
CurrencyPair: cp,
|
|
Side: side,
|
|
OrderID: result[x].OrderID,
|
|
ClientOrderID: result[x].OrderLinkID,
|
|
Price: result[x].ExecPrice.Float64(),
|
|
Amount: result[x].ExecQty.Float64(),
|
|
}
|
|
}
|
|
e.Websocket.DataHandler <- executions
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessPosition(resp *WebsocketResponse) error {
|
|
var result WsPositions
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- result
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsLeverageTokenNav(resp *WebsocketResponse) error {
|
|
var result LTNav
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- result
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessLeverageTokenTicker(assetType asset.Item, resp *WebsocketResponse) error {
|
|
var result TickerItem
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cp, err := e.MatchSymbolWithAvailablePairs(result.Symbol, assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- &ticker.Price{
|
|
Last: result.LastPrice.Float64(),
|
|
High: result.HighPrice24H.Float64(),
|
|
Low: result.LowPrice24H.Float64(),
|
|
Pair: cp,
|
|
ExchangeName: e.Name,
|
|
AssetType: assetType,
|
|
LastUpdated: resp.PushTimestamp.Time(),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessLeverageTokenKline(assetType asset.Item, resp *WebsocketResponse, topicSplit []string) error {
|
|
var result LTKlines
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cp, err := e.MatchSymbolWithAvailablePairs(topicSplit[2], assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ltKline := make([]websocket.KlineData, len(result))
|
|
for x := range result {
|
|
interval, err := stringToInterval(result[x].Interval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ltKline[x] = websocket.KlineData{
|
|
Timestamp: result[x].Timestamp.Time(),
|
|
Pair: cp,
|
|
AssetType: assetType,
|
|
Exchange: e.Name,
|
|
StartTime: result[x].Start.Time(),
|
|
CloseTime: result[x].End.Time(),
|
|
Interval: interval.String(),
|
|
OpenPrice: result[x].Open.Float64(),
|
|
ClosePrice: result[x].Close.Float64(),
|
|
HighPrice: result[x].High.Float64(),
|
|
LowPrice: result[x].Low.Float64(),
|
|
}
|
|
}
|
|
e.Websocket.DataHandler <- result
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessLiquidation(resp *WebsocketResponse) error {
|
|
var result WebsocketLiquidation
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Websocket.DataHandler <- result
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessKline(assetType asset.Item, resp *WebsocketResponse, topicSplit []string) error {
|
|
var result WsKlines
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cp, err := e.MatchSymbolWithAvailablePairs(topicSplit[2], assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spotCandlesticks := make([]websocket.KlineData, len(result))
|
|
for x := range result {
|
|
interval, err := stringToInterval(result[x].Interval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spotCandlesticks[x] = websocket.KlineData{
|
|
Timestamp: result[x].Timestamp.Time(),
|
|
Pair: cp,
|
|
AssetType: assetType,
|
|
Exchange: e.Name,
|
|
StartTime: result[x].Start.Time(),
|
|
CloseTime: result[x].End.Time(),
|
|
Interval: interval.String(),
|
|
OpenPrice: result[x].Open.Float64(),
|
|
ClosePrice: result[x].Close.Float64(),
|
|
HighPrice: result[x].High.Float64(),
|
|
LowPrice: result[x].Low.Float64(),
|
|
Volume: result[x].Volume.Float64(),
|
|
}
|
|
}
|
|
e.Websocket.DataHandler <- spotCandlesticks
|
|
return nil
|
|
}
|
|
|
|
func (e *Exchange) wsProcessPublicTicker(assetType asset.Item, resp *WebsocketResponse) error {
|
|
tickResp := new(TickerItem)
|
|
if err := json.Unmarshal(resp.Data, tickResp); err != nil {
|
|
return err
|
|
}
|
|
|
|
p, err := e.MatchSymbolWithAvailablePairs(tickResp.Symbol, assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pFmt, err := e.GetPairFormat(assetType, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p = p.Format(pFmt)
|
|
|
|
var tick *ticker.Price
|
|
if resp.Type == "snapshot" {
|
|
tick = &ticker.Price{
|
|
Pair: p,
|
|
ExchangeName: e.Name,
|
|
AssetType: assetType,
|
|
}
|
|
} else {
|
|
// ticker updates may be partial, so we need to update the current ticker
|
|
tick, err = ticker.GetTicker(e.Name, p, assetType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
updateTicker(tick, tickResp)
|
|
tick.LastUpdated = resp.PushTimestamp.Time()
|
|
|
|
if err = ticker.ProcessTicker(tick); err == nil {
|
|
e.Websocket.DataHandler <- tick
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func updateTicker(tick *ticker.Price, resp *TickerItem) {
|
|
if resp.LastPrice.Float64() != 0 {
|
|
tick.Last = resp.LastPrice.Float64()
|
|
}
|
|
if resp.HighPrice24H.Float64() != 0 {
|
|
tick.High = resp.HighPrice24H.Float64()
|
|
}
|
|
if resp.LowPrice24H.Float64() != 0 {
|
|
tick.Low = resp.LowPrice24H.Float64()
|
|
}
|
|
if resp.Volume24H.Float64() != 0 {
|
|
tick.Volume = resp.Volume24H.Float64()
|
|
}
|
|
|
|
if tick.AssetType == asset.Spot {
|
|
return
|
|
}
|
|
|
|
if resp.MarkPrice.Float64() != 0 {
|
|
tick.MarkPrice = resp.MarkPrice.Float64()
|
|
}
|
|
if resp.IndexPrice.Float64() != 0 {
|
|
tick.IndexPrice = resp.IndexPrice.Float64()
|
|
}
|
|
if resp.OpenInterest.Float64() != 0 {
|
|
tick.OpenInterest = resp.OpenInterest.Float64()
|
|
}
|
|
|
|
switch tick.AssetType {
|
|
case asset.Options:
|
|
if resp.BidPrice.Float64() != 0 {
|
|
tick.Bid = resp.BidPrice.Float64()
|
|
}
|
|
if resp.BidSize.Float64() != 0 {
|
|
tick.BidSize = resp.BidSize.Float64()
|
|
}
|
|
if resp.AskPrice.Float64() != 0 {
|
|
tick.Ask = resp.AskPrice.Float64()
|
|
}
|
|
if resp.AskSize.Float64() != 0 {
|
|
tick.AskSize = resp.AskSize.Float64()
|
|
}
|
|
case asset.USDCMarginedFutures, asset.USDTMarginedFutures, asset.CoinMarginedFutures:
|
|
if resp.Bid1Price.Float64() != 0 {
|
|
tick.Bid = resp.Bid1Price.Float64()
|
|
}
|
|
if resp.Bid1Size.Float64() != 0 {
|
|
tick.BidSize = resp.Bid1Size.Float64()
|
|
}
|
|
if resp.Ask1Price.Float64() != 0 {
|
|
tick.Ask = resp.Ask1Price.Float64()
|
|
}
|
|
if resp.Ask1Size.Float64() != 0 {
|
|
tick.AskSize = resp.Ask1Size.Float64()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Exchange) wsProcessPublicTrade(assetType asset.Item, resp *WebsocketResponse) error {
|
|
var result WebsocketPublicTrades
|
|
err := json.Unmarshal(resp.Data, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tradeDatas := make([]trade.Data, len(result))
|
|
for x := range result {
|
|
cp, err := e.MatchSymbolWithAvailablePairs(result[x].Symbol, assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
side, err := order.StringToOrderSide(result[x].Side)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tradeDatas[x] = trade.Data{
|
|
Timestamp: result[x].OrderFillTimestamp.Time(),
|
|
CurrencyPair: cp,
|
|
AssetType: assetType,
|
|
Exchange: e.Name,
|
|
Price: result[x].Price.Float64(),
|
|
Amount: result[x].Size.Float64(),
|
|
Side: side,
|
|
TID: result[x].TradeID,
|
|
}
|
|
}
|
|
return trade.AddTradesToBuffer(tradeDatas...)
|
|
}
|
|
|
|
func (e *Exchange) wsProcessOrderbook(assetType asset.Item, resp *WebsocketResponse) error {
|
|
var result WsOrderbookDetail
|
|
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
|
return err
|
|
}
|
|
if len(result.Bids) == 0 && len(result.Asks) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cp, err := e.MatchSymbolWithAvailablePairs(result.Symbol, assetType, hasPotentialDelimiter(assetType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
asks := make([]orderbook.Level, len(result.Asks))
|
|
for i := range result.Asks {
|
|
asks[i].Price = result.Asks[i][0].Float64()
|
|
asks[i].Amount = result.Asks[i][1].Float64()
|
|
}
|
|
bids := make([]orderbook.Level, len(result.Bids))
|
|
for i := range result.Bids {
|
|
bids[i].Price = result.Bids[i][0].Float64()
|
|
bids[i].Amount = result.Bids[i][1].Float64()
|
|
}
|
|
|
|
if resp.Type == "snapshot" {
|
|
return e.Websocket.Orderbook.LoadSnapshot(&orderbook.Book{
|
|
Pair: cp,
|
|
Exchange: e.Name,
|
|
Asset: assetType,
|
|
LastUpdated: resp.OrderbookLastUpdated.Time(),
|
|
LastUpdateID: result.UpdateID,
|
|
LastPushed: resp.PushTimestamp.Time(),
|
|
Asks: asks,
|
|
Bids: bids,
|
|
})
|
|
}
|
|
return e.Websocket.Orderbook.Update(&orderbook.Update{
|
|
Pair: cp,
|
|
Asks: asks,
|
|
Bids: bids,
|
|
Asset: assetType,
|
|
UpdateID: result.UpdateID,
|
|
UpdateTime: resp.OrderbookLastUpdated.Time(),
|
|
LastPushed: resp.PushTimestamp.Time(),
|
|
})
|
|
}
|
|
|
|
// channelName converts global channel names to exchange specific names
|
|
func channelName(s *subscription.Subscription) string {
|
|
if name, ok := subscriptionNames[s.Channel]; ok {
|
|
return name
|
|
}
|
|
return s.Channel
|
|
}
|
|
|
|
// isSymbolChannel returns whether the channel accepts a symbol parameter
|
|
func isSymbolChannel(name string) bool {
|
|
switch name {
|
|
case chanPositions, chanExecution, chanOrder, chanDCP, chanWallet:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isCategorisedChannel(name string) bool {
|
|
switch name {
|
|
case chanPositions, chanExecution, chanOrder:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const subTplText = `
|
|
{{ with $name := channelName $.S }}
|
|
{{- range $asset, $pairs := $.AssetPairs }}
|
|
{{- if isSymbolChannel $name }}
|
|
{{- range $p := $pairs }}
|
|
{{- $name -}} .
|
|
{{- if eq $name "orderbook" -}} {{- $.S.Levels -}} . {{- end }}
|
|
{{- if eq $name "kline" -}} {{- intervalToString $.S.Interval -}} . {{- end }}
|
|
{{- $p }}
|
|
{{- $.PairSeparator }}
|
|
{{- end }}
|
|
{{- else }}
|
|
{{- $name }}
|
|
{{- if and (isCategorisedChannel $name) ($categoryName := getCategoryName $asset) -}} . {{- $categoryName -}} {{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- $.AssetSeparator }}
|
|
{{- end }}
|
|
`
|
|
|
|
// hasPotentialDelimiter returns if the asset has a potential delimiter on the pairs being returned.
|
|
func hasPotentialDelimiter(a asset.Item) bool {
|
|
return a == asset.Options || a == asset.USDCMarginedFutures
|
|
}
|