mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
349 lines
9.9 KiB
Go
349 lines
9.9 KiB
Go
package okcoin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-/gocryptotrader/common"
|
|
"github.com/thrasher-/gocryptotrader/currency/pair"
|
|
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
|
log "github.com/thrasher-/gocryptotrader/logger"
|
|
)
|
|
|
|
const (
|
|
wsSubTicker = "ok_sub_spot_%s_ticker"
|
|
wsSubDepthIncrement = "ok_sub_spot_%s_depth"
|
|
wsSubDepthFull = "ok_sub_spot_%s_depth_%s"
|
|
wsSubTrades = "ok_sub_spot_%s_deals"
|
|
wsSubKline = "ok_sub_spot_%s_kline_%s"
|
|
)
|
|
|
|
// PingHandler handles the keep alive
|
|
func (o *OKCoin) PingHandler(_ string) error {
|
|
return o.WebsocketConn.WriteControl(websocket.PingMessage,
|
|
[]byte("{'event':'ping'}"),
|
|
time.Now().Add(time.Second))
|
|
}
|
|
|
|
// AddChannel adds a new channel on the websocket client
|
|
func (o *OKCoin) AddChannel(channel string) error {
|
|
event := WebsocketEvent{"addChannel", channel}
|
|
data, err := common.JSONEncode(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return o.WebsocketConn.WriteMessage(websocket.TextMessage, data)
|
|
}
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (o *OKCoin) WsConnect() error {
|
|
if !o.Websocket.IsEnabled() || !o.IsEnabled() {
|
|
return errors.New(exchange.WebsocketNotEnabled)
|
|
}
|
|
|
|
klineValues := []string{"1min", "3min", "5min", "15min", "30min", "1hour",
|
|
"2hour", "4hour", "6hour", "12hour", "day", "3day", "week"}
|
|
|
|
var dialer websocket.Dialer
|
|
|
|
if o.Websocket.GetProxyAddress() != "" {
|
|
proxy, err := url.Parse(o.Websocket.GetProxyAddress())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dialer.Proxy = http.ProxyURL(proxy)
|
|
}
|
|
|
|
var err error
|
|
o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(),
|
|
http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.WebsocketConn.SetPingHandler(o.PingHandler)
|
|
|
|
go o.WsHandleData()
|
|
|
|
for _, p := range o.GetEnabledCurrencies() {
|
|
fPair := exchange.FormatExchangeCurrency(o.GetName(), p)
|
|
|
|
o.AddChannel(fmt.Sprintf(wsSubDepthFull, fPair.String(), "20"))
|
|
o.AddChannel(fmt.Sprintf(wsSubKline, fPair.String(), klineValues[0]))
|
|
o.AddChannel(fmt.Sprintf(wsSubTicker, fPair.String()))
|
|
o.AddChannel(fmt.Sprintf(wsSubTrades, fPair.String()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WsReadData reads from the websocket connection
|
|
func (o *OKCoin) WsReadData() (exchange.WebsocketResponse, error) {
|
|
_, resp, err := o.WebsocketConn.ReadMessage()
|
|
if err != nil {
|
|
return exchange.WebsocketResponse{}, err
|
|
}
|
|
|
|
o.Websocket.TrafficAlert <- struct{}{}
|
|
return exchange.WebsocketResponse{Raw: resp}, nil
|
|
}
|
|
|
|
// WsHandleData handles stream data from the websocket connection
|
|
func (o *OKCoin) WsHandleData() {
|
|
o.Websocket.Wg.Add(1)
|
|
|
|
defer func() {
|
|
err := o.WebsocketConn.Close()
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- fmt.Errorf("okcoin_websocket.go - Unable to to close Websocket connection. Error: %s",
|
|
err)
|
|
}
|
|
o.Websocket.Wg.Done()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-o.Websocket.ShutdownC:
|
|
return
|
|
|
|
default:
|
|
resp, err := o.WsReadData()
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
}
|
|
|
|
var init []WsResponse
|
|
err = common.JSONDecode(resp.Raw, &init)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
if init[0].ErrorCode != "" {
|
|
log.Error(o.WebsocketErrors[init[0].ErrorCode])
|
|
}
|
|
|
|
if init[0].Success {
|
|
if init[0].Data == nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if init[0].Channel == "addChannel" {
|
|
continue
|
|
}
|
|
|
|
var currencyPairSlice []string
|
|
splitChar := common.SplitStrings(init[0].Channel, "_")
|
|
currencyPairSlice = append(currencyPairSlice,
|
|
common.StringToUpper(splitChar[3]),
|
|
common.StringToUpper(splitChar[4]))
|
|
currencyPair := common.JoinStrings(currencyPairSlice, "-")
|
|
|
|
assetType := common.StringToUpper(splitChar[2])
|
|
|
|
switch {
|
|
case common.StringContains(init[0].Channel, "ticker") &&
|
|
common.StringContains(init[0].Channel, "spot"):
|
|
var ticker WsTicker
|
|
|
|
err = common.JSONDecode(init[0].Data, &ticker)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
o.Websocket.DataHandler <- exchange.TickerData{
|
|
Timestamp: time.Unix(0, ticker.Timestamp),
|
|
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
|
AssetType: assetType,
|
|
Exchange: o.GetName(),
|
|
ClosePrice: ticker.Close,
|
|
OpenPrice: ticker.Open,
|
|
HighPrice: ticker.Last,
|
|
LowPrice: ticker.Low,
|
|
Quantity: ticker.Volume,
|
|
}
|
|
|
|
case common.StringContains(init[0].Channel, "depth"):
|
|
var orderbook WsOrderbook
|
|
|
|
err = common.JSONDecode(init[0].Data, &orderbook)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
|
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
|
Exchange: o.GetName(),
|
|
Asset: assetType,
|
|
}
|
|
|
|
case common.StringContains(init[0].Channel, "kline"):
|
|
var klineData [][]interface{}
|
|
|
|
err = common.JSONDecode(init[0].Data, &klineData)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
var klines []WsKlines
|
|
for _, data := range klineData {
|
|
var newKline WsKlines
|
|
|
|
newKline.Timestamp, _ = strconv.ParseInt(data[0].(string), 10, 64)
|
|
newKline.Open, _ = strconv.ParseFloat(data[1].(string), 64)
|
|
newKline.High, _ = strconv.ParseFloat(data[1].(string), 64)
|
|
newKline.Low, _ = strconv.ParseFloat(data[1].(string), 64)
|
|
newKline.Close, _ = strconv.ParseFloat(data[1].(string), 64)
|
|
newKline.Volume, _ = strconv.ParseFloat(data[1].(string), 64)
|
|
|
|
klines = append(klines, newKline)
|
|
}
|
|
|
|
for _, data := range klines {
|
|
o.Websocket.DataHandler <- exchange.KlineData{
|
|
Timestamp: time.Unix(0, data.Timestamp),
|
|
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
|
AssetType: assetType,
|
|
Exchange: o.GetName(),
|
|
OpenPrice: data.Open,
|
|
ClosePrice: data.Close,
|
|
HighPrice: data.High,
|
|
LowPrice: data.Low,
|
|
Volume: data.Volume,
|
|
}
|
|
}
|
|
|
|
case common.StringContains(init[0].Channel, "spot") &&
|
|
common.StringContains(init[0].Channel, "deals"):
|
|
var dealsData [][]interface{}
|
|
err = common.JSONDecode(init[0].Data, &dealsData)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
var deals []WsDeals
|
|
for _, data := range dealsData {
|
|
var newDeal WsDeals
|
|
newDeal.TID, _ = strconv.ParseInt(data[0].(string), 10, 64)
|
|
newDeal.Price, _ = strconv.ParseFloat(data[1].(string), 64)
|
|
newDeal.Amount, _ = strconv.ParseFloat(data[2].(string), 64)
|
|
newDeal.Timestamp, _ = data[3].(string)
|
|
newDeal.Type, _ = data[4].(string)
|
|
deals = append(deals, newDeal) // nolint: staticcheck
|
|
// TODO: will need to link this up
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetWebsocketErrorDefaults sets default errors for websocket
|
|
func (o *OKCoin) SetWebsocketErrorDefaults() {
|
|
o.WebsocketErrors = map[string]string{
|
|
"10001": "Illegal parameters",
|
|
"10002": "Authentication failure",
|
|
"10003": "This connection has requested other user data",
|
|
"10004": "This connection did not request this user data",
|
|
"10005": "System error",
|
|
"10009": "Order does not exist",
|
|
"10010": "Insufficient funds",
|
|
"10011": "Order quantity too low",
|
|
"10012": "Only support btc_usd/btc_cny ltc_usd/ltc_cny",
|
|
"10014": "Order price must be between 0 - 1,000,000",
|
|
"10015": "Channel subscription temporally not available",
|
|
"10016": "Insufficient coins",
|
|
"10017": "WebSocket authorization error",
|
|
"10100": "User frozen",
|
|
"10216": "Non-public API",
|
|
"20001": "User does not exist",
|
|
"20002": "User frozen",
|
|
"20003": "Frozen due to force liquidation",
|
|
"20004": "Future account frozen",
|
|
"20005": "User future account does not exist",
|
|
"20006": "Required field can not be null",
|
|
"20007": "Illegal parameter",
|
|
"20008": "Future account fund balance is zero",
|
|
"20009": "Future contract status error",
|
|
"20010": "Risk rate information does not exist",
|
|
"20011": `Risk rate bigger than 90% before opening position`,
|
|
"20012": `Risk rate bigger than 90% after opening position`,
|
|
"20013": "Temporally no counter party price",
|
|
"20014": "System error",
|
|
"20015": "Order does not exist",
|
|
"20016": "Liquidation quantity bigger than holding",
|
|
"20017": "Not authorized/illegal order ID",
|
|
"20018": `Order price higher than 105% or lower than 95% of the price of last minute`,
|
|
"20019": "IP restrained to access the resource",
|
|
"20020": "Secret key does not exist",
|
|
"20021": "Index information does not exist",
|
|
"20022": "Wrong API interface",
|
|
"20023": "Fixed margin user",
|
|
"20024": "Signature does not match",
|
|
"20025": "Leverage rate error",
|
|
}
|
|
}
|
|
|
|
// WsOrderbook defines orderbook data from websocket connection
|
|
type WsOrderbook struct {
|
|
Asks [][]string `json:"asks"`
|
|
Bids [][]string `json:"bids"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
// WsResponse defines initial response stream
|
|
type WsResponse struct {
|
|
Channel string `json:"channel"`
|
|
Result bool `json:"result"`
|
|
Success bool `json:"success"`
|
|
ErrorCode string `json:"errorcode"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
// WsKlines defines a Kline response data from the websocket connection
|
|
type WsKlines struct {
|
|
Timestamp int64
|
|
Open float64
|
|
High float64
|
|
Low float64
|
|
Close float64
|
|
Volume float64
|
|
}
|
|
|
|
// WsTicker holds ticker data for websocket
|
|
type WsTicker struct {
|
|
High float64 `json:"high,string"`
|
|
Volume float64 `json:"vol,string"`
|
|
Last float64 `json:"last,string"`
|
|
Low float64 `json:"low,string"`
|
|
Buy float64 `json:"buy,string"`
|
|
Change float64 `json:"change,string"`
|
|
Sell float64 `json:"sell,string"`
|
|
DayLow float64 `json:"dayLow,string"`
|
|
Close float64 `json:"close,string"`
|
|
DayHigh float64 `json:"dayHigh,string"`
|
|
Open float64 `json:"open,string"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
// WsDeals defines a deal response from the websocket connection
|
|
type WsDeals struct {
|
|
TID int64
|
|
Price float64
|
|
Amount float64
|
|
Timestamp string
|
|
Type string
|
|
}
|