mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-16 23:16:48 +00:00
* common: adjust common error slice to allow multi errors.Is matching and conform to interface better * zb: forgot to save? * linties: fixies * linties: word change as well. * nitters: glorious * buts * nitters: fix glorious bug * Update common/common.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * nitters: shifty --------- Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io> Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
625 lines
17 KiB
Go
625 lines
17 KiB
Go
package bittrex
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
|
"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/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
const (
|
|
bittrexAPIWSURL = "wss://socket-v3.bittrex.com/signalr"
|
|
bittrexAPIWSNegotiationsURL = "https://socket-v3.bittrex.com/signalr"
|
|
|
|
bittrexWebsocketTimer = 13 * time.Second
|
|
wsTicker = "ticker"
|
|
wsOrderbook = "orderbook"
|
|
wsMarketSummary = "market_summary"
|
|
wsOrders = "order"
|
|
wsHeartbeat = "heartbeat"
|
|
authenticate = "Authenticate"
|
|
subscribe = "subscribe"
|
|
unsubscribe = "unsubscribe"
|
|
wsRateLimit = 50
|
|
wsMessageRateLimit = 60
|
|
)
|
|
|
|
var defaultSpotSubscribedChannels = []string{
|
|
// wsHeartbeat,
|
|
wsOrderbook,
|
|
wsTicker,
|
|
wsMarketSummary,
|
|
}
|
|
|
|
var defaultSpotSubscribedChannelsAuth = []string{
|
|
wsOrders,
|
|
}
|
|
|
|
// TickerCache holds ticker and market summary data
|
|
// in order to combine them when processing data
|
|
type TickerCache struct {
|
|
MarketSummaries map[string]*MarketSummaryData
|
|
Tickers map[string]*TickerData
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// WsConnect connects to a websocket feed
|
|
func (b *Bittrex) WsConnect() error {
|
|
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
|
return errors.New(stream.WebsocketNotEnabled)
|
|
}
|
|
|
|
var wsHandshakeData WsSignalRHandshakeData
|
|
err := b.WsSignalRHandshake(context.TODO(), &wsHandshakeData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var dialer websocket.Dialer
|
|
endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("clientProtocol", "1.5")
|
|
params.Set("transport", "webSockets")
|
|
params.Set("connectionToken", wsHandshakeData.ConnectionToken)
|
|
params.Set("connectionData", "[{name:\"c3\"}]")
|
|
params.Set("tid", "10")
|
|
|
|
path := common.EncodeURLValues("/connect", params)
|
|
|
|
err = b.Websocket.SetWebsocketURL(endpoint+path, false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.Websocket.Conn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Can set up custom ping handler per websocket connection.
|
|
b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
|
|
MessageType: websocket.PingMessage,
|
|
Delay: bittrexWebsocketTimer,
|
|
})
|
|
|
|
// This reader routine is called prior to initiating a subscription for
|
|
// efficient processing.
|
|
b.Websocket.Wg.Add(1)
|
|
go b.wsReadData()
|
|
|
|
b.setupOrderbookManager()
|
|
b.tickerCache = &TickerCache{
|
|
MarketSummaries: make(map[string]*MarketSummaryData),
|
|
Tickers: make(map[string]*TickerData),
|
|
}
|
|
|
|
if b.IsWebsocketAuthenticationSupported() {
|
|
err = b.WsAuth(context.TODO())
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- err
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsSignalRHandshake requests the SignalR connection token over https
|
|
func (b *Bittrex) WsSignalRHandshake(ctx context.Context, result interface{}) error {
|
|
endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpotSupplementary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := "/negotiate?connectionData=[{name:\"c3\"}]&clientProtocol=1.5"
|
|
item := &request.Item{
|
|
Method: http.MethodGet,
|
|
Path: endpoint + path,
|
|
Result: result,
|
|
Verbose: b.Verbose,
|
|
HTTPDebugging: b.HTTPDebugging,
|
|
HTTPRecording: b.HTTPRecording,
|
|
}
|
|
return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
|
|
return item, nil
|
|
})
|
|
}
|
|
|
|
// WsAuth sends an authentication message to receive auth data
|
|
// Authentications expire after 10 minutes
|
|
func (b *Bittrex) WsAuth(ctx context.Context) error {
|
|
creds, err := b.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// [apiKey, timestamp in ms, random uuid, signed payload]
|
|
apiKey := creds.Key
|
|
randomContent, err := uuid.NewV4()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
hmac, err := crypto.GetHMAC(
|
|
crypto.HashSHA512,
|
|
[]byte(timestamp+randomContent.String()),
|
|
[]byte(creds.Secret),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
signature := crypto.HexEncodeToString(hmac)
|
|
|
|
req := WsEventRequest{
|
|
Hub: "c3",
|
|
Method: authenticate,
|
|
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
|
|
}
|
|
|
|
arguments := make([]string, 0)
|
|
arguments = append(arguments, apiKey, timestamp, randomContent.String(), signature)
|
|
req.Arguments = arguments
|
|
|
|
requestString, err := json.Marshal(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if b.Verbose {
|
|
log.Debugf(log.WebsocketMgr, "%s Sending JSON message - %s\n", b.Name, requestString)
|
|
}
|
|
|
|
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var response WsAuthResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
log.Warnf(log.WebsocketMgr, "%s - Cannot unmarshal into WsAuthResponse (%s)\n", b.Name, string(respRaw))
|
|
return err
|
|
}
|
|
if !response.Response.Success {
|
|
log.Warnf(log.WebsocketMgr, "%s - Unable to authenticate (%s)", b.Name, response.Response.ErrorCode)
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
|
|
// handled by ManageSubscriptions()
|
|
func (b *Bittrex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
|
|
var subscriptions []stream.ChannelSubscription
|
|
pairs, err := b.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channels := defaultSpotSubscribedChannels
|
|
if b.IsWebsocketAuthenticationSupported() {
|
|
channels = append(channels, defaultSpotSubscribedChannelsAuth...)
|
|
}
|
|
|
|
for i := range pairs {
|
|
pair, err := b.FormatExchangeCurrency(pairs[i], asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for y := range channels {
|
|
var channel string
|
|
switch channels[y] {
|
|
case wsOrderbook:
|
|
channel = channels[y] + "_" + pair.String() + "_" + strconv.FormatInt(orderbookDepth, 10)
|
|
case wsTicker:
|
|
channel = channels[y] + "_" + pair.String()
|
|
case wsMarketSummary:
|
|
channel = channels[y] + "_" + pair.String()
|
|
default:
|
|
channel = channels[y]
|
|
}
|
|
subscriptions = append(subscriptions,
|
|
stream.ChannelSubscription{
|
|
Channel: channel,
|
|
Currency: pair,
|
|
Asset: asset.Spot,
|
|
})
|
|
}
|
|
}
|
|
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (b *Bittrex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
|
var x int
|
|
var errs error
|
|
for x = 0; x+wsMessageRateLimit < len(channelsToSubscribe); x += wsMessageRateLimit {
|
|
err := b.subscribeSlice(channelsToSubscribe[x : x+wsMessageRateLimit])
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
err := b.subscribeSlice(channelsToSubscribe[x:])
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func (b *Bittrex) subscribeSlice(channelsToSubscribe []stream.ChannelSubscription) error {
|
|
req := WsEventRequest{
|
|
Hub: "c3",
|
|
Method: subscribe,
|
|
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
|
|
}
|
|
|
|
channels := make([]string, len(channelsToSubscribe))
|
|
for i := range channelsToSubscribe {
|
|
channels[i] = channelsToSubscribe[i].Channel
|
|
}
|
|
arguments := make([][]string, 0)
|
|
arguments = append(arguments, channels)
|
|
req.Arguments = arguments
|
|
|
|
requestString, err := json.Marshal(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if b.Verbose {
|
|
log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
|
|
}
|
|
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var response WsSubscriptionResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var errs error
|
|
for i := range response.Response {
|
|
if !response.Response[i].Success {
|
|
errs = common.AppendError(errs, errors.New("unable to subscribe to "+channels[i]+" - error code "+response.Response[i].ErrorCode))
|
|
continue
|
|
}
|
|
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to receive data from the channel
|
|
func (b *Bittrex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
|
var x int
|
|
var errs error
|
|
for x = 0; x+wsMessageRateLimit < len(channelsToUnsubscribe); x += wsMessageRateLimit {
|
|
err := b.unsubscribeSlice(channelsToUnsubscribe[x : x+wsMessageRateLimit])
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
err := b.unsubscribeSlice(channelsToUnsubscribe[x:])
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func (b *Bittrex) unsubscribeSlice(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
|
req := WsEventRequest{
|
|
Hub: "c3",
|
|
Method: unsubscribe,
|
|
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
|
|
}
|
|
|
|
channels := make([]string, len(channelsToUnsubscribe))
|
|
for i := range channelsToUnsubscribe {
|
|
channels[i] = channelsToUnsubscribe[i].Channel
|
|
}
|
|
arguments := make([][]string, 0)
|
|
arguments = append(arguments, channels)
|
|
req.Arguments = arguments
|
|
|
|
requestString, err := json.Marshal(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if b.Verbose {
|
|
log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
|
|
}
|
|
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var response WsSubscriptionResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var errs error
|
|
for i := range response.Response {
|
|
if !response.Response[i].Success {
|
|
errs = common.AppendError(errs, errors.New("unable to unsubscribe from "+channels[i]+" - error code "+response.Response[i].ErrorCode))
|
|
continue
|
|
}
|
|
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// wsReadData gets and passes on websocket messages for processing
|
|
func (b *Bittrex) wsReadData() {
|
|
defer b.Websocket.Wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-b.Websocket.ShutdownC:
|
|
return
|
|
default:
|
|
resp := b.Websocket.Conn.ReadMessage()
|
|
if resp.Raw == nil {
|
|
log.Warnf(log.WebsocketMgr, "%s Received empty message\n", b.Name)
|
|
return
|
|
}
|
|
|
|
err := b.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Bittrex) wsDecodeMessage(encodedMessage string, v interface{}) error {
|
|
raw, err := crypto.Base64Decode(encodedMessage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader := flate.NewReader(bytes.NewBuffer(raw))
|
|
message, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = reader.Close(); err != nil {
|
|
log.Warnf(log.WebsocketMgr, "%s wsDecodeMessage: unable to close reader: %s",
|
|
b.Name,
|
|
err,
|
|
)
|
|
}
|
|
return json.Unmarshal(message, v)
|
|
}
|
|
|
|
func (b *Bittrex) wsHandleData(respRaw []byte) error {
|
|
var response WsEventResponse
|
|
err := json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
log.Warnf(log.WebsocketMgr, "%s Cannot unmarshal into eventResponse (%s)\n", b.Name, string(respRaw))
|
|
return err
|
|
}
|
|
if response.Response != nil && response.InvocationID > 0 {
|
|
if b.Websocket.Match.IncomingWithData(response.InvocationID, respRaw) {
|
|
return nil
|
|
}
|
|
return errors.New("received response to unknown request")
|
|
}
|
|
|
|
if response.Response == nil && len(response.Message) == 0 && response.C == "" {
|
|
if b.Verbose {
|
|
log.Warnf(log.WebsocketMgr, "%s Received keep-alive (%s)\n", b.Name, string(respRaw))
|
|
}
|
|
return nil
|
|
}
|
|
for i := range response.Message {
|
|
switch response.Message[i].Method {
|
|
case "orderBook":
|
|
for j := range response.Message[i].Arguments {
|
|
var orderbookUpdate OrderbookUpdateMessage
|
|
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderbookUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var init bool
|
|
init, err = b.UpdateLocalOBBuffer(&orderbookUpdate)
|
|
if err != nil {
|
|
if init {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%v - UpdateLocalCache error: %s",
|
|
b.Name,
|
|
err)
|
|
}
|
|
}
|
|
case "ticker":
|
|
for j := range response.Message[i].Arguments {
|
|
var tickerUpdate TickerData
|
|
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &tickerUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.WsProcessUpdateTicker(tickerUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "marketSummary":
|
|
for j := range response.Message[i].Arguments {
|
|
var marketSummaryUpdate MarketSummaryData
|
|
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &marketSummaryUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.WsProcessUpdateMarketSummary(&marketSummaryUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "heartbeat":
|
|
if b.Verbose {
|
|
log.Warnf(log.WebsocketMgr, "%s Received heartbeat\n", b.Name)
|
|
}
|
|
case "authenticationExpiring":
|
|
if b.Verbose {
|
|
log.Debugf(log.WebsocketMgr, "%s - Re-authenticating.\n", b.Name)
|
|
}
|
|
err = b.WsAuth(context.TODO())
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- err
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
case "order":
|
|
for j := range response.Message[i].Arguments {
|
|
var orderUpdate OrderUpdateMessage
|
|
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.WsProcessUpdateOrder(&orderUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsProcessUpdateTicker processes an update on the ticker
|
|
func (b *Bittrex) WsProcessUpdateTicker(tickerData TickerData) error {
|
|
pair, err := currency.NewPairFromString(tickerData.Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
|
|
if err != nil {
|
|
b.tickerCache.mu.Lock()
|
|
defer b.tickerCache.mu.Unlock()
|
|
if b.tickerCache.MarketSummaries[tickerData.Symbol] != nil {
|
|
marketSummaryData := b.tickerCache.MarketSummaries[tickerData.Symbol]
|
|
tickerPrice = b.constructTicker(tickerData, marketSummaryData, pair, asset.Spot)
|
|
b.Websocket.DataHandler <- tickerPrice
|
|
return nil
|
|
}
|
|
b.tickerCache.Tickers[tickerData.Symbol] = &tickerData
|
|
return nil
|
|
}
|
|
|
|
tickerPrice.Last = tickerData.LastTradeRate
|
|
tickerPrice.Bid = tickerData.BidRate
|
|
tickerPrice.Ask = tickerData.AskRate
|
|
|
|
b.Websocket.DataHandler <- tickerPrice
|
|
|
|
return nil
|
|
}
|
|
|
|
// WsProcessUpdateMarketSummary processes an update on the ticker
|
|
func (b *Bittrex) WsProcessUpdateMarketSummary(marketSummaryData *MarketSummaryData) error {
|
|
pair, err := currency.NewPairFromString(marketSummaryData.Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
|
|
if err != nil {
|
|
b.tickerCache.mu.Lock()
|
|
defer b.tickerCache.mu.Unlock()
|
|
if b.tickerCache.Tickers[marketSummaryData.Symbol] != nil {
|
|
tickerData := b.tickerCache.Tickers[marketSummaryData.Symbol]
|
|
tickerPrice = b.constructTicker(*tickerData, marketSummaryData, pair, asset.Spot)
|
|
b.Websocket.DataHandler <- tickerPrice
|
|
return nil
|
|
}
|
|
b.tickerCache.MarketSummaries[marketSummaryData.Symbol] = marketSummaryData
|
|
return nil
|
|
}
|
|
|
|
tickerPrice.High = marketSummaryData.High
|
|
tickerPrice.Low = marketSummaryData.Low
|
|
tickerPrice.Volume = marketSummaryData.Volume
|
|
tickerPrice.QuoteVolume = marketSummaryData.QuoteVolume
|
|
tickerPrice.LastUpdated = marketSummaryData.UpdatedAt
|
|
|
|
b.Websocket.DataHandler <- tickerPrice
|
|
|
|
return nil
|
|
}
|
|
|
|
// WsProcessUpdateOrder processes an update on the open orders
|
|
func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error {
|
|
orderType, err := order.StringToOrderType(data.Delta.Type)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: data.Delta.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
orderSide, err := order.StringToOrderSide(data.Delta.Direction)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: data.Delta.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
orderStatus, err := order.StringToOrderStatus(data.Delta.Status)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: data.Delta.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
pair, err := currency.NewPairFromString(data.Delta.MarketSymbol)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: data.Delta.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
b.Websocket.DataHandler <- &order.Detail{
|
|
ImmediateOrCancel: data.Delta.TimeInForce == string(ImmediateOrCancel),
|
|
FillOrKill: data.Delta.TimeInForce == string(GoodTilCancelled),
|
|
PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled),
|
|
Price: data.Delta.Limit,
|
|
Amount: data.Delta.Quantity,
|
|
RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity,
|
|
ExecutedAmount: data.Delta.FillQuantity,
|
|
Exchange: b.Name,
|
|
OrderID: data.Delta.ID,
|
|
Type: orderType,
|
|
Side: orderSide,
|
|
Status: orderStatus,
|
|
AssetType: asset.Spot,
|
|
Date: data.Delta.CreatedAt,
|
|
Pair: pair,
|
|
}
|
|
return nil
|
|
}
|