Files
gocryptotrader/exchanges/poloniex/poloniex_websocket.go
Ryan O'Hara-Reid d2561402c4 common: update Errors type (#1129)
* 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>
2023-02-20 10:48:24 +11:00

1160 lines
29 KiB
Go

package poloniex
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"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/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
poloniexWebsocketAddress = "wss://api2.poloniex.com"
wsAccountNotificationID = 1000
wsTickerDataID = 1002
ws24HourExchangeVolumeID = 1003
wsHeartbeat = 1010
accountNotificationBalanceUpdate = "b"
accountNotificationOrderUpdate = "o"
accountNotificationPendingOrder = "p"
accountNotificationOrderLimitCreated = "n"
accountNotificationTrades = "t"
accountNotificationKilledOrder = "k"
accountNotificationMarginPosition = "m"
orderbookInitial = "i"
orderbookUpdate = "o"
tradeUpdate = "t"
)
var (
errNotEnoughData = errors.New("element length not adequate to process")
errTypeAssertionFailure = errors.New("type assertion failure")
errIDNotFoundInPairMap = errors.New("id not associated with currency pair map")
errIDNotFoundInCodeMap = errors.New("id not associated with currency code map")
)
// WsConnect initiates a websocket connection
func (p *Poloniex) WsConnect() error {
if !p.Websocket.IsEnabled() || !p.IsEnabled() {
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := p.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
err = p.loadCurrencyDetails(context.TODO())
if err != nil {
return err
}
p.Websocket.Wg.Add(1)
go p.wsReadData()
return nil
}
// TODO: Create routine to refresh list every day/week(?) for production
func (p *Poloniex) loadCurrencyDetails(ctx context.Context) error {
if p.details.isInitial() {
ticks, err := p.GetTicker(ctx)
if err != nil {
return err
}
err = p.details.loadPairs(ticks)
if err != nil {
return err
}
currs, err := p.GetCurrencies(ctx)
if err != nil {
return err
}
err = p.details.loadCodes(currs)
if err != nil {
return err
}
}
return nil
}
// wsReadData handles data from the websocket connection
func (p *Poloniex) wsReadData() {
defer p.Websocket.Wg.Done()
for {
resp := p.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := p.wsHandleData(resp.Raw)
if err != nil {
p.Websocket.DataHandler <- fmt.Errorf("%s: %w", p.Name, err)
}
}
}
func (p *Poloniex) wsHandleData(respRaw []byte) error {
var result interface{}
err := json.Unmarshal(respRaw, &result)
if err != nil {
return err
}
data, ok := result.([]interface{})
if !ok {
return fmt.Errorf("%w data is not []interface{}",
errTypeAssertionFailure)
}
if len(data) == 0 {
return nil
}
if len(data) == 2 {
// subscription acknowledgement
// TODO: Add in subscriber ack
return nil
}
channelID, ok := data[0].(float64)
if !ok {
return fmt.Errorf("%w channel id is not of type float64",
errTypeAssertionFailure)
}
switch channelID {
case ws24HourExchangeVolumeID, wsHeartbeat:
return nil
case wsAccountNotificationID:
var notificationsArray []interface{}
notificationsArray, ok = data[2].([]interface{})
if !ok {
return fmt.Errorf("%w account notification is not a []interface{}",
errTypeAssertionFailure)
}
for i := range notificationsArray {
var notification []interface{}
notification, ok = (notificationsArray[i]).([]interface{})
if !ok {
return fmt.Errorf("%w notification array element is not a []interface{}",
errTypeAssertionFailure)
}
var updateType string
updateType, ok = notification[0].(string)
if !ok {
return fmt.Errorf("%w update type is not a string",
errTypeAssertionFailure)
}
switch updateType {
case accountNotificationPendingOrder:
err = p.processAccountPendingOrder(notification)
if err != nil {
return fmt.Errorf("account notification pending order: %w", err)
}
case accountNotificationOrderUpdate:
err = p.processAccountOrderUpdate(notification)
if err != nil {
return fmt.Errorf("account notification order update: %w", err)
}
case accountNotificationOrderLimitCreated:
err = p.processAccountOrderLimit(notification)
if err != nil {
return fmt.Errorf("account notification limit order creation: %w", err)
}
case accountNotificationBalanceUpdate:
err = p.processAccountBalanceUpdate(notification)
if err != nil {
return fmt.Errorf("account notification balance update: %w", err)
}
case accountNotificationTrades:
err = p.processAccountTrades(notification)
if err != nil {
return fmt.Errorf("account notification trades: %w", err)
}
case accountNotificationKilledOrder:
err = p.processAccountKilledOrder(notification)
if err != nil {
return fmt.Errorf("account notification killed order: %w", err)
}
case accountNotificationMarginPosition:
err = p.processAccountMarginPosition(notification)
if err != nil {
return fmt.Errorf("account notification margin position: %w", err)
}
default:
return fmt.Errorf("unhandled account update: %s", string(respRaw))
}
}
return nil
case wsTickerDataID:
err = p.wsHandleTickerData(data)
if err != nil {
return fmt.Errorf("websocket ticker process: %w", err)
}
return nil
}
priceAggBook, ok := data[2].([]interface{})
if !ok {
return fmt.Errorf("%w price aggregated book not []interface{}",
errTypeAssertionFailure)
}
for x := range priceAggBook {
subData, ok := priceAggBook[x].([]interface{})
if !ok {
return fmt.Errorf("%w price aggregated book element not []interface{}",
errTypeAssertionFailure)
}
updateIdent, ok := subData[0].(string)
if !ok {
return fmt.Errorf("%w update identifier not a string",
errTypeAssertionFailure)
}
switch updateIdent {
case orderbookInitial:
err = p.WsProcessOrderbookSnapshot(subData)
if err != nil {
return fmt.Errorf("websocket process orderbook snapshot: %w", err)
}
case orderbookUpdate:
var pair currency.Pair
pair, err = p.details.GetPair(channelID)
if err != nil {
return err
}
var seqNo float64
seqNo, ok = data[1].(float64)
if !ok {
return fmt.Errorf("%w sequence number is not a float64",
errTypeAssertionFailure)
}
err = p.WsProcessOrderbookUpdate(seqNo, subData, pair)
if err != nil {
return fmt.Errorf("websocket process orderbook update: %w", err)
}
case tradeUpdate:
err = p.processTrades(channelID, subData)
if err != nil {
return fmt.Errorf("websocket process trades update: %w", err)
}
default:
p.Websocket.DataHandler <- stream.UnhandledMessageWarning{
Message: p.Name + stream.UnhandledMessage + string(respRaw),
}
}
}
return nil
}
func (p *Poloniex) wsHandleTickerData(data []interface{}) error {
tickerData, ok := data[2].([]interface{})
if !ok {
return fmt.Errorf("%w ticker data is not []interface{}",
errTypeAssertionFailure)
}
currencyID, ok := tickerData[0].(float64)
if !ok {
return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure)
}
pair, err := p.details.GetPair(currencyID)
if err != nil {
return err
}
enabled, err := p.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
if !enabled.Contains(pair, true) {
return nil
}
tlp, ok := tickerData[1].(string)
if !ok {
return fmt.Errorf("%w last price not string", errTypeAssertionFailure)
}
lastPrice, err := strconv.ParseFloat(tlp, 64)
if err != nil {
return err
}
la, ok := tickerData[2].(string)
if !ok {
return fmt.Errorf("%w lowest ask price not string",
errTypeAssertionFailure)
}
lowestAsk, err := strconv.ParseFloat(la, 64)
if err != nil {
return err
}
hb, ok := tickerData[3].(string)
if !ok {
return fmt.Errorf("%w highest bid price not string",
errTypeAssertionFailure)
}
highestBid, err := strconv.ParseFloat(hb, 64)
if err != nil {
return err
}
bcv, ok := tickerData[5].(string)
if !ok {
return fmt.Errorf("%w base currency volume not string",
errTypeAssertionFailure)
}
baseCurrencyVolume24H, err := strconv.ParseFloat(bcv, 64)
if err != nil {
return err
}
qcv, ok := tickerData[6].(string)
if !ok {
return fmt.Errorf("%w quote currency volume not string",
errTypeAssertionFailure)
}
quoteCurrencyVolume24H, err := strconv.ParseFloat(qcv, 64)
if err != nil {
return err
}
// Unused variables below, can add later if needed:
// percentageChange, ok := tickerData[4].(string)
// Not integrating isFrozen with currency details as this will slow down
// the sync RW mutex (can use REST calls for now).
// isFrozen, ok := tickerData[7].(float64) // == 1 means it is frozen
// highestTradeIn24Hm, ok := tickerData[8].(string)
// lowestTradePrice24H, ok := tickerData[9].(string)
p.Websocket.DataHandler <- &ticker.Price{
ExchangeName: p.Name,
Volume: baseCurrencyVolume24H,
QuoteVolume: quoteCurrencyVolume24H,
High: highestBid,
Low: lowestAsk,
Bid: highestBid,
Ask: lowestAsk,
Last: lastPrice,
AssetType: asset.Spot,
Pair: pair,
}
return nil
}
// WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local
// of orderbooks
func (p *Poloniex) WsProcessOrderbookSnapshot(data []interface{}) error {
subDataMap, ok := data[1].(map[string]interface{})
if !ok {
return fmt.Errorf("%w subData element is not map[string]interface{}",
errTypeAssertionFailure)
}
pMap, ok := subDataMap["currencyPair"]
if !ok {
return errors.New("could not find currency pair in map")
}
pair, ok := pMap.(string)
if !ok {
return fmt.Errorf("%w subData element is not map[string]interface{}",
errTypeAssertionFailure)
}
oMap, ok := subDataMap["orderBook"]
if !ok {
return errors.New("could not find orderbook data in map")
}
ob, ok := oMap.([]interface{})
if !ok {
return fmt.Errorf("%w orderbook data is not []interface{}",
errTypeAssertionFailure)
}
if len(ob) != 2 {
return errNotEnoughData
}
askData, ok := ob[0].(map[string]interface{})
if !ok {
return fmt.Errorf("%w ask data is not map[string]interface{}",
errTypeAssertionFailure)
}
bidData, ok := ob[1].(map[string]interface{})
if !ok {
return fmt.Errorf("%w bid data is not map[string]interface{}",
errTypeAssertionFailure)
}
var book orderbook.Base
book.Asks = make(orderbook.Items, 0, len(askData))
for price, volume := range askData {
p, err := strconv.ParseFloat(price, 64)
if err != nil {
return err
}
v, ok := volume.(string)
if !ok {
return fmt.Errorf("%w ask volume data not string",
errTypeAssertionFailure)
}
a, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
book.Asks = append(book.Asks, orderbook.Item{Price: p, Amount: a})
}
book.Bids = make(orderbook.Items, 0, len(bidData))
for price, volume := range bidData {
p, err := strconv.ParseFloat(price, 64)
if err != nil {
return err
}
v, ok := volume.(string)
if !ok {
return fmt.Errorf("%w bid volume data not string",
errTypeAssertionFailure)
}
a, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
book.Bids = append(book.Bids, orderbook.Item{Price: p, Amount: a})
}
// Both sides are completely out of order - sort needs to be used
book.Asks.SortAsks()
book.Bids.SortBids()
book.Asset = asset.Spot
book.VerifyOrderbook = p.CanVerifyOrderbook
var err error
book.Pair, err = currency.NewPairFromString(pair)
if err != nil {
return err
}
book.Exchange = p.Name
return p.Websocket.Orderbook.LoadSnapshot(&book)
}
// WsProcessOrderbookUpdate processes new orderbook updates
func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber float64, data []interface{}, pair currency.Pair) error {
if len(data) < 4 {
return errNotEnoughData
}
ps, ok := data[2].(string)
if !ok {
return fmt.Errorf("%w price not string", errTypeAssertionFailure)
}
price, err := strconv.ParseFloat(ps, 64)
if err != nil {
return err
}
vs, ok := data[3].(string)
if !ok {
return fmt.Errorf("%w volume not string", errTypeAssertionFailure)
}
volume, err := strconv.ParseFloat(vs, 64)
if err != nil {
return err
}
bs, ok := data[1].(float64)
if !ok {
return fmt.Errorf("%w buysell not float64", errTypeAssertionFailure)
}
update := &orderbook.Update{
Pair: pair,
Asset: asset.Spot,
UpdateID: int64(sequenceNumber),
}
if bs == 1 {
update.Bids = []orderbook.Item{{Price: price, Amount: volume}}
} else {
update.Asks = []orderbook.Item{{Price: price, Amount: volume}}
}
return p.Websocket.Orderbook.Update(update)
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (p *Poloniex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
enabledCurrencies, err := p.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
subscriptions := make([]stream.ChannelSubscription, 0, len(enabledCurrencies))
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: strconv.FormatInt(wsTickerDataID, 10),
})
if p.IsWebsocketAuthenticationSupported() {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: strconv.FormatInt(wsAccountNotificationID, 10),
})
}
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = currency.UnderscoreDelimiter
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: "orderbook",
Currency: enabledCurrencies[j],
Asset: asset.Spot,
})
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (p *Poloniex) Subscribe(sub []stream.ChannelSubscription) error {
var creds *account.Credentials
if p.IsWebsocketAuthenticationSupported() {
var err error
creds, err = p.GetCredentials(context.TODO())
if err != nil {
return err
}
}
var errs error
channels:
for i := range sub {
subscriptionRequest := WsCommand{
Command: "subscribe",
}
switch {
case strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10),
sub[i].Channel) && creds != nil:
err := p.wsSendAuthorisedCommand(creds.Secret, creds.Key, "subscribe")
if err != nil {
errs = common.AppendError(errs, err)
continue channels
}
p.Websocket.AddSuccessfulSubscriptions(sub[i])
continue channels
case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10),
sub[i].Channel):
subscriptionRequest.Channel = wsTickerDataID
default:
subscriptionRequest.Channel = sub[i].Currency.String()
}
err := p.Websocket.Conn.SendJSONMessage(subscriptionRequest)
if err != nil {
errs = common.AppendError(errs, err)
continue
}
p.Websocket.AddSuccessfulSubscriptions(sub[i])
}
if errs != nil {
return errs
}
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (p *Poloniex) Unsubscribe(unsub []stream.ChannelSubscription) error {
var creds *account.Credentials
if p.IsWebsocketAuthenticationSupported() {
var err error
creds, err = p.GetCredentials(context.TODO())
if err != nil {
return err
}
}
var errs error
channels:
for i := range unsub {
unsubscriptionRequest := WsCommand{
Command: "unsubscribe",
}
switch {
case strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10),
unsub[i].Channel) && creds != nil:
err := p.wsSendAuthorisedCommand(creds.Secret, creds.Key, "unsubscribe")
if err != nil {
errs = common.AppendError(errs, err)
continue channels
}
p.Websocket.RemoveSuccessfulUnsubscriptions(unsub[i])
continue channels
case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10),
unsub[i].Channel):
unsubscriptionRequest.Channel = wsTickerDataID
default:
unsubscriptionRequest.Channel = unsub[i].Currency.String()
}
err := p.Websocket.Conn.SendJSONMessage(unsubscriptionRequest)
if err != nil {
errs = common.AppendError(errs, err)
continue
}
p.Websocket.RemoveSuccessfulUnsubscriptions(unsub[i])
}
if errs != nil {
return errs
}
return nil
}
func (p *Poloniex) wsSendAuthorisedCommand(secret, key, command string) error {
nonce := fmt.Sprintf("nonce=%v", time.Now().UnixNano())
hmac, err := crypto.GetHMAC(crypto.HashSHA512,
[]byte(nonce),
[]byte(secret))
if err != nil {
return err
}
request := WsAuthorisationRequest{
Command: command,
Channel: 1000,
Sign: crypto.HexEncodeToString(hmac),
Key: key,
Payload: nonce,
}
return p.Websocket.Conn.SendJSONMessage(request)
}
func (p *Poloniex) processAccountMarginPosition(notification []interface{}) error {
if len(notification) < 5 {
return errNotEnoughData
}
orderID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w order id not float64", errTypeAssertionFailure)
}
currencyID, ok := notification[2].(float64)
if !ok {
return fmt.Errorf("%w currency id not float64", errTypeAssertionFailure)
}
code, err := p.details.GetCode(currencyID)
if err != nil {
return err
}
a, ok := notification[3].(string)
if !ok {
return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
}
amount, err := strconv.ParseFloat(a, 64)
if err != nil {
return err
}
// null returned so ok check is not needed
clientOrderID, _ := notification[4].(string)
// Temp struct for margin position changes
p.Websocket.DataHandler <- struct {
OrderID string
Code currency.Code
Amount float64
ClientOrderID string
}{
OrderID: strconv.FormatFloat(orderID, 'f', -1, 64),
Code: code,
Amount: amount,
ClientOrderID: clientOrderID,
}
return nil
}
func (p *Poloniex) processAccountPendingOrder(notification []interface{}) error {
if len(notification) < 7 {
return errNotEnoughData
}
orderID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w order id not float64", errTypeAssertionFailure)
}
currencyID, ok := notification[2].(float64)
if !ok {
return fmt.Errorf("%w currency id not float64", errTypeAssertionFailure)
}
pair, err := p.details.GetPair(currencyID)
if err != nil {
if !errors.Is(err, errIDNotFoundInPairMap) {
return err
}
log.Errorf(log.WebsocketMgr,
"%s - Unknown currency pair ID. Currency will appear as the pair ID: '%v'",
p.Name,
currencyID)
}
price, ok := notification[3].(string)
if !ok {
return fmt.Errorf("%w price not string", errTypeAssertionFailure)
}
orderPrice, err := strconv.ParseFloat(price, 64)
if err != nil {
return err
}
amount, ok := notification[4].(string)
if !ok {
return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
}
orderAmount, err := strconv.ParseFloat(amount, 64)
if err != nil {
return err
}
side, ok := notification[5].(string)
if !ok {
return fmt.Errorf("%w order type not string", errTypeAssertionFailure)
}
orderSide := order.Buy
if side == "0" {
orderSide = order.Sell
}
// null returned so ok check is not needed
clientOrderID, _ := notification[6].(string)
p.Websocket.DataHandler <- &order.Detail{
Exchange: p.Name,
OrderID: strconv.FormatFloat(orderID, 'f', -1, 64),
Pair: pair,
AssetType: asset.Spot,
Side: orderSide,
Price: orderPrice,
Amount: orderAmount,
RemainingAmount: orderAmount,
ClientOrderID: clientOrderID,
Status: order.Pending,
}
return nil
}
func (p *Poloniex) processAccountOrderUpdate(notification []interface{}) error {
if len(notification) < 5 {
return errNotEnoughData
}
orderID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w order id not float64", errTypeAssertionFailure)
}
a, ok := notification[2].(string)
if !ok {
return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
}
amount, err := strconv.ParseFloat(a, 64)
if err != nil {
return err
}
oType, ok := notification[3].(string)
if !ok {
return fmt.Errorf("%w order type not string", errTypeAssertionFailure)
}
var oStatus order.Status
var cancelledAmount float64
if oType == "c" {
if len(notification) < 6 {
return errNotEnoughData
}
cancel, ok := notification[5].(string)
if !ok {
return fmt.Errorf("%w cancel amount not string", errTypeAssertionFailure)
}
cancelledAmount, err = strconv.ParseFloat(cancel, 64)
if err != nil {
return err
}
if amount > 0 {
oStatus = order.PartiallyCancelled
} else {
oStatus = order.Cancelled
}
} else {
if amount > 0 {
oStatus = order.PartiallyFilled
} else {
oStatus = order.Filled
}
}
// null returned so ok check is not needed
clientOrderID, _ := notification[4].(string)
p.Websocket.DataHandler <- &order.Detail{
Exchange: p.Name,
RemainingAmount: cancelledAmount,
Amount: amount + cancelledAmount,
ExecutedAmount: amount,
OrderID: strconv.FormatFloat(orderID, 'f', -1, 64),
Type: order.Limit,
Status: oStatus,
AssetType: asset.Spot,
ClientOrderID: clientOrderID,
}
return nil
}
func (p *Poloniex) processAccountOrderLimit(notification []interface{}) error {
if len(notification) != 9 {
return errNotEnoughData
}
currencyID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w currency ID not string", errTypeAssertionFailure)
}
pair, err := p.details.GetPair(currencyID)
if err != nil {
if !errors.Is(err, errIDNotFoundInPairMap) {
return err
}
log.Errorf(log.WebsocketMgr,
"%s - Unknown currency pair ID. Currency will appear as the pair ID: '%v'",
p.Name,
currencyID)
}
orderID, ok := notification[2].(float64)
if !ok {
return fmt.Errorf("%w order ID not float64", errTypeAssertionFailure)
}
side, ok := notification[3].(string)
if !ok {
return fmt.Errorf("%w order type not string", errTypeAssertionFailure)
}
orderSide := order.Buy
if side == "0" {
orderSide = order.Sell
}
rate, ok := notification[4].(string)
if !ok {
return fmt.Errorf("%w rate not string", errTypeAssertionFailure)
}
orderPrice, err := strconv.ParseFloat(rate, 64)
if err != nil {
return err
}
amount, ok := notification[5].(string)
if !ok {
return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
}
orderAmount, err := strconv.ParseFloat(amount, 64)
if err != nil {
return err
}
ts, ok := notification[6].(string)
if !ok {
return fmt.Errorf("%w time not string", errTypeAssertionFailure)
}
var timeParse time.Time
timeParse, err = time.Parse(common.SimpleTimeFormat, ts)
if err != nil {
return err
}
origAmount, ok := notification[7].(string)
if !ok {
return fmt.Errorf("%w original amount not string", errTypeAssertionFailure)
}
origOrderAmount, err := strconv.ParseFloat(origAmount, 64)
if err != nil {
return err
}
// null returned so ok check is not needed
clientOrderID, _ := notification[8].(string)
p.Websocket.DataHandler <- &order.Detail{
Exchange: p.Name,
Price: orderPrice,
RemainingAmount: orderAmount,
ExecutedAmount: origOrderAmount - orderAmount,
Amount: origOrderAmount,
OrderID: strconv.FormatFloat(orderID, 'f', -1, 64),
Type: order.Limit,
Side: orderSide,
Status: order.New,
AssetType: asset.Spot,
Date: timeParse,
Pair: pair,
ClientOrderID: clientOrderID,
}
return nil
}
func (p *Poloniex) processAccountBalanceUpdate(notification []interface{}) error {
if len(notification) < 4 {
return errNotEnoughData
}
currencyID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure)
}
code, err := p.details.GetCode(currencyID)
if err != nil {
return err
}
walletType, ok := notification[2].(string)
if !ok {
return fmt.Errorf("%w wallet addr not string", errTypeAssertionFailure)
}
a, ok := notification[3].(string)
if !ok {
return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
}
amount, err := strconv.ParseFloat(a, 64)
if err != nil {
return err
}
// TODO: Integrate with exchange account system
// NOTES: This will affect free amount, a rest call might be needed to get
// locked and total amounts periodically.
p.Websocket.DataHandler <- account.Change{
Exchange: p.Name,
Currency: code,
Asset: asset.Spot,
Account: deriveWalletType(walletType),
Amount: amount,
}
return nil
}
func deriveWalletType(s string) string {
switch s {
case "e":
return "exchange"
case "m":
return "margin"
case "l":
return "lending"
default:
return "unknown"
}
}
func (p *Poloniex) processAccountTrades(notification []interface{}) error {
if len(notification) < 11 {
return errNotEnoughData
}
tradeID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w tradeID not float64", errTypeAssertionFailure)
}
r, ok := notification[2].(string)
if !ok {
return fmt.Errorf("%w rate not string", errTypeAssertionFailure)
}
rate, err := strconv.ParseFloat(r, 64)
if err != nil {
return err
}
a, ok := notification[3].(string)
if !ok {
return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
}
amount, err := strconv.ParseFloat(a, 64)
if err != nil {
return err
}
// notification[4].(string) is the fee multiplier
// notification[5].(string) is the funding type 0 (exchange wallet),
// 1 (borrowed funds), 2 (margin funds), or 3 (lending funds)
orderID, ok := notification[6].(float64)
if !ok {
return fmt.Errorf("%w orderID not float64", errTypeAssertionFailure)
}
fee, ok := notification[7].(string)
if !ok {
return fmt.Errorf("%w fee not string", errTypeAssertionFailure)
}
totalFee, err := strconv.ParseFloat(fee, 64)
if err != nil {
return err
}
t, ok := notification[8].(string)
if !ok {
return fmt.Errorf("%w time not string", errTypeAssertionFailure)
}
timeParse, err := time.Parse(common.SimpleTimeFormat, t)
if err != nil {
return err
}
// null returned so ok check is not needed
clientOrderID, _ := notification[9].(string)
tt, ok := notification[10].(string)
if !ok {
return fmt.Errorf("%w time not string", errTypeAssertionFailure)
}
tradeTotal, err := strconv.ParseFloat(tt, 64)
if err != nil {
return err
}
p.Websocket.DataHandler <- &order.Detail{
Exchange: p.Name,
OrderID: strconv.FormatFloat(orderID, 'f', -1, 64),
Fee: totalFee,
Trades: []order.TradeHistory{{
Price: rate,
Amount: amount,
Fee: totalFee,
Exchange: p.Name,
TID: strconv.FormatFloat(tradeID, 'f', -1, 64),
Timestamp: timeParse,
Total: tradeTotal,
}},
AssetType: asset.Spot,
ClientOrderID: clientOrderID,
}
return nil
}
func (p *Poloniex) processAccountKilledOrder(notification []interface{}) error {
if len(notification) < 3 {
return errNotEnoughData
}
orderID, ok := notification[1].(float64)
if !ok {
return fmt.Errorf("%w order ID not float64", errTypeAssertionFailure)
}
// null returned so ok check is not needed
clientOrderID, _ := notification[2].(string)
p.Websocket.DataHandler <- &order.Detail{
Exchange: p.Name,
OrderID: strconv.FormatFloat(orderID, 'f', -1, 64),
Status: order.Cancelled,
AssetType: asset.Spot,
ClientOrderID: clientOrderID,
}
return nil
}
func (p *Poloniex) processTrades(currencyID float64, subData []interface{}) error {
if !p.IsSaveTradeDataEnabled() {
return nil
}
pair, err := p.details.GetPair(currencyID)
if err != nil {
return err
}
if len(subData) != 6 {
return errNotEnoughData
}
var tradeID string
switch tradeIDData := subData[1].(type) { // tradeID type intermittently changes
case string:
tradeID = tradeIDData
case float64:
tradeID = strconv.FormatFloat(tradeIDData, 'f', -1, 64)
default:
return fmt.Errorf("unhandled type for websocket trade update: %v",
tradeIDData)
}
orderSide, ok := subData[2].(float64)
if !ok {
return fmt.Errorf("%w order side not float64",
errTypeAssertionFailure)
}
side := order.Buy
if orderSide != 1 {
side = order.Sell
}
v, ok := subData[3].(string)
if !ok {
return fmt.Errorf("%w volume not string",
errTypeAssertionFailure)
}
volume, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
rate, ok := subData[4].(string)
if !ok {
return fmt.Errorf("%w rate not string", errTypeAssertionFailure)
}
price, err := strconv.ParseFloat(rate, 64)
if err != nil {
return err
}
timestamp, ok := subData[5].(float64)
if !ok {
return fmt.Errorf("%w time not float64", errTypeAssertionFailure)
}
return p.AddTradesToBuffer(trade.Data{
TID: tradeID,
Exchange: p.Name,
CurrencyPair: pair,
AssetType: asset.Spot,
Side: side,
Price: price,
Amount: volume,
Timestamp: time.Unix(int64(timestamp), 0),
})
}