Files
gocryptotrader/exchanges/coinut/coinut.go
Scott 3a66e99899 Authenticated Websocket support (#315)
* Improves subscribing by not allowing duplicates. Adds bitmex auth support

* Adds coinbase pro support. Partial BTCC support. Adds WebsocketAuthenticatedEndpointsSupported websocket feature. Adds GateIO support

* Adds Coinut support

* Moves Coinut WS types to file. Implements Gemini's secure WS endpoint

* Adds HitBTC ws authenticated support. Fixes var names

* Adds huobi and hadax authenticated websocket support

* Adds auth to okgroup (okex, okcoin). Fixes some linting

* Adds Poloniex support

* Adds ZB support

* Adds proper bitmex support

* Improves bitfinex support, improves websocket functionality definitions

* Fixes coinbasepro auth

* Tests all endpoints

* go formatting, importing, linting run

* Adds wrapper supports

* General clean up. Data race destruction

* Improves testing on all exchanges except ZB

* Fixes ZB hashing, parsing and tests

* minor nits before someone else sees them <_<

* Fixes some nits pertaining to variable usage, comments, typos and rate limiting

* Addresses nits regarding types and test responses where applicable

* fmt import

* Fixes linting issues

* No longer returns an error on failure to authenticate, just logs. Adds new AuthenticatedWebsocketAPISupport config value to allow a user to seperate auth from REST and WS. Prevents WS auth if AuthenticatedWebsocketAPISupport is false, adds additional login check 'CanUseAuthenticatedEndpoints' for when login only occurs once (not per request). Removes unnecessary time.Sleeps from code. Moves WS auth error logic to auth function so that wrappers can get involved in all the auth fun. New-fandangled shared test package, used exclusively in testing, will be the store of all the constant boilerplate things like timeout values. Moves WS test setup function to only run once when there are multiple WS endpoint tests. Cleans up some struct types

* Increases test coverage with tests for config.areAuthenticatedCredentialsValid config.CheckExchangeConfigValues, exchange.SetAPIKeys, exchange.GetAuthenticatedAPISupport, exchange_websocket.CanUseAuthenticatedEndpoitns and exchange_websocket.SetCanUseAuthenticatedEndpoints. Adds b.Websocket.SetCanUseAuthenticatedEndpoints(false) when bitfinex fails to authenticate
Fixes a typo. gofmt and goimport

* Trim Test Typos

* Reformats various websocket types. Adds more specific error messaging to config.areAuthenticatedCredentialsValid
2019-06-19 13:19:01 +10:00

503 lines
14 KiB
Go

package coinut
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/request"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
log "github.com/thrasher-/gocryptotrader/logger"
)
const (
coinutAPIURL = "https://api.coinut.com"
coinutAPIVersion = "1"
coinutInstruments = "inst_list"
coinutTicker = "inst_tick"
coinutOrderbook = "inst_order_book"
coinutTrades = "inst_trade"
coinutBalance = "user_balance"
coinutOrder = "new_order"
coinutOrders = "new_orders"
coinutOrdersOpen = "user_open_orders"
coinutOrderCancel = "cancel_order"
coinutOrdersCancel = "cancel_orders"
coinutTradeHistory = "trade_history"
coinutIndexTicker = "index_tick"
coinutOptionChain = "option_chain"
coinutPositionHistory = "position_history"
coinutPositionOpen = "user_open_positions"
coinutAuthRate = 0
coinutUnauthRate = 0
coinutStatusOK = "OK"
)
// COINUT is the overarching type across the coinut package
type COINUT struct {
exchange.Base
WebsocketConn *websocket.Conn
InstrumentMap map[string]int
wsRequestMtx sync.Mutex
}
// SetDefaults sets current default values
func (c *COINUT) SetDefaults() {
c.Name = "COINUT"
c.Enabled = false
c.Verbose = false
c.TakerFee = 0.1 // spot
c.MakerFee = 0
c.Verbose = false
c.RESTPollingDelay = 10
c.APIWithdrawPermissions = exchange.WithdrawCryptoViaWebsiteOnly |
exchange.WithdrawFiatViaWebsiteOnly
c.RequestCurrencyPairFormat.Delimiter = ""
c.RequestCurrencyPairFormat.Uppercase = true
c.ConfigCurrencyPairFormat.Delimiter = ""
c.ConfigCurrencyPairFormat.Uppercase = true
c.AssetTypes = []string{ticker.Spot}
c.SupportsAutoPairUpdating = true
c.SupportsRESTTickerBatching = false
c.Requester = request.New(c.Name,
request.NewRateLimit(time.Second, coinutAuthRate),
request.NewRateLimit(time.Second, coinutUnauthRate),
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
c.APIUrlDefault = coinutAPIURL
c.APIUrl = c.APIUrlDefault
c.WebsocketInit()
c.Websocket.Functionality = exchange.WebsocketTickerSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketSubmitOrderSupported |
exchange.WebsocketCancelOrderSupported
}
// Setup sets the current exchange configuration
func (c *COINUT) Setup(exch *config.ExchangeConfig) {
if !exch.Enabled {
c.SetEnabled(false)
} else {
c.Enabled = true
c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
c.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false)
c.SetHTTPClientTimeout(exch.HTTPTimeout)
c.SetHTTPClientUserAgent(exch.HTTPUserAgent)
c.RESTPollingDelay = exch.RESTPollingDelay
c.Verbose = exch.Verbose
c.HTTPDebugging = exch.HTTPDebugging
c.Websocket.SetWsStatusAndConnection(exch.Websocket)
c.BaseCurrencies = exch.BaseCurrencies
c.AvailablePairs = exch.AvailablePairs
c.EnabledPairs = exch.EnabledPairs
err := c.SetCurrencyPairFormat()
if err != nil {
log.Fatal(err)
}
err = c.SetAssetTypes()
if err != nil {
log.Fatal(err)
}
err = c.SetAutoPairDefaults()
if err != nil {
log.Fatal(err)
}
err = c.SetAPIURL(exch)
if err != nil {
log.Fatal(err)
}
err = c.SetClientProxyAddress(exch.ProxyAddress)
if err != nil {
log.Fatal(err)
}
err = c.WebsocketSetup(c.WsConnect,
c.Subscribe,
c.Unsubscribe,
exch.Name,
exch.Websocket,
exch.Verbose,
coinutWebsocketURL,
exch.WebsocketURL)
if err != nil {
log.Fatal(err)
}
}
}
// GetInstruments returns instruments
func (c *COINUT) GetInstruments() (Instruments, error) {
var result Instruments
params := make(map[string]interface{})
params["sec_type"] = "SPOT"
return result, c.SendHTTPRequest(coinutInstruments, params, false, &result)
}
// GetInstrumentTicker returns a ticker for a specific instrument
func (c *COINUT) GetInstrumentTicker(instrumentID int) (Ticker, error) {
var result Ticker
params := make(map[string]interface{})
params["inst_id"] = instrumentID
return result, c.SendHTTPRequest(coinutTicker, params, false, &result)
}
// GetInstrumentOrderbook returns the orderbooks for a specific instrument
func (c *COINUT) GetInstrumentOrderbook(instrumentID, limit int) (Orderbook, error) {
var result Orderbook
params := make(map[string]interface{})
params["inst_id"] = instrumentID
if limit > 0 {
params["top_n"] = limit
}
return result, c.SendHTTPRequest(coinutOrderbook, params, false, &result)
}
// GetTrades returns trade information
func (c *COINUT) GetTrades(instrumentID int) (Trades, error) {
var result Trades
params := make(map[string]interface{})
params["inst_id"] = instrumentID
return result, c.SendHTTPRequest(coinutTrades, params, false, &result)
}
// GetUserBalance returns the full user balance
func (c *COINUT) GetUserBalance() (UserBalance, error) {
result := UserBalance{}
return result, c.SendHTTPRequest(coinutBalance, nil, true, &result)
}
// NewOrder places a new order on the exchange
func (c *COINUT) NewOrder(instrumentID int, quantity, price float64, buy bool, orderID uint32) (interface{}, error) {
var result interface{}
params := make(map[string]interface{})
params["inst_id"] = instrumentID
if price > 0 {
params["price"] = fmt.Sprintf("%v", price)
}
params["qty"] = fmt.Sprintf("%v", quantity)
params["side"] = "BUY"
if !buy {
params["side"] = "SELL"
}
params["client_ord_id"] = orderID
err := c.SendHTTPRequest(coinutOrder, params, true, &result)
if _, ok := result.(OrderRejectResponse); ok {
return result.(OrderRejectResponse), err
}
if _, ok := result.(OrderFilledResponse); ok {
return result.(OrderFilledResponse), err
}
if _, ok := result.(OrdersBase); ok {
return result.(OrdersBase), err
}
return result, err
}
// NewOrders places multiple orders on the exchange
func (c *COINUT) NewOrders(orders []Order) ([]OrdersBase, error) {
var result OrdersResponse
params := make(map[string]interface{})
params["orders"] = orders
return result.Data, c.SendHTTPRequest(coinutOrders, params, true, &result.Data)
}
// GetOpenOrders returns a list of open order and relevant information
func (c *COINUT) GetOpenOrders(instrumentID int) (GetOpenOrdersResponse, error) {
var result GetOpenOrdersResponse
params := make(map[string]interface{})
params["inst_id"] = instrumentID
return result, c.SendHTTPRequest(coinutOrdersOpen, params, true, &result)
}
// CancelExistingOrder cancels a specific order and returns if it was actioned
func (c *COINUT) CancelExistingOrder(instrumentID, orderID int) (bool, error) {
var result GenericResponse
params := make(map[string]interface{})
type Request struct {
InstrumentID int `json:"inst_id"`
OrderID int `json:"order_id"`
}
var entry = Request{
InstrumentID: instrumentID,
OrderID: orderID,
}
entries := []Request{entry}
params["entries"] = entries
err := c.SendHTTPRequest(coinutOrdersCancel, params, true, &result)
if err != nil {
return false, err
}
return true, nil
}
// CancelOrders cancels multiple orders
func (c *COINUT) CancelOrders(orders []CancelOrders) (CancelOrdersResponse, error) {
var result CancelOrdersResponse
params := make(map[string]interface{})
type Request struct {
InstrumentID int `json:"inst_id"`
OrderID int `json:"order_id"`
}
var entries []CancelOrders
entries = append(entries, orders...)
params["entries"] = entries
return result, c.SendHTTPRequest(coinutOrdersCancel, params, true, &result)
}
// GetTradeHistory returns trade history for a specific instrument.
func (c *COINUT) GetTradeHistory(instrumentID, start, limit int) (TradeHistory, error) {
var result TradeHistory
params := make(map[string]interface{})
params["inst_id"] = instrumentID
if start >= 0 && start <= 100 {
params["start"] = start
}
if limit >= 0 && start <= 100 {
params["limit"] = limit
}
return result, c.SendHTTPRequest(coinutTradeHistory, params, true, &result)
}
// GetIndexTicker returns the index ticker for an asset
func (c *COINUT) GetIndexTicker(asset string) (IndexTicker, error) {
var result IndexTicker
params := make(map[string]interface{})
params["asset"] = asset
return result, c.SendHTTPRequest(coinutIndexTicker, params, false, &result)
}
// GetDerivativeInstruments returns a list of derivative instruments
func (c *COINUT) GetDerivativeInstruments(secType string) (interface{}, error) {
var result interface{} // to-do
params := make(map[string]interface{})
params["sec_type"] = secType
return result, c.SendHTTPRequest(coinutInstruments, params, false, &result)
}
// GetOptionChain returns option chain
func (c *COINUT) GetOptionChain(asset, secType string) (OptionChainResponse, error) {
var result OptionChainResponse
params := make(map[string]interface{})
params["asset"] = asset
params["sec_type"] = secType
return result, c.SendHTTPRequest(coinutOptionChain, params, false, &result)
}
// GetPositionHistory returns position history
func (c *COINUT) GetPositionHistory(secType string, start, limit int) (PositionHistory, error) {
var result PositionHistory
params := make(map[string]interface{})
params["sec_type"] = secType
if start >= 0 {
params["start"] = start
}
if limit >= 0 {
params["limit"] = limit
}
return result, c.SendHTTPRequest(coinutPositionHistory, params, true, &result)
}
// GetOpenPositions returns all your current opened positions
func (c *COINUT) GetOpenPositions(instrumentID int) ([]OpenPosition, error) {
type Response struct {
Positions []OpenPosition `json:"positions"`
}
var result Response
params := make(map[string]interface{})
params["inst_id"] = instrumentID
return result.Positions,
c.SendHTTPRequest(coinutPositionOpen, params, true, &result)
}
// to-do: user position update via websocket
// SendHTTPRequest sends either an authenticated or unauthenticated HTTP request
func (c *COINUT) SendHTTPRequest(apiRequest string, params map[string]interface{}, authenticated bool, result interface{}) (err error) {
if !c.AuthenticatedAPISupport && authenticated {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, c.Name)
}
n := c.Requester.GetNonce(false)
if params == nil {
params = map[string]interface{}{}
}
params["nonce"] = n
params["request"] = apiRequest
payload, err := common.JSONEncode(params)
if err != nil {
return errors.New("sendHTTPRequest: Unable to JSON request")
}
if c.Verbose {
log.Debugf("Request JSON: %s", payload)
}
headers := make(map[string]string)
if authenticated {
headers["X-USER"] = c.ClientID
hmac := common.GetHMAC(common.HashSHA256, payload, []byte(c.APIKey))
headers["X-SIGNATURE"] = common.HexEncodeToString(hmac)
}
headers["Content-Type"] = "application/json"
var rawMsg json.RawMessage
err = c.SendPayload(http.MethodPost,
c.APIUrl,
headers,
bytes.NewBuffer(payload),
&rawMsg,
authenticated,
true,
c.Verbose,
c.HTTPDebugging,
)
if err != nil {
return err
}
var genResp GenericResponse
err = common.JSONDecode(rawMsg, &genResp)
if err != nil {
return err
}
if genResp.Status[0] != coinutStatusOK {
return fmt.Errorf("%s SendHTTPRequest error: %s", c.Name,
genResp.Status[0])
}
return common.JSONDecode(rawMsg, result)
}
// GetFee returns an estimate of fee based on type of transaction
func (c *COINUT) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee = c.calculateTradingFee(feeBuilder.Pair.Base,
feeBuilder.Pair.Quote,
feeBuilder.PurchasePrice,
feeBuilder.Amount,
feeBuilder.IsMaker)
case exchange.InternationalBankWithdrawalFee:
fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency,
feeBuilder.Amount)
case exchange.InternationalBankDepositFee:
fee = getInternationalBankDepositFee(feeBuilder.FiatCurrency,
feeBuilder.Amount)
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder.Pair, feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(c currency.Pair, price, amount float64) float64 {
if c.IsCryptoFiatPair() {
return 0.0035 * price * amount
}
return 0.002 * price * amount
}
func (c *COINUT) calculateTradingFee(base, quote currency.Code, purchasePrice, amount float64, isMaker bool) float64 {
var fee float64
switch {
case isMaker:
fee = 0
case currency.NewPair(base, quote).IsCryptoFiatPair():
fee = 0.002
default:
fee = 0.001
}
return fee * amount * purchasePrice
}
func getInternationalBankWithdrawalFee(c currency.Code, amount float64) float64 {
var fee float64
switch c {
case currency.USD:
if amount*0.001 < 10 {
fee = 10
} else {
fee = amount * 0.001
}
case currency.CAD:
if amount*0.005 < 10 {
fee = 2
} else {
fee = amount * 0.005
}
case currency.SGD:
if amount*0.001 < 10 {
fee = 10
} else {
fee = amount * 0.001
}
}
return fee
}
func getInternationalBankDepositFee(c currency.Code, amount float64) float64 {
var fee float64
if c == currency.USD {
if amount*0.001 < 10 {
fee = 10
} else {
fee = amount * 0.001
}
} else if c == currency.CAD {
if amount*0.005 < 10 {
fee = 2
} else {
fee = amount * 0.005
}
}
return fee
}