Files
gocryptotrader/exchanges/poloniex/poloniex_websocket.go
Ryan O'Hara-Reid c6ad429827 orderbook/buffer: data integrity and resubscription pass (#910)
* orderbook/buffer: data integrity and resubscription pass

* btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!!

* buffer: reinstate publish, refaactor, invalidate more and comments

* buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments.

* dispatch: change publish func to variadic id param

* dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests.

* dispatch: don't create pointers for every job container

* rpcserver: fix assertion issues with data publishing change

* linter: fixes

* glorious: nits addr

* depth: change validation handling to incorporate and store err

* linter: fix more issues

* dispatch: fix race

* travis: update before fetching

* depth: wrap and return wrapped error in invalidate call and fix tests

* btcmarkets: fix commenting

* workflow: check

* workflow: check

* orderbook: check error

* buffer/depth: return invalidation error and fix tests

* gctcli: display errors on orderbook streams

* buffer: remove unused types

* orderbook/bitmex: shift function to bitmex

* orderbook: Add specific comments to unexported functions that don't have locking require locking.

* orderbook: restrict published data functionality to orderbook.Outbound interface

* common: add assertion failure helper for error

* dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests

* dispatch: export function

* engine: revert and change sub logger to manager

* engine: remove old test

* dispatch: add common variable ;)

* btcmarket: don't overflow int in tests on 32bit systems

* ci: force 1.17.7 usage for go

* Revert "ci: force 1.17.7 usage for go"

This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91.

* golangci: bump version add and remove linter items

* Revert "golangci: bump version add and remove linter items"

This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b.

* dispatch: remove unsused mutex from mux

* order: slight optimizations

* nits: glorious

* dispatch: fix regression on uuid generation and input inline with master

* linter: fix

* linter: fix

* glorious: nit - rm slice segration

* account: fix test after merge

* coinbasepro: revert change

* account: close channel instead of needing a receiver, push alert in routine to prepare for waiter.

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
2022-05-03 12:37:08 +10:00

1161 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"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"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.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
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 *exchange.Credentials
if p.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
var err error
creds, err = p.GetCredentials(context.TODO())
if err != nil {
return err
}
}
var errs common.Errors
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 = append(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 = append(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 *exchange.Credentials
if p.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
var err error
creds, err = p.GetCredentials(context.TODO())
if err != nil {
return err
}
}
var errs common.Errors
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 = append(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 = append(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,
ID: 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.Modify{
Exchange: p.Name,
RemainingAmount: cancelledAmount,
Amount: amount + cancelledAmount,
ExecutedAmount: amount,
ID: 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,
ID: 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.Modify{
Exchange: p.Name,
ID: 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.Modify{
Exchange: p.Name,
ID: 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),
})
}