mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-06 23:16:53 +00:00
* gateio: Add multi asset websocket support WIP. * meow * Add tests and shenanigans * integrate flushing and for enabling/disabling pairs from rpc shenanigans * some changes * linter: fixes strikes again. * Change name ConnectionAssociation -> ConnectionCandidate for better clarity on purpose. Change connections map to point to candidate to track subscriptions for future dynamic connections holder and drop struct ConnectionDetails. * Add subscription tests (state functional) * glorious:nits + proxy handling * Spelling * linter: fixerino * instead of nil, dont do nil. * clean up nils * cya nils * don't need to set URL or check if its running * stop ping handler routine leak * * Fix bug where reader routine on error that is not a disconnection error but websocket frame error or anything really makes the reader routine return and then connection never cycles and the buffer gets filled. * Handle reconnection via an errors.Is check which is simpler and in that scope allow for quick disconnect reconnect without waiting for connection cycle. * Dial now uses code from DialContext but just calls context.Background() * Don't allow reader to return on parse binary response error. Just output error and return a non nil response * Allow rollback on connect on any error across all connections * fix shadow jutsu * glorious/gk: nitters - adds in ws mock server * linter: fix * fix deadlock on connection as the previous channel had no reader and would hang connection reader for eternity. * gk: nits * Leak issue and edge case * gk: nits * gk: drain brain * glorious: nits * Update exchanges/stream/websocket.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * glorious: nits * add tests * linter: fix * After merge * Add error connection info * Fix edge case where it does not reconnect made by an already closed connection * stream coverage * glorious: nits * glorious: nits removed asset error handling in stream package * linter: fix * rm block * Add basic readme * fix asset enabled flush cycle for multi connection * spella: fix * linter: fix * Add glorious suggestions, fix some race thing * reinstate name before any routine gets spawned * stop on error in mock tests * glorious: nits * glorious: nits found in CI build * Add test for drain, bumped wait times as there seems to be something happening on macos CI builds, used context.WithTimeout because its instant. * mutex across shutdown and connect for protection * lint: fix * test time withoffset, reinstate stop * fix whoops * const trafficCheckInterval; rm testmain * y * fix lint * bump time check window * stream: fix intermittant test failures while testing routines and remove code that is not needed. * spells * cant do what I did * protect race due to routine. * update testURL * use mock websocket connection instead of test URL's * linter: fix * remove url because its throwing errors on CI builds * connections drop all the time, don't need to worry about not being able to echo back ws data as it can be easily reviewed _test file side. * remove another superfluous url thats not really set up for this * spawn overwatch routine when there is no errors, inline checker instead of waiting for a time period, add sleep inline with echo handler as this is really quick and wanted to ensure that latency is handing correctly * linter: fixerino uperino * glorious: panix * linter: things * whoops * defer lock and use functions that don't require locking in SetProxyAddress * lint: fix * thrasher: nits --------- Co-authored-by: shazbert <ryan.oharareid@thrasher.io> Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
1150 lines
28 KiB
Go
1150 lines
28 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/request"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
|
"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 stream.ErrWebsocketNotEnabled
|
|
}
|
|
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)
|
|
}
|
|
|
|
if len(data) < 3 {
|
|
return fmt.Errorf("%w for pair %v", errNotEnoughData, pair)
|
|
}
|
|
|
|
ts, ok := data[2].(string)
|
|
if !ok {
|
|
return common.GetTypeAssertError("string", data[2], "timestamp string")
|
|
}
|
|
|
|
tsMilli, err := strconv.ParseInt(ts, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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.Tranches, 0, len(askData))
|
|
for price, volume := range askData {
|
|
var p float64
|
|
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)
|
|
}
|
|
var a float64
|
|
a, err = strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
book.Asks = append(book.Asks, orderbook.Tranche{Price: p, Amount: a})
|
|
}
|
|
|
|
book.Bids = make(orderbook.Tranches, 0, len(bidData))
|
|
for price, volume := range bidData {
|
|
var p float64
|
|
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)
|
|
}
|
|
var a float64
|
|
a, err = strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
book.Bids = append(book.Bids, orderbook.Tranche{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
|
|
book.LastUpdated = time.UnixMilli(tsMilli)
|
|
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) < 5 {
|
|
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)
|
|
}
|
|
|
|
ts, ok := data[4].(string)
|
|
if !ok {
|
|
return common.GetTypeAssertError("string", data[2], "timestamp string")
|
|
}
|
|
|
|
tsMilli, err := strconv.ParseInt(ts, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
update := &orderbook.Update{
|
|
Pair: pair,
|
|
Asset: asset.Spot,
|
|
UpdateID: int64(sequenceNumber),
|
|
UpdateTime: time.UnixMilli(tsMilli),
|
|
}
|
|
if bs == 1 {
|
|
update.Bids = []orderbook.Tranche{{Price: price, Amount: volume}}
|
|
} else {
|
|
update.Asks = []orderbook.Tranche{{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() (subscription.List, error) {
|
|
enabledPairs, err := p.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
subscriptions := make(subscription.List, 0, len(enabledPairs))
|
|
subscriptions = append(subscriptions, &subscription.Subscription{
|
|
Channel: strconv.FormatInt(wsTickerDataID, 10),
|
|
})
|
|
|
|
if p.IsWebsocketAuthenticationSupported() {
|
|
subscriptions = append(subscriptions, &subscription.Subscription{
|
|
Channel: strconv.FormatInt(wsAccountNotificationID, 10),
|
|
})
|
|
}
|
|
|
|
for j := range enabledPairs {
|
|
enabledPairs[j].Delimiter = currency.UnderscoreDelimiter
|
|
subscriptions = append(subscriptions, &subscription.Subscription{
|
|
Channel: "orderbook",
|
|
Pairs: currency.Pairs{enabledPairs[j]},
|
|
Asset: asset.Spot,
|
|
})
|
|
}
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (p *Poloniex) Subscribe(subs subscription.List) error {
|
|
return p.manageSubs(subs, wsSubscribeOp)
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (p *Poloniex) Unsubscribe(subs subscription.List) error {
|
|
return p.manageSubs(subs, wsUnsubscribeOp)
|
|
}
|
|
|
|
func (p *Poloniex) manageSubs(subs subscription.List, op wsOp) 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
|
|
for _, s := range subs {
|
|
var err error
|
|
if creds != nil && strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10), s.Channel) {
|
|
err = p.wsSendAuthorisedCommand(creds.Secret, creds.Key, op)
|
|
} else {
|
|
req := wsCommand{Command: op}
|
|
if strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), s.Channel) {
|
|
req.Channel = wsTickerDataID
|
|
} else {
|
|
if len(s.Pairs) != 1 {
|
|
return subscription.ErrNotSinglePair
|
|
}
|
|
req.Channel = s.Pairs[0].String()
|
|
}
|
|
err = p.Websocket.Conn.SendJSONMessage(context.TODO(), request.Unset, req)
|
|
}
|
|
if err == nil {
|
|
if op == wsSubscribeOp {
|
|
err = p.Websocket.AddSuccessfulSubscriptions(p.Websocket.Conn, s)
|
|
} else {
|
|
err = p.Websocket.RemoveSubscriptions(p.Websocket.Conn, s)
|
|
}
|
|
}
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func (p *Poloniex) wsSendAuthorisedCommand(secret, key string, op wsOp) error {
|
|
nonce := fmt.Sprintf("nonce=%v", time.Now().UnixNano())
|
|
hmac, err := crypto.GetHMAC(crypto.HashSHA512,
|
|
[]byte(nonce),
|
|
[]byte(secret))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req := wsAuthorisationRequest{
|
|
Command: op,
|
|
Channel: 1000,
|
|
Sign: crypto.HexEncodeToString(hmac),
|
|
Key: key,
|
|
Payload: nonce,
|
|
}
|
|
return p.Websocket.Conn.SendJSONMessage(context.TODO(), request.Unset, req)
|
|
}
|
|
|
|
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(time.DateTime, 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(time.DateTime, 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),
|
|
})
|
|
}
|