mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-30 07:26:46 +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>
1160 lines
29 KiB
Go
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),
|
|
})
|
|
}
|