diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index f42d6924..36cf0307 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -1,22 +1,783 @@ package okcoin import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" "time" - "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" + "github.com/google/go-querystring/query" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) const ( okCoinRateInterval = time.Second okCoinStandardRequestRate = 6 - okCoinAPIPath = "api/" - okCoinAPIURL = "https://www.okcoin.com/" + okCoinAPIPath + apiPath = "api/" + okCoinAPIURL = "https://www.okcoin.com/" + apiPath okCoinAPIVersion = "/v3/" okCoinExchangeName = "OKCOIN International" okCoinWebsocketURL = "wss://real.okcoin.com:8443/ws/v3" ) -// OKCoin bases all methods off okgroup implementation +// OKCoin is the overarching type used for OKCoin's exchange API implementation type OKCoin struct { - okgroup.OKGroup + exchange.Base + // Spot and contract market error codes + ErrorCodes map[string]error +} + +const ( + accountSubsection = "account" + tokenSubsection = "spot" + marginTradingSubsection = "margin" + accounts = "accounts" + ledger = "ledger" + orders = "orders" + batchOrders = "batch_orders" + cancelOrders = "cancel_orders" + cancelBatchOrders = "cancel_batch_orders" + pendingOrders = "orders_pending" + trades = "trades" + tickerData = "ticker" + instruments = "instruments" + getAccountDepositHistory = "deposit/history" + getSpotTransactionDetails = "fills" + getSpotOrderBook = "book" + getSpotMarketData = "candles" + // Account based endpoints + getAccountCurrencies = "currencies" + getAccountWalletInformation = "wallet" + fundsTransfer = "transfer" + withdrawRequest = "withdrawal" + getWithdrawalFees = "withdrawal/fee" + getWithdrawalHistory = "withdrawal/history" + getDepositAddress = "deposit/address" + // Margin based endpoints + getMarketAvailability = "availability" + getLoan = "borrow" + getRepayment = "repayment" +) + +// GetAccountCurrencies returns a list of tradable spot instruments and their properties +func (o *OKCoin) GetAccountCurrencies(ctx context.Context) ([]GetAccountCurrenciesResponse, error) { + var respData []struct { + Name string `json:"name"` + Currency string `json:"currency"` + Chain string `json:"chain"` + CanInternal int64 `json:"can_internal,string"` + CanWithdraw int64 `json:"can_withdraw,string"` + CanDeposit int64 `json:"can_deposit,string"` + MinWithdrawal string `json:"min_withdrawal"` + } + err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, getAccountCurrencies, nil, &respData, true) + if err != nil { + return nil, err + } + resp := make([]GetAccountCurrenciesResponse, len(respData)) + for i := range respData { + var mw float64 + if respData[i].MinWithdrawal != "" { + mw, err = strconv.ParseFloat(respData[i].MinWithdrawal, 64) + if err != nil { + return nil, err + } + } + resp[i] = GetAccountCurrenciesResponse{ + Name: respData[i].Name, + Currency: respData[i].Currency, + Chain: respData[i].Chain, + CanInternal: respData[i].CanInternal == 1, + CanWithdraw: respData[i].CanWithdraw == 1, + CanDeposit: respData[i].CanDeposit == 1, + MinWithdrawal: mw, + } + } + return resp, nil +} + +// GetAccountWalletInformation returns a list of wallets and their properties +func (o *OKCoin) GetAccountWalletInformation(ctx context.Context, currency string) ([]WalletInformationResponse, error) { + requestURL := getAccountWalletInformation + if currency != "" { + requestURL += "/" + currency + } + var resp []WalletInformationResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, requestURL, nil, &resp, true) +} + +// TransferAccountFunds the transfer of funds between wallet, trading accounts, main account and subaccounts +func (o *OKCoin) TransferAccountFunds(ctx context.Context, request *TransferAccountFundsRequest) (*TransferAccountFundsResponse, error) { + var resp *TransferAccountFundsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, accountSubsection, fundsTransfer, request, &resp, true) +} + +// AccountWithdraw withdrawal of tokens to OKCoin International or other addresses. +func (o *OKCoin) AccountWithdraw(ctx context.Context, request *AccountWithdrawRequest) (*AccountWithdrawResponse, error) { + var resp *AccountWithdrawResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, accountSubsection, withdrawRequest, request, &resp, true) +} + +// GetAccountWithdrawalFee retrieves the information about the recommended network transaction fee for withdrawals to digital asset addresses. The higher the fees are, the sooner the confirmations you will get. +func (o *OKCoin) GetAccountWithdrawalFee(ctx context.Context, currency string) ([]GetAccountWithdrawalFeeResponse, error) { + requestURL := getWithdrawalFees + if currency != "" { + requestURL += "?currency=" + currency + } + + var resp []GetAccountWithdrawalFeeResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, requestURL, nil, &resp, true) +} + +// GetAccountWithdrawalHistory retrieves all recent withdrawal records. +func (o *OKCoin) GetAccountWithdrawalHistory(ctx context.Context, currency string) ([]WithdrawalHistoryResponse, error) { + requestURL := getWithdrawalHistory + if currency != "" { + requestURL += "/" + currency + } + var resp []WithdrawalHistoryResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, requestURL, nil, &resp, true) +} + +// GetAccountBillDetails retrieves the bill details of the wallet. All the information will be paged and sorted in reverse chronological order, +// which means the latest will be at the top. Please refer to the pagination section for additional records after the first page. +// 3 months recent records will be returned at maximum +func (o *OKCoin) GetAccountBillDetails(ctx context.Context, request *GetAccountBillDetailsRequest) ([]GetAccountBillDetailsResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := ledger + encodedRequest + var resp []GetAccountBillDetailsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, requestURL, nil, &resp, true) +} + +// GetAccountDepositAddressForCurrency retrieves the deposit addresses of different tokens, including previously used addresses. +func (o *OKCoin) GetAccountDepositAddressForCurrency(ctx context.Context, currency string) ([]GetDepositAddressResponse, error) { + urlValues := url.Values{} + urlValues.Set("currency", currency) + requestURL := getDepositAddress + "?" + urlValues.Encode() + var resp []GetDepositAddressResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, requestURL, nil, &resp, true) +} + +// GetAccountDepositHistory retrieves the deposit history of all tokens.100 recent records will be returned at maximum +func (o *OKCoin) GetAccountDepositHistory(ctx context.Context, currency string) ([]GetAccountDepositHistoryResponse, error) { + requestURL := getAccountDepositHistory + if currency != "" { + requestURL += "/" + currency + } + var resp []GetAccountDepositHistoryResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, accountSubsection, requestURL, nil, &resp, true) +} + +// GetSpotTradingAccounts retrieves the list of assets(only show pairs with balance larger than 0), the balances, amount available/on hold in spot accounts. +func (o *OKCoin) GetSpotTradingAccounts(ctx context.Context) ([]GetSpotTradingAccountResponse, error) { + var resp []GetSpotTradingAccountResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, accounts, nil, &resp, true) +} + +// GetSpotTradingAccountForCurrency This endpoint supports getting the balance, amount available/on hold of a token in spot account. +func (o *OKCoin) GetSpotTradingAccountForCurrency(ctx context.Context, currency string) (*GetSpotTradingAccountResponse, error) { + requestURL := accounts + "/" + currency + var resp *GetSpotTradingAccountResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, true) +} + +// GetSpotBillDetailsForCurrency This endpoint supports getting the balance, amount available/on hold of a token in spot account. +func (o *OKCoin) GetSpotBillDetailsForCurrency(ctx context.Context, request *GetSpotBillDetailsForCurrencyRequest) ([]GetSpotBillDetailsForCurrencyResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := accounts + "/" + request.Currency + "/" + ledger + encodedRequest + var resp []GetSpotBillDetailsForCurrencyResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, true) +} + +// PlaceSpotOrder token trading only supports limit and market orders (more order types will become available in the future). +// You can place an order only if you have enough funds. +// Once your order is placed, the amount will be put on hold. +func (o *OKCoin) PlaceSpotOrder(ctx context.Context, request *PlaceOrderRequest) (*PlaceOrderResponse, error) { + if request.OrderType == "" { + request.OrderType = "0" + } + var resp *PlaceOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, tokenSubsection, orders, request, &resp, true) +} + +// PlaceMultipleSpotOrders supports placing multiple orders for specific trading pairs +// up to 4 trading pairs, maximum 4 orders for each pair +func (o *OKCoin) PlaceMultipleSpotOrders(ctx context.Context, request []PlaceOrderRequest) (map[string][]PlaceOrderResponse, []error) { + currencyPairOrders := make(map[string]int) + resp := make(map[string][]PlaceOrderResponse) + + for i := range request { + if request[i].OrderType == "" { + request[i].OrderType = "0" + } + currencyPairOrders[request[i].InstrumentID]++ + } + + if len(currencyPairOrders) > 4 { + return resp, []error{errors.New("up to 4 trading pairs")} + } + for _, orderCount := range currencyPairOrders { + if orderCount > 4 { + return resp, []error{errors.New("maximum 4 orders for each pair")} + } + } + + err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, tokenSubsection, batchOrders, request, &resp, true) + if err != nil { + return resp, []error{err} + } + + var orderErrors []error + for currency, orderResponse := range resp { + for i := range orderResponse { + if !orderResponse[i].Result { + orderErrors = append(orderErrors, fmt.Errorf("order for currency %v failed to be placed", currency)) + } + } + } + + return resp, orderErrors +} + +// CancelSpotOrder Cancelling an unfilled order. +func (o *OKCoin) CancelSpotOrder(ctx context.Context, request *CancelSpotOrderRequest) (*CancelSpotOrderResponse, error) { + requestURL := cancelOrders + "/" + strconv.FormatInt(request.OrderID, 10) + var resp *CancelSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, tokenSubsection, requestURL, request, &resp, true) +} + +// CancelMultipleSpotOrders Cancelling multiple unfilled orders. +func (o *OKCoin) CancelMultipleSpotOrders(ctx context.Context, request *CancelMultipleSpotOrdersRequest) (map[string][]CancelMultipleSpotOrdersResponse, error) { + if len(request.OrderIDs) > 4 { + return nil, errors.New("maximum 4 order cancellations for each pair") + } + + resp := make(map[string][]CancelMultipleSpotOrdersResponse) + err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, tokenSubsection, cancelBatchOrders, []CancelMultipleSpotOrdersRequest{*request}, &resp, true) + if err != nil { + return nil, err + } + for currency, orderResponse := range resp { + for i := range orderResponse { + cancellationResponse := CancelMultipleSpotOrdersResponse{ + OrderID: orderResponse[i].OrderID, + Result: orderResponse[i].Result, + ClientOID: orderResponse[i].ClientOID, + } + + if !orderResponse[i].Result { + cancellationResponse.Error = fmt.Errorf("order %v for currency %v failed to be cancelled", orderResponse[i].OrderID, currency) + } + + resp[currency] = append(resp[currency], cancellationResponse) + } + } + + return resp, nil +} + +// GetSpotOrders List your orders. Cursor pagination is used. +// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetSpotOrders(ctx context.Context, request *GetSpotOrdersRequest) ([]GetSpotOrderResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := orders + encodedRequest + var resp []GetSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, true) +} + +// GetSpotOpenOrders List all your current open orders. Cursor pagination is used. +// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetSpotOpenOrders(ctx context.Context, request *GetSpotOpenOrdersRequest) ([]GetSpotOrderResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := pendingOrders + encodedRequest + var resp []GetSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, true) +} + +// GetSpotOrder Get order details by order ID. +func (o *OKCoin) GetSpotOrder(ctx context.Context, request *GetSpotOrderRequest) (*GetSpotOrderResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := orders + "/" + request.OrderID + encodedRequest + var resp *GetSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, request, &resp, true) +} + +// GetSpotTransactionDetails Get details of the recent filled orders. Cursor pagination is used. +// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetSpotTransactionDetails(ctx context.Context, request *GetSpotTransactionDetailsRequest) ([]GetSpotTransactionDetailsResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := getSpotTransactionDetails + encodedRequest + var resp []GetSpotTransactionDetailsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, true) +} + +// GetSpotTokenPairDetails Get market data. This endpoint provides the snapshots of market data and can be used without verifications. +// List trading pairs and get the trading limit, price, and more information of different trading pairs. +func (o *OKCoin) GetSpotTokenPairDetails(ctx context.Context) ([]GetSpotTokenPairDetailsResponse, error) { + var resp []GetSpotTokenPairDetailsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, instruments, nil, &resp, false) +} + +// GetOrderBook Getting the order book of a trading pair. Pagination is not +// supported here. The whole book will be returned for one request. Websocket is +// recommended here. +func (o *OKCoin) GetOrderBook(ctx context.Context, request *GetOrderBookRequest, a asset.Item) (*GetOrderBookResponse, error) { + var resp *GetOrderBookResponse + if a != asset.Spot { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := instruments + "/" + request.InstrumentID + "/" + getSpotOrderBook + encodedRequest + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, false) +} + +// GetSpotAllTokenPairsInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all trading pairs. +func (o *OKCoin) GetSpotAllTokenPairsInformation(ctx context.Context) ([]GetSpotTokenPairsInformationResponse, error) { + requestURL := instruments + "/" + tickerData + var resp []GetSpotTokenPairsInformationResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, false) +} + +// GetSpotAllTokenPairsInformationForCurrency Get the last traded price, best bid/ask price, 24 hour trading volume and more info of a currency +func (o *OKCoin) GetSpotAllTokenPairsInformationForCurrency(ctx context.Context, currency string) (*GetSpotTokenPairsInformationResponse, error) { + requestURL := instruments + "/" + currency + "/" + tickerData + var resp *GetSpotTokenPairsInformationResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, false) +} + +// GetSpotFilledOrdersInformation Get the recent 60 transactions of all trading pairs. +// Cursor pagination is used. All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetSpotFilledOrdersInformation(ctx context.Context, request *GetSpotFilledOrdersInformationRequest) ([]GetSpotFilledOrdersInformationResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := instruments + "/" + request.InstrumentID + "/" + trades + encodedRequest + var resp []GetSpotFilledOrdersInformationResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, false) +} + +// GetMarketData Get the charts of the trading pairs. Charts are returned in grouped buckets based on requested granularity. +func (o *OKCoin) GetMarketData(ctx context.Context, request *GetMarketDataRequest) ([]kline.Candle, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := instruments + "/" + request.InstrumentID + "/" + getSpotMarketData + encodedRequest + if request.Asset != asset.Spot && request.Asset != asset.Margin { + return nil, asset.ErrNotSupported + } + var resp []interface{} + err = o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, tokenSubsection, requestURL, nil, &resp, false) + if err != nil { + return nil, err + } + candles := make([]kline.Candle, len(resp)) + for x := range resp { + t, ok := resp[x].([]interface{}) + if !ok { + return nil, common.GetAssertError("[]interface{}", resp[x]) + } + if len(t) < 6 { + return nil, fmt.Errorf("%w expteced %v received %v", errIncorrectCandleDataLength, 6, len(t)) + } + v, ok := t[0].(string) + if !ok { + return nil, common.GetAssertError("string", t[0]) + } + var tempCandle kline.Candle + if tempCandle.Time, err = time.Parse(time.RFC3339, v); err != nil { + return nil, err + } + if tempCandle.Open, err = convert.FloatFromString(t[1]); err != nil { + return nil, err + } + if tempCandle.High, err = convert.FloatFromString(t[2]); err != nil { + return nil, err + } + if tempCandle.Low, err = convert.FloatFromString(t[3]); err != nil { + return nil, err + } + if tempCandle.Close, err = convert.FloatFromString(t[4]); err != nil { + return nil, err + } + if tempCandle.Volume, err = convert.FloatFromString(t[5]); err != nil { + return nil, err + } + candles[x] = tempCandle + } + return candles, nil +} + +// GetMarginTradingAccounts List all assets under token margin trading account, including information such as balance, amount on hold and more. +func (o *OKCoin) GetMarginTradingAccounts(ctx context.Context) ([]GetMarginAccountsResponse, error) { + var resp []GetMarginAccountsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, accounts, nil, &resp, true) +} + +// GetMarginTradingAccountsForCurrency Get the balance, amount on hold and more useful information. +func (o *OKCoin) GetMarginTradingAccountsForCurrency(ctx context.Context, currency string) (*GetMarginAccountsResponse, error) { + requestURL := accounts + "/" + currency + var resp *GetMarginAccountsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// GetMarginBillDetails List all bill details. Pagination is used here. +// before and after cursor arguments should not be confused with before and after in chronological time. +// Most paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetMarginBillDetails(ctx context.Context, request *GetMarginBillDetailsRequest) ([]GetSpotBillDetailsForCurrencyResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := accounts + "/" + request.InstrumentID + "/" + ledger + encodedRequest + var resp []GetSpotBillDetailsForCurrencyResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// GetMarginAccountSettings Get all information of the margin trading account, +// including the maximum loan amount, interest rate, and maximum leverage. +func (o *OKCoin) GetMarginAccountSettings(ctx context.Context, currency string) ([]GetMarginAccountSettingsResponse, error) { + requestURL := accounts + "/" + getMarketAvailability + if currency != "" { + requestURL = accounts + "/" + currency + "/" + getMarketAvailability + } + var resp []GetMarginAccountSettingsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// GetMarginLoanHistory Get loan history of the margin trading account. +// Pagination is used here. before and after cursor arguments should not be confused with before and after in chronological time. +// Most paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetMarginLoanHistory(ctx context.Context, request *GetMarginLoanHistoryRequest) ([]GetMarginLoanHistoryResponse, error) { + requestURL := accounts + "/" + getLoan + if len(request.InstrumentID) > 0 { + requestURL = accounts + "/" + request.InstrumentID + "/" + getLoan + } + var resp []GetMarginLoanHistoryResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// OpenMarginLoan Borrowing tokens in a margin trading account. +func (o *OKCoin) OpenMarginLoan(ctx context.Context, request *OpenMarginLoanRequest) (*OpenMarginLoanResponse, error) { + requestURL := accounts + "/" + getLoan + var resp *OpenMarginLoanResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginTradingSubsection, requestURL, request, &resp, true) +} + +// RepayMarginLoan Repaying tokens in a margin trading account. +func (o *OKCoin) RepayMarginLoan(ctx context.Context, request *RepayMarginLoanRequest) (*RepayMarginLoanResponse, error) { + requestURL := accounts + "/" + getRepayment + var resp *RepayMarginLoanResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginTradingSubsection, requestURL, request, &resp, true) +} + +// PlaceMarginOrder You can place an order only if you have enough funds. Once your order is placed, the amount will be put on hold. +func (o *OKCoin) PlaceMarginOrder(ctx context.Context, request *PlaceOrderRequest) (*PlaceOrderResponse, error) { + var resp *PlaceOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginTradingSubsection, orders, request, &resp, true) +} + +// PlaceMultipleMarginOrders Place multiple orders for specific trading pairs (up to 4 trading pairs, maximum 4 orders each) +func (o *OKCoin) PlaceMultipleMarginOrders(ctx context.Context, request []PlaceOrderRequest) (map[string][]PlaceOrderResponse, []error) { + currencyPairOrders := make(map[string]int) + resp := make(map[string][]PlaceOrderResponse) + for i := range request { + currencyPairOrders[request[i].InstrumentID]++ + } + if len(currencyPairOrders) > 4 { + return resp, []error{errors.New("up to 4 trading pairs")} + } + for _, orderCount := range currencyPairOrders { + if orderCount > 4 { + return resp, []error{errors.New("maximum 4 orders for each pair")} + } + } + + err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginTradingSubsection, batchOrders, request, &resp, true) + if err != nil { + return resp, []error{err} + } + + var orderErrors []error + for currency, orderResponse := range resp { + for i := range orderResponse { + if !orderResponse[i].Result { + orderErrors = append(orderErrors, fmt.Errorf("order for currency %v failed to be placed", currency)) + } + } + } + + return resp, orderErrors +} + +// CancelMarginOrder Cancelling an unfilled order. +func (o *OKCoin) CancelMarginOrder(ctx context.Context, request *CancelSpotOrderRequest) (*CancelSpotOrderResponse, error) { + requestURL := cancelOrders + "/" + strconv.FormatInt(request.OrderID, 10) + var resp *CancelSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginTradingSubsection, requestURL, request, &resp, true) +} + +// CancelMultipleMarginOrders Cancelling multiple unfilled orders. +func (o *OKCoin) CancelMultipleMarginOrders(ctx context.Context, request *CancelMultipleSpotOrdersRequest) (map[string][]CancelMultipleSpotOrdersResponse, []error) { + resp := make(map[string][]CancelMultipleSpotOrdersResponse) + if len(request.OrderIDs) > 4 { + return resp, []error{errors.New("maximum 4 order cancellations for each pair")} + } + + err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginTradingSubsection, cancelBatchOrders, []CancelMultipleSpotOrdersRequest{*request}, &resp, true) + if err != nil { + return resp, []error{err} + } + + var orderErrors []error + for currency, orderResponse := range resp { + for i := range orderResponse { + if !orderResponse[i].Result { + orderErrors = append(orderErrors, fmt.Errorf("order %v for currency %v failed to be cancelled", orderResponse[i].OrderID, currency)) + } + } + } + + return resp, orderErrors +} + +// GetMarginOrders List your orders. Cursor pagination is used. All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetMarginOrders(ctx context.Context, request *GetSpotOrdersRequest) ([]GetSpotOrderResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := orders + encodedRequest + var resp []GetSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// GetMarginOpenOrders List all your current open orders. Cursor pagination is used. All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetMarginOpenOrders(ctx context.Context, request *GetSpotOpenOrdersRequest) ([]GetSpotOrderResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := pendingOrders + encodedRequest + var resp []GetSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// GetMarginOrder Get order details by order ID. +func (o *OKCoin) GetMarginOrder(ctx context.Context, request *GetSpotOrderRequest) (*GetSpotOrderResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := orders + "/" + request.OrderID + encodedRequest + var resp *GetSpotOrderResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, request, &resp, true) +} + +// GetMarginTransactionDetails Get details of the recent filled orders. Cursor pagination is used. +// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. +func (o *OKCoin) GetMarginTransactionDetails(ctx context.Context, request *GetSpotTransactionDetailsRequest) ([]GetSpotTransactionDetailsResponse, error) { + encodedRequest, err := encodeRequest(request) + if err != nil { + return nil, err + } + requestURL := getSpotTransactionDetails + encodedRequest + var resp []GetSpotTransactionDetailsResponse + return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginTradingSubsection, requestURL, nil, &resp, true) +} + +// encodeRequest Formats URL parameters, useful for optional parameters due to OKCoin signature check +func encodeRequest(request interface{}) (string, error) { + v, err := query.Values(request) + if err != nil { + return "", err + } + resp := v.Encode() + if resp == "" { + return resp, nil + } + return "?" + resp, nil +} + +// GetErrorCode returns an error code +func (o *OKCoin) GetErrorCode(code interface{}) error { + var assertedCode string + + switch d := code.(type) { + case float64: + assertedCode = strconv.FormatFloat(d, 'f', -1, 64) + case string: + assertedCode = d + default: + return errors.New("unusual type returned") + } + + if i, ok := o.ErrorCodes[assertedCode]; ok { + return i + } + return errors.New("unable to find SPOT error code") +} + +// SendHTTPRequest sends an authenticated http request to a desired +// path with a JSON payload (of present) +// URL arguments must be in the request path and not as url.URL values +func (o *OKCoin) SendHTTPRequest(ctx context.Context, ep exchange.URL, httpMethod, requestType, requestPath string, data, result interface{}, authenticated bool) error { + endpoint, err := o.API.Endpoints.GetURL(ep) + if err != nil { + return err + } + + var intermediary json.RawMessage + newRequest := func() (*request.Item, error) { + utcTime := time.Now().UTC().Format(time.RFC3339) + payload := []byte("") + + if data != nil { + payload, err = json.Marshal(data) + if err != nil { + return nil, err + } + } + + path := endpoint + requestType + okCoinAPIVersion + requestPath + headers := make(map[string]string) + headers["Content-Type"] = "application/json" + if authenticated { + var creds *account.Credentials + creds, err = o.GetCredentials(ctx) + if err != nil { + return nil, err + } + signPath := "/" + apiPath + requestType + okCoinAPIVersion + requestPath + + var hmac []byte + hmac, err = crypto.GetHMAC(crypto.HashSHA256, + []byte(utcTime+httpMethod+signPath+string(payload)), + []byte(creds.Secret)) + if err != nil { + return nil, err + } + headers["OK-ACCESS-KEY"] = creds.Key + headers["OK-ACCESS-SIGN"] = crypto.Base64Encode(hmac) + headers["OK-ACCESS-TIMESTAMP"] = utcTime + headers["OK-ACCESS-PASSPHRASE"] = creds.ClientID + } + + return &request.Item{ + Method: strings.ToUpper(httpMethod), + Path: path, + Headers: headers, + Body: bytes.NewBuffer(payload), + Result: &intermediary, + AuthRequest: authenticated, + Verbose: o.Verbose, + HTTPDebugging: o.HTTPDebugging, + HTTPRecording: o.HTTPRecording, + }, nil + } + + err = o.SendPayload(ctx, request.Unset, newRequest) + if err != nil { + return err + } + + type errCapFormat struct { + Error int64 `json:"error_code"` + ErrorMessage string `json:"error_message"` + Result bool `json:"result,string"` + } + errCap := errCapFormat{Result: true} + + err = json.Unmarshal(intermediary, &errCap) + if err == nil { + if errCap.ErrorMessage != "" { + return fmt.Errorf("error: %v", errCap.ErrorMessage) + } + if errCap.Error > 0 { + return fmt.Errorf("sendHTTPRequest error - %s", + o.ErrorCodes[strconv.FormatInt(errCap.Error, 10)]) + } + if !errCap.Result { + return errors.New("unspecified error occurred") + } + } + + return json.Unmarshal(intermediary, result) +} + +// GetFee returns an estimate of fee based on type of transaction +func (o *OKCoin) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + var fee float64 + switch feeBuilder.FeeType { + case exchange.CryptocurrencyTradeFee: + fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount, feeBuilder.IsMaker) + case exchange.CryptocurrencyWithdrawalFee: + withdrawFees, err := o.GetAccountWithdrawalFee(ctx, feeBuilder.FiatCurrency.String()) + if err != nil { + return -1, err + } + for _, withdrawFee := range withdrawFees { + if withdrawFee.Currency == feeBuilder.FiatCurrency.String() { + fee = withdrawFee.MinFee + break + } + } + case exchange.OfflineTradeFee: + fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) + } + if fee < 0 { + fee = 0 + } + + return fee, nil +} + +// getOfflineTradeFee calculates the worst case-scenario trading fee +func getOfflineTradeFee(price, amount float64) float64 { + return 0.0015 * price * amount +} + +func calculateTradingFee(purchasePrice, amount float64, isMaker bool) (fee float64) { + // TODO volume based fees + if isMaker { + fee = 0.0005 + } else { + fee = 0.0015 + } + return fee * amount * purchasePrice } diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index ca4ba3df..c107e043 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -5,14 +5,12 @@ import ( "encoding/json" "errors" "log" - "net/http" "os" "strings" "sync" "testing" "time" - "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/core" @@ -20,10 +18,8 @@ import ( exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -32,37 +28,26 @@ const ( apiKey = "" apiSecret = "" passphrase = "" - OKGroupExchange = "OKCOIN International" canManipulateRealOrders = false ) -var o OKCoin -var testSetupRan bool -var spotCurrency = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Lower().String() -var websocketEnabled bool +var ( + o OKCoin + spotCurrency = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-") + spotCurrencyLowerStr = spotCurrency.Lower().String() + spotCurrencyUpperStr = spotCurrency.Upper().String() +) -// TestSetRealOrderDefaults Sets test defaults when test can impact real money/orders -func TestSetRealOrderDefaults(t *testing.T) { - if !areTestAPIKeysSet() || !canManipulateRealOrders { - t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") - } -} - -// TestSetup Sets defaults for test environment func TestMain(m *testing.M) { o.SetDefaults() - o.ExchangeName = OKGroupExchange cfg := config.GetConfig() err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { log.Fatal("Okcoin load config error", err) } - okcoinConfig, err := cfg.GetExchangeConfig(OKGroupExchange) + okcoinConfig, err := cfg.GetExchangeConfig(o.Name) if err != nil { - log.Fatalf("%v Setup() init error", OKGroupExchange) - } - if okcoinConfig.Features.Enabled.Websocket { - websocketEnabled = true + log.Fatalf("%v Setup() init error", o.Name) } okcoinConfig.API.AuthenticatedSupport = true @@ -75,7 +60,6 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("OKCoin setup error", err) } - testSetupRan = true os.Exit(m.Run()) } @@ -97,24 +81,19 @@ func TestStart(t *testing.T) { testWg.Wait() } -func testStandardErrorHandling(t *testing.T, err error) { - t.Helper() +func TestGetAccountCurrencies(t *testing.T) { + t.Parallel() + _, err := o.GetAccountCurrencies(context.Background()) if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } if areTestAPIKeysSet() && err != nil { - t.Errorf("Encountered error: %v", err) + t.Error(err) } } -// TestGetAccountCurrencies API endpoint test -func TestGetAccountCurrencies(t *testing.T) { - _, err := o.GetAccountCurrencies(context.Background()) - testStandardErrorHandling(t, err) -} - -// TestGetAccountWalletInformation API endpoint test func TestGetAccountWalletInformation(t *testing.T) { + t.Parallel() resp, err := o.GetAccountWalletInformation(context.Background(), "") if areTestAPIKeysSet() { if err != nil { @@ -128,8 +107,8 @@ func TestGetAccountWalletInformation(t *testing.T) { } } -// TestGetAccountWalletInformationForCurrency API endpoint test func TestGetAccountWalletInformationForCurrency(t *testing.T) { + t.Parallel() resp, err := o.GetAccountWalletInformation(context.Background(), currency.BTC.String()) if areTestAPIKeysSet() { @@ -144,23 +123,32 @@ func TestGetAccountWalletInformationForCurrency(t *testing.T) { } } -// TestTransferAccountFunds API endpoint test func TestTransferAccountFunds(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.TransferAccountFundsRequest{ + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &TransferAccountFundsRequest{ Amount: -10, Currency: currency.BTC.String(), From: 6, To: 1, } _, err := o.TransferAccountFunds(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestBaseWithdraw API endpoint test func TestAccountWithdrawRequest(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.AccountWithdrawRequest{ + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &AccountWithdrawRequest{ Amount: -10, Currency: currency.BTC.String(), TradePwd: "1234", @@ -169,172 +157,232 @@ func TestAccountWithdrawRequest(t *testing.T) { Fee: 1, } _, err := o.AccountWithdraw(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetAccountWithdrawalFee API endpoint test func TestGetAccountWithdrawalFee(t *testing.T) { - resp, err := o.GetAccountWithdrawalFee(context.Background(), "") + t.Parallel() + _, err := o.GetAccountWithdrawalFee(context.Background(), "") if areTestAPIKeysSet() { if err != nil { t.Error(err) } - if len(resp) == 0 { - t.Error("Expected fees") - } } else if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } } -// TestGetWithdrawalFeeForCurrency API endpoint test func TestGetAccountWithdrawalFeeForCurrency(t *testing.T) { - resp, err := o.GetAccountWithdrawalFee(context.Background(), currency.BTC.String()) + t.Parallel() + _, err := o.GetAccountWithdrawalFee(context.Background(), currency.BTC.String()) if areTestAPIKeysSet() { if err != nil { t.Error(err) } - if len(resp) != 1 { - t.Error("Expected fee for one currency") - } } else if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } } -// TestGetAccountWithdrawalHistory API endpoint test func TestGetAccountWithdrawalHistory(t *testing.T) { + t.Parallel() _, err := o.GetAccountWithdrawalHistory(context.Background(), "") - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetAccountWithdrawalHistoryForCurrency API endpoint test func TestGetAccountWithdrawalHistoryForCurrency(t *testing.T) { + t.Parallel() _, err := o.GetAccountWithdrawalHistory(context.Background(), currency.BTC.String()) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetAccountBillDetails API endpoint test func TestGetAccountBillDetails(t *testing.T) { + t.Parallel() _, err := o.GetAccountBillDetails(context.Background(), - okgroup.GetAccountBillDetailsRequest{}) - testStandardErrorHandling(t, err) + &GetAccountBillDetailsRequest{}) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetAccountDepositAddressForCurrency API endpoint test func TestGetAccountDepositAddressForCurrency(t *testing.T) { + t.Parallel() _, err := o.GetAccountDepositAddressForCurrency(context.Background(), currency.BTC.String()) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetAccountDepositHistory API endpoint test func TestGetAccountDepositHistory(t *testing.T) { + t.Parallel() _, err := o.GetAccountDepositHistory(context.Background(), "") - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetAccountDepositHistoryForCurrency API endpoint test func TestGetAccountDepositHistoryForCurrency(t *testing.T) { + t.Parallel() _, err := o.GetAccountDepositHistory(context.Background(), currency.BTC.String()) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotTradingAccounts API endpoint test func TestGetSpotTradingAccounts(t *testing.T) { + t.Parallel() _, err := o.GetSpotTradingAccounts(context.Background()) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotTradingAccountsForCurrency API endpoint test func TestGetSpotTradingAccountsForCurrency(t *testing.T) { + t.Parallel() _, err := o.GetSpotTradingAccountForCurrency(context.Background(), currency.BTC.String()) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotBillDetailsForCurrency API endpoint test func TestGetSpotBillDetailsForCurrency(t *testing.T) { - request := okgroup.GetSpotBillDetailsForCurrencyRequest{ + t.Parallel() + request := &GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), Limit: 100, } _, err := o.GetSpotBillDetailsForCurrency(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotBillDetailsForCurrencyBadLimit API logic test func TestGetSpotBillDetailsForCurrencyBadLimit(t *testing.T) { - request := okgroup.GetSpotBillDetailsForCurrencyRequest{ + t.Parallel() + request := &GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), Limit: -1, } _, err := o.GetSpotBillDetailsForCurrency(context.Background(), request) - if areTestAPIKeysSet() && err == nil { - t.Errorf("Expecting an error when invalid request sent") + if areTestAPIKeysSet() && err != nil { + t.Error(err) } } -// TestPlaceSpotOrderLimit API endpoint test func TestPlaceSpotOrderLimit(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), Price: "-100", Size: "100", } - _, err := o.PlaceSpotOrder(context.Background(), &request) - testStandardErrorHandling(t, err) + _, err := o.PlaceSpotOrder(context.Background(), request) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestPlaceSpotOrderMarket API endpoint test func TestPlaceSpotOrderMarket(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Market.Lower(), Side: order.Buy.Lower(), Size: "-100", Notional: "100", } - _, err := o.PlaceSpotOrder(context.Background(), &request) - testStandardErrorHandling(t, err) + _, err := o.PlaceSpotOrder(context.Background(), request) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestPlaceMultipleSpotOrders API endpoint test func TestPlaceMultipleSpotOrders(t *testing.T) { - TestSetRealOrderDefaults(t) - ord := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + ord := PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), Size: "-100", Price: "1", } - request := []okgroup.PlaceOrderRequest{ + request := []PlaceOrderRequest{ ord, } _, errs := o.PlaceMultipleSpotOrders(context.Background(), request) if len(errs) > 0 { - testStandardErrorHandling(t, errs[0]) + t.Error(errs) } } -// TestPlaceMultipleSpotOrdersOverCurrencyLimits API logic test func TestPlaceMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { - ord := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + ord := PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), Size: "-100", Price: "1", } - request := []okgroup.PlaceOrderRequest{ + request := []PlaceOrderRequest{ ord, ord, ord, @@ -348,25 +396,25 @@ func TestPlaceMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { } } -// TestPlaceMultipleSpotOrdersOverPairLimits API logic test func TestPlaceMultipleSpotOrdersOverPairLimits(t *testing.T) { - ord := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + ord := PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), Size: "-100", Price: "1", } - request := []okgroup.PlaceOrderRequest{ + request := []PlaceOrderRequest{ ord, } pairs := currency.Pairs{ - currency.NewPair(currency.LTC, currency.USDT), - currency.NewPair(currency.ETH, currency.USDT), - currency.NewPair(currency.BCH, currency.USDT), - currency.NewPair(currency.XMR, currency.USDT), + currency.NewPair(currency.LTC, currency.USD), + currency.NewPair(currency.ETH, currency.USD), + currency.NewPair(currency.BCH, currency.USD), + currency.NewPair(currency.XMR, currency.USD), } for x := range pairs { @@ -380,42 +428,58 @@ func TestPlaceMultipleSpotOrdersOverPairLimits(t *testing.T) { } } -// TestCancelSpotOrder API endpoint test func TestCancelSpotOrder(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.CancelSpotOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &CancelSpotOrderRequest{ + InstrumentID: spotCurrencyLowerStr, OrderID: 1234, } _, err := o.CancelSpotOrder(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestCancelMultipleSpotOrders API endpoint test func TestCancelMultipleSpotOrders(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.CancelMultipleSpotOrdersRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &CancelMultipleSpotOrdersRequest{ + InstrumentID: spotCurrencyLowerStr, OrderIDs: []int64{1, 2, 3, 4}, } cancellations, err := o.CancelMultipleSpotOrders(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } for _, cancellationsPerCurrency := range cancellations { for _, cancellation := range cancellationsPerCurrency { - if !cancellation.Result { + if cancellation.Error != nil { t.Error(cancellation.Error) } } } } -// TestCancelMultipleSpotOrdersOverCurrencyLimits API logic test func TestCancelMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.CancelMultipleSpotOrdersRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &CancelMultipleSpotOrdersRequest{ + InstrumentID: spotCurrencyLowerStr, OrderIDs: []int64{1, 2, 3, 4, 5}, } @@ -425,72 +489,92 @@ func TestCancelMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { } } -// TestGetSpotOrders API endpoint test func TestGetSpotOrders(t *testing.T) { - request := okgroup.GetSpotOrdersRequest{ - InstrumentID: spotCurrency, + t.Parallel() + request := &GetSpotOrdersRequest{ + InstrumentID: spotCurrencyLowerStr, Status: "all", } _, err := o.GetSpotOrders(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotOpenOrders API endpoint test func TestGetSpotOpenOrders(t *testing.T) { - request := okgroup.GetSpotOpenOrdersRequest{} + t.Parallel() + request := &GetSpotOpenOrdersRequest{} _, err := o.GetSpotOpenOrders(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotOrder API endpoint test func TestGetSpotOrder(t *testing.T) { - request := okgroup.GetSpotOrderRequest{ - OrderID: "-1234", + t.Parallel() + request := &GetSpotOrderRequest{ + OrderID: "1234", InstrumentID: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Upper().String(), } _, err := o.GetSpotOrder(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotTransactionDetails API endpoint test func TestGetSpotTransactionDetails(t *testing.T) { - request := okgroup.GetSpotTransactionDetailsRequest{ + t.Parallel() + request := &GetSpotTransactionDetailsRequest{ OrderID: 1234, - InstrumentID: spotCurrency, + InstrumentID: spotCurrencyLowerStr, } _, err := o.GetSpotTransactionDetails(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetSpotTokenPairDetails API endpoint test func TestGetSpotTokenPairDetails(t *testing.T) { + t.Parallel() _, err := o.GetSpotTokenPairDetails(context.Background()) if err != nil { t.Error(err) } } -// TestGetSpotAllTokenPairsInformation API endpoint test func TestGetSpotAllTokenPairsInformation(t *testing.T) { + t.Parallel() _, err := o.GetSpotAllTokenPairsInformation(context.Background()) if err != nil { t.Error(err) } } -// TestGetSpotAllTokenPairsInformationForCurrency API endpoint test func TestGetSpotAllTokenPairsInformationForCurrency(t *testing.T) { + t.Parallel() _, err := o.GetSpotAllTokenPairsInformationForCurrency(context.Background(), - spotCurrency) + spotCurrencyLowerStr) if err != nil { t.Error(err) } } -// TestGetSpotFilledOrdersInformation API endpoint test func TestGetSpotFilledOrdersInformation(t *testing.T) { - request := okgroup.GetSpotFilledOrdersInformationRequest{ - InstrumentID: spotCurrency, + t.Parallel() + request := &GetSpotFilledOrdersInformationRequest{ + InstrumentID: spotCurrencyLowerStr, } _, err := o.GetSpotFilledOrdersInformation(context.Background(), request) if err != nil { @@ -498,85 +582,133 @@ func TestGetSpotFilledOrdersInformation(t *testing.T) { } } -// TestGetSpotMarketData API endpoint test func TestGetSpotMarketData(t *testing.T) { - request := &okgroup.GetMarketDataRequest{ + t.Parallel() + _, err := o.GetMarketData(context.Background(), &GetMarketDataRequest{ Asset: asset.Spot, - InstrumentID: spotCurrency, + InstrumentID: spotCurrencyLowerStr, Granularity: "604800", - } - _, err := o.GetMarketData(context.Background(), request) + }) if err != nil { t.Error(err) } + _, err = o.GetMarketData(context.Background(), &GetMarketDataRequest{ + Asset: asset.Binary, + InstrumentID: spotCurrencyLowerStr, + Granularity: "604800", + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } } -// TestGetMarginTradingAccounts API endpoint test func TestGetMarginTradingAccounts(t *testing.T) { + t.Parallel() _, err := o.GetMarginTradingAccounts(context.Background()) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginTradingAccountsForCurrency API endpoint test func TestGetMarginTradingAccountsForCurrency(t *testing.T) { - _, err := o.GetMarginTradingAccountsForCurrency(context.Background(), spotCurrency) - testStandardErrorHandling(t, err) + t.Parallel() + _, err := o.GetMarginTradingAccountsForCurrency(context.Background(), spotCurrencyLowerStr) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginBillDetails API endpoint test func TestGetMarginBillDetails(t *testing.T) { - request := okgroup.GetMarginBillDetailsRequest{ - InstrumentID: spotCurrency, + t.Parallel() + request := &GetMarginBillDetailsRequest{ + InstrumentID: spotCurrencyLowerStr, Limit: 100, } _, err := o.GetMarginBillDetails(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginAccountSettings API endpoint test func TestGetMarginAccountSettings(t *testing.T) { + t.Parallel() _, err := o.GetMarginAccountSettings(context.Background(), "") - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginAccountSettingsForCurrency API endpoint test func TestGetMarginAccountSettingsForCurrency(t *testing.T) { - _, err := o.GetMarginAccountSettings(context.Background(), spotCurrency) - testStandardErrorHandling(t, err) + t.Parallel() + _, err := o.GetMarginAccountSettings(context.Background(), "") + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestOpenMarginLoan API endpoint test func TestOpenMarginLoan(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.OpenMarginLoanRequest{ + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &OpenMarginLoanRequest{ Amount: -100, - InstrumentID: spotCurrency, + InstrumentID: spotCurrencyLowerStr, QuoteCurrency: currency.USD.String(), } _, err := o.OpenMarginLoan(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestRepayMarginLoan API endpoint test func TestRepayMarginLoan(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.RepayMarginLoanRequest{ + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &RepayMarginLoanRequest{ Amount: -100, - InstrumentID: spotCurrency, + InstrumentID: spotCurrencyLowerStr, QuoteCurrency: currency.USD.String(), BorrowID: 1, } _, err := o.RepayMarginLoan(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestPlaceMarginOrderLimit API endpoint test func TestPlaceMarginOrderLimit(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), MarginTrading: "2", @@ -584,15 +716,22 @@ func TestPlaceMarginOrderLimit(t *testing.T) { Size: "100", } - _, err := o.PlaceMarginOrder(context.Background(), &request) - testStandardErrorHandling(t, err) + _, err := o.PlaceMarginOrder(context.Background(), request) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestPlaceMarginOrderMarket API endpoint test func TestPlaceMarginOrderMarket(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Market.Lower(), Side: order.Buy.Lower(), MarginTrading: "2", @@ -600,36 +739,43 @@ func TestPlaceMarginOrderMarket(t *testing.T) { Notional: "100", } - _, err := o.PlaceMarginOrder(context.Background(), &request) - testStandardErrorHandling(t, err) + _, err := o.PlaceMarginOrder(context.Background(), request) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestPlaceMultipleMarginOrders API endpoint test func TestPlaceMultipleMarginOrders(t *testing.T) { - TestSetRealOrderDefaults(t) - ord := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + ord := PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), - MarginTrading: "1", + MarginTrading: "2", Size: "-100", Notional: "100", } - request := []okgroup.PlaceOrderRequest{ + request := []PlaceOrderRequest{ ord, } _, errs := o.PlaceMultipleMarginOrders(context.Background(), request) if len(errs) > 0 { - testStandardErrorHandling(t, errs[0]) + t.Error(errs) } } -// TestPlaceMultipleMarginOrdersOverCurrencyLimits API logic test func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { - ord := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + ord := PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), MarginTrading: "1", @@ -637,7 +783,7 @@ func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { Notional: "100", } - request := []okgroup.PlaceOrderRequest{ + request := []PlaceOrderRequest{ ord, ord, ord, @@ -651,26 +797,26 @@ func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { } } -// TestPlaceMultipleMarginOrdersOverPairLimits API logic test func TestPlaceMultipleMarginOrdersOverPairLimits(t *testing.T) { - ord := okgroup.PlaceOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + ord := PlaceOrderRequest{ + InstrumentID: spotCurrencyLowerStr, Type: order.Limit.Lower(), Side: order.Buy.Lower(), - MarginTrading: "1", + MarginTrading: "2", Size: "-100", Notional: "100", } - request := []okgroup.PlaceOrderRequest{ + request := []PlaceOrderRequest{ ord, } pairs := currency.Pairs{ - currency.NewPair(currency.LTC, currency.USDT), - currency.NewPair(currency.ETH, currency.USDT), - currency.NewPair(currency.BCH, currency.USDT), - currency.NewPair(currency.XMR, currency.USDT), + currency.NewPair(currency.LTC, currency.USD), + currency.NewPair(currency.ETH, currency.USD), + currency.NewPair(currency.BCH, currency.USD), + currency.NewPair(currency.XMR, currency.USD), } for x := range pairs { @@ -684,37 +830,48 @@ func TestPlaceMultipleMarginOrdersOverPairLimits(t *testing.T) { } } -// TestCancelMarginOrder API endpoint test func TestCancelMarginOrder(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.CancelSpotOrderRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &CancelSpotOrderRequest{ + InstrumentID: spotCurrencyLowerStr, OrderID: 1234, } _, err := o.CancelMarginOrder(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestCancelMultipleMarginOrders API endpoint test func TestCancelMultipleMarginOrders(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.CancelMultipleSpotOrdersRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &CancelMultipleSpotOrdersRequest{ + InstrumentID: spotCurrencyLowerStr, OrderIDs: []int64{1, 2, 3, 4}, } _, errs := o.CancelMultipleMarginOrders(context.Background(), request) if len(errs) > 0 { - testStandardErrorHandling(t, errs[0]) + t.Error(errs) } } -// TestCancelMultipleMarginOrdersOverCurrencyLimits API logic test func TestCancelMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { - TestSetRealOrderDefaults(t) - request := okgroup.CancelMultipleSpotOrdersRequest{ - InstrumentID: spotCurrency, + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } + request := &CancelMultipleSpotOrdersRequest{ + InstrumentID: spotCurrencyLowerStr, OrderIDs: []int64{1, 2, 3, 4, 5}, } @@ -724,96 +881,77 @@ func TestCancelMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { } } -// TestGetMarginOrders API endpoint test func TestGetMarginOrders(t *testing.T) { - request := okgroup.GetSpotOrdersRequest{ - InstrumentID: spotCurrency, + t.Parallel() + request := &GetSpotOrdersRequest{ + InstrumentID: spotCurrencyLowerStr, Status: "all", } _, err := o.GetMarginOrders(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginOpenOrders API endpoint test func TestGetMarginOpenOrders(t *testing.T) { - request := okgroup.GetSpotOpenOrdersRequest{} + t.Parallel() + request := &GetSpotOpenOrdersRequest{} _, err := o.GetMarginOpenOrders(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginOrder API endpoint test func TestGetMarginOrder(t *testing.T) { - request := okgroup.GetSpotOrderRequest{ + t.Parallel() + request := &GetSpotOrderRequest{ OrderID: "1234", InstrumentID: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Upper().String(), } _, err := o.GetMarginOrder(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestGetMarginTransactionDetails API endpoint test func TestGetMarginTransactionDetails(t *testing.T) { - request := okgroup.GetSpotTransactionDetailsRequest{ + t.Parallel() + request := &GetSpotTransactionDetailsRequest{ OrderID: 1234, - InstrumentID: spotCurrency, + InstrumentID: spotCurrencyLowerStr, } _, err := o.GetMarginTransactionDetails(context.Background(), request) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } // Websocket tests ---------------------------------------------------------------------------------------------- -// TestSendWsMessages Logic test -// Attempts to subscribe to a channel that doesn't exist -// Will log in if credentials are present -func TestSendWsMessages(t *testing.T) { - if !o.Websocket.IsEnabled() && !o.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { - t.Skip(stream.WebsocketNotEnabled) - } - var ok bool - var dialer websocket.Dialer - err := o.Websocket.Conn.Dial(&dialer, http.Header{}) - if err != nil { - t.Fatal(err) - } - go o.WsReadData() - subscriptions := []stream.ChannelSubscription{ - { - Channel: "badChannel", - }, - } - err = o.Subscribe(subscriptions) - if err != nil { - t.Fatal(err) - } - response := <-o.Websocket.DataHandler - if err, ok = response.(error); ok && err != nil { - if !strings.Contains(err.Error(), subscriptions[0].Channel) { - t.Error("Expecting OKCoin error - 30040 message: Channel badChannel doesn't exist") - } - } - err = o.WsLogin(context.Background()) - if err != nil { - t.Error(err) - } - responseTwo := <-o.Websocket.DataHandler - if err, ok := responseTwo.(error); ok && err != nil { - t.Error(err) - } -} - -// TestGetAssetTypeFromTableName logic test func TestGetAssetTypeFromTableName(t *testing.T) { - str := "spot/candle300s:BTC-USDT" + t.Parallel() + str := "spot/candle300s:BTC-USD" spot := o.GetAssetTypeFromTableName(str) if !strings.EqualFold(spot.String(), asset.Spot.String()) { t.Errorf("Error, expected 'SPOT', received: '%v'", spot) } } -// TestGetWsChannelWithoutOrderType logic test func TestGetWsChannelWithoutOrderType(t *testing.T) { - str := "spot/depth5:BTC-USDT" + t.Parallel() + str := "spot/depth5:BTC-USD" expected := "depth5" resp := o.GetWsChannelWithoutOrderType(str) if resp != expected { @@ -832,13 +970,10 @@ func TestGetWsChannelWithoutOrderType(t *testing.T) { } } -// TestOrderBookUpdateChecksumCalculator logic test func TestOrderBookUpdateChecksumCalculator(t *testing.T) { - if !websocketEnabled { - t.Skip("Websocket not enabled, skipping") - } - original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` - update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` + t.Parallel() + original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USD","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` + update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USD","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` err := o.WsProcessOrderBook([]byte(original)) if err != nil { t.Fatal(err) @@ -849,11 +984,8 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { } } -// TestOrderBookUpdateChecksumCalculatorWithDash logic test func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { - if !websocketEnabled { - t.Skip("Websocket not enabled, skipping") - } + t.Parallel() original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` err := o.WsProcessOrderBook([]byte(original)) @@ -866,10 +998,10 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { } } -// TestOrderBookPartialChecksumCalculator logic test func TestOrderBookPartialChecksumCalculator(t *testing.T) { - orderbookPartialJSON := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"EOS-USDT","asks":[["3.5196","0.1077",1],["3.5198","21.71",1],["3.5199","51.1805",1],["3.5208","75.09",1],["3.521","196.3333",1],["3.5213","0.1",1],["3.5218","39.276",2],["3.5219","395.6334",1],["3.522","27.956",1],["3.5222","404.9595",1],["3.5225","300",1],["3.5227","143.5442",2],["3.523","42.4746",1],["3.5231","852.64",2],["3.5235","34.9602",1],["3.5237","442.0918",2],["3.5238","352.8404",2],["3.5239","341.6759",2],["3.524","84.9493",1],["3.5241","148.4882",1],["3.5242","261.64",1],["3.5243","142.045",1],["3.5246","10",1],["3.5247","284.0788",1],["3.5248","720",1],["3.5249","89.2518",2],["3.5251","1201.8965",2],["3.5254","426.2938",1],["3.5255","213.0863",1],["3.5257","568.1576",1],["3.5258","0.3",1],["3.5259","34.4602",1],["3.526","0.1",1],["3.5263","850.771",1],["3.5265","5.9",1],["3.5268","10.5064",2],["3.5272","1136.8965",1],["3.5274","255.1481",1],["3.5276","29.5374",1],["3.5278","50",1],["3.5282","284.1797",1],["3.5283","1136.8965",1],["3.5284","0.4275",1],["3.5285","100",1],["3.5292","90.9",1],["3.5298","0.2",1],["3.5303","568.1576",1],["3.5305","279.9999",1],["3.532","0.409",1],["3.5321","568.1576",1],["3.5326","6016.8756",1],["3.5328","4.9849",1],["3.533","92.88",2],["3.5343","1200.2383",2],["3.5344","100",1],["3.535","359.7047",1],["3.5354","100",1],["3.5355","100",1],["3.5356","10",1],["3.5358","200",2],["3.5362","435.139",1],["3.5365","2152",1],["3.5366","284.1756",1],["3.5367","568.4644",1],["3.5369","33.9878",1],["3.537","337.1191",2],["3.5373","0.4045",1],["3.5383","1136.7188",1],["3.5386","12.1614",1],["3.5387","90.89",1],["3.54","4.54",1],["3.5423","90.8",1],["3.5436","0.1",1],["3.5454","853.4156",1],["3.5468","142.0656",1],["3.5491","0.0008",1],["3.55","14478.8206",6],["3.5537","21521",1],["3.5555","11.53",1],["3.5573","50.6001",1],["3.5599","4591.4221",1],["3.56","1227.0002",4],["3.5603","2670",1],["3.5608","58.6638",1],["3.5613","0.1",1],["3.5621","45.9473",1],["3.57","2141.7274",3],["3.5712","2956.9816",1],["3.5717","27.9978",1],["3.5718","0.9285",1],["3.5739","299.73",1],["3.5761","864",1],["3.579","22.5225",1],["3.5791","38.26",2],["3.58","7618.4634",5],["3.5801","457.2184",1],["3.582","24.5",1],["3.5822","1572.6425",1],["3.5845","14.1438",1],["3.585","527.169",1],["3.5865","20",1],["3.5867","4490",1],["3.5876","39.0493",1],["3.5879","392.9083",1],["3.5888","436.42",2],["3.5896","50",1],["3.59","2608.9128",8],["3.5913","19.5246",1],["3.5938","7082",1],["3.597","0.1",1],["3.5979","399",1],["3.5995","315.1509",1],["3.5999","2566.2648",1],["3.6","18511.2292",35],["3.603","22.3379",2],["3.605","499.5",1],["3.6055","100",1],["3.6058","499.5",1],["3.608","1021.1485",1],["3.61","11755.4596",13],["3.611","42.8571",1],["3.6131","6690",1],["3.6157","19.5247",1],["3.618","2500",1],["3.6197","525.7146",1],["3.6198","0.4455",1],["3.62","6440.6295",8],["3.6219","0.4175",1],["3.6237","168",1],["3.6265","0.1001",1],["3.628","64.9345",1],["3.63","4435.4985",6],["3.6308","1.7815",1],["3.6331","0.1",1],["3.6338","355.527",2],["3.6358","50",1],["3.6363","2074.7096",1],["3.6376","4000",1],["3.6396","11090",1],["3.6399","0.4055",1],["3.64","4161.9805",4],["3.6437","117.6524",1],["3.648","190",1],["3.6488","200",1],["3.65","11740.5045",25],["3.6512","0.1",1],["3.6521","728",1],["3.6555","100",1],["3.6598","36.6914",1],["3.66","4331.2148",6],["3.6638","200",1],["3.6673","100",1],["3.6679","38",1],["3.6688","2",1],["3.6695","0.1",1],["3.67","7984.698",6],["3.672","300",1],["3.6777","257.8247",1],["3.6789","393.4217",2],["3.68","9202.3222",11],["3.6818","500",1],["3.6823","299.7",1],["3.6839","422.3748",1],["3.685","100",1],["3.6878","0.1",1],["3.6888","72.0958",2],["3.6889","2876",1],["3.689","28",1],["3.6891","28",1],["3.6892","28",1],["3.6895","28",1],["3.6898","28",1],["3.69","643.96",7],["3.6908","118",2],["3.691","28",1],["3.6916","28",1],["3.6918","28",1],["3.6926","28",1],["3.6928","28",1],["3.6932","28",1],["3.6933","200",1],["3.6935","28",1],["3.6936","28",1],["3.6938","28",1],["3.694","28",1],["3.698","1498.5",1],["3.6988","2014.2004",2],["3.7","21904.2689",22],["3.7029","71.95",1],["3.704","3690.1362",1],["3.7055","100",1],["3.7063","0.1",1],["3.71","4421.3468",4],["3.719","17.3491",1],["3.72","1304.5995",3],["3.7211","10",1],["3.7248","0.1",1],["3.725","1900",1],["3.73","31.1785",2],["3.7375","38",1]],"bids":[["3.5182","151.5343",6],["3.5181","0.3691",1],["3.518","271.3967",2],["3.5179","257.8352",1],["3.5178","12.3811",1],["3.5173","34.1921",2],["3.5171","1013.8256",2],["3.517","272.1119",2],["3.5168","395.3376",1],["3.5166","317.1756",2],["3.5165","348.302",3],["3.5164","142.0414",1],["3.5163","96.8933",2],["3.516","600.1034",3],["3.5159","27.481",1],["3.5158","27.33",1],["3.5157","583.1898",2],["3.5156","24.6819",2],["3.5154","25",1],["3.5153","0.429",1],["3.5152","453.9204",3],["3.5151","2131.592",4],["3.515","335",3],["3.5149","37.1586",1],["3.5147","41.6759",1],["3.5146","54.569",1],["3.5145","70.3515",1],["3.5143","68.206",3],["3.5142","359.4538",2],["3.5139","45.4123",2],["3.5137","71.673",2],["3.5136","25",1],["3.5135","300",1],["3.5134","442.57",2],["3.5132","83.3518",1],["3.513","1245.2529",3],["3.5127","20",1],["3.512","284.1353",1],["3.5119","1136.8319",1],["3.5113","56.9351",1],["3.5111","588.1898",2],["3.5109","255.0946",1],["3.5105","48.65",1],["3.5103","50.2",1],["3.5098","720",1],["3.5096","148.95",1],["3.5094","570.5758",2],["3.509","2.386",1],["3.5089","0.4065",1],["3.5087","282.3859",2],["3.5086","145.036",2],["3.5084","2.386",1],["3.5082","90.98",1],["3.5081","2.386",1],["3.5079","2.386",1],["3.5078","857.6229",2],["3.5075","2.386",1],["3.5074","284.1877",1],["3.5073","100",1],["3.5071","100",1],["3.507","768.4159",3],["3.5069","313.0863",2],["3.5068","426.2938",1],["3.5066","568.3594",1],["3.5063","1136.6865",1],["3.5059","0.3",1],["3.5054","9.9999",1],["3.5053","0.2",1],["3.5051","392.428",1],["3.505","13.79",1],["3.5048","99.5497",2],["3.5047","78.5331",2],["3.5046","2153",1],["3.5041","5983.999",1],["3.5037","668.5682",1],["3.5036","160.5948",1],["3.5024","534.8075",1],["3.5014","28.5604",1],["3.5011","91",1],["3.5","1058.8771",2],["3.4997","50.2",1],["3.4985","3430.0414",1],["3.4949","232.0591",1],["3.4942","21521",1],["3.493","2",1],["3.4928","2",1],["3.4925","0.44",1],["3.4917","142.0656",1],["3.49","2051.8826",4],["3.488","280.7459",1],["3.4852","643.4038",1],["3.4851","86.0807",1],["3.485","213.2436",1],["3.484","0.1",1],["3.4811","144.3399",1],["3.4808","89",1],["3.4803","12.1999",1],["3.4801","2390",1],["3.48","930.8453",9],["3.4791","310",1],["3.4768","206",1],["3.4767","0.9415",1],["3.4754","1.4387",1],["3.4728","20",1],["3.4701","1219.2873",1],["3.47","1904.3139",7],["3.468","0.4035",1],["3.4667","0.1",1],["3.4666","3020.0101",1],["3.465","10",1],["3.464","0.4485",1],["3.462","2119.6556",1],["3.46","1305.6113",8],["3.4589","8.0228",1],["3.457","100",1],["3.456","70.3859",2],["3.4538","20",1],["3.4536","4323.9486",2],["3.4531","827.0427",1],["3.4528","0.439",1],["3.4522","8.0381",1],["3.4513","441.1873",1],["3.4512","50.707",1],["3.451","87.0902",1],["3.4509","200",1],["3.4506","100",1],["3.4505","86.4045",2],["3.45","12409.4595",28],["3.4494","0.5365",2],["3.449","10761",1],["3.4482","8.0476",1],["3.4469","0.449",1],["3.445","2000",1],["3.4427","14",1],["3.4421","100",1],["3.4416","8.0631",1],["3.4404","1",1],["3.44","4580.733",11],["3.4388","1868.2085",1],["3.438","937.7246",2],["3.4367","1500",1],["3.4366","62",1],["3.436","29.8743",1],["3.4356","25.4801",1],["3.4349","4.3086",1],["3.4343","43.2402",1],["3.433","2.0688",1],["3.4322","2.7335",2],["3.432","93.3233",1],["3.4302","328.8301",2],["3.43","4440.8158",11],["3.4288","754.574",2],["3.4283","125.7043",2],["3.428","744.3154",2],["3.4273","5460",1],["3.4258","50",1],["3.4255","109.005",1],["3.4248","100",1],["3.4241","129.2048",2],["3.4233","5.3598",1],["3.4228","4498.866",1],["3.4222","3.5435",1],["3.4217","404.3252",2],["3.4211","1000",1],["3.4208","31",1],["3.42","1834.024",9],["3.4175","300",1],["3.4162","400",1],["3.4152","0.1",1],["3.4151","4.3336",1],["3.415","1.5974",1],["3.414","1146",1],["3.4134","306.4246",1],["3.4129","7.5556",1],["3.4111","198.5188",1],["3.4109","500",1],["3.4106","4305",1],["3.41","2150.7635",13],["3.4085","4.342",1],["3.4054","5.6985",1],["3.4019","5.438",1],["3.4015","1010.846",1],["3.4009","8610",1],["3.4005","1.9122",1],["3.4004","1",1],["3.4","27081.1806",67],["3.3955","3.2682",1],["3.3953","5.4486",1],["3.3937","1591.3805",1],["3.39","3221.4155",8],["3.3899","3.2736",1],["3.3888","1500",2],["3.3887","5.4592",1],["3.385","117.0969",2],["3.3821","5.4699",1],["3.382","100.0529",1],["3.3818","172.0164",1],["3.3815","165.6288",1],["3.381","887.3115",1],["3.3808","100",1]],"timestamp":"2019-03-04T00:15:04.155Z","checksum":-2036653089}]}` - var dataResponse okgroup.WebsocketOrderBook + t.Parallel() + orderbookPartialJSON := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"EOS-USD","asks":[["3.5196","0.1077",1],["3.5198","21.71",1],["3.5199","51.1805",1],["3.5208","75.09",1],["3.521","196.3333",1],["3.5213","0.1",1],["3.5218","39.276",2],["3.5219","395.6334",1],["3.522","27.956",1],["3.5222","404.9595",1],["3.5225","300",1],["3.5227","143.5442",2],["3.523","42.4746",1],["3.5231","852.64",2],["3.5235","34.9602",1],["3.5237","442.0918",2],["3.5238","352.8404",2],["3.5239","341.6759",2],["3.524","84.9493",1],["3.5241","148.4882",1],["3.5242","261.64",1],["3.5243","142.045",1],["3.5246","10",1],["3.5247","284.0788",1],["3.5248","720",1],["3.5249","89.2518",2],["3.5251","1201.8965",2],["3.5254","426.2938",1],["3.5255","213.0863",1],["3.5257","568.1576",1],["3.5258","0.3",1],["3.5259","34.4602",1],["3.526","0.1",1],["3.5263","850.771",1],["3.5265","5.9",1],["3.5268","10.5064",2],["3.5272","1136.8965",1],["3.5274","255.1481",1],["3.5276","29.5374",1],["3.5278","50",1],["3.5282","284.1797",1],["3.5283","1136.8965",1],["3.5284","0.4275",1],["3.5285","100",1],["3.5292","90.9",1],["3.5298","0.2",1],["3.5303","568.1576",1],["3.5305","279.9999",1],["3.532","0.409",1],["3.5321","568.1576",1],["3.5326","6016.8756",1],["3.5328","4.9849",1],["3.533","92.88",2],["3.5343","1200.2383",2],["3.5344","100",1],["3.535","359.7047",1],["3.5354","100",1],["3.5355","100",1],["3.5356","10",1],["3.5358","200",2],["3.5362","435.139",1],["3.5365","2152",1],["3.5366","284.1756",1],["3.5367","568.4644",1],["3.5369","33.9878",1],["3.537","337.1191",2],["3.5373","0.4045",1],["3.5383","1136.7188",1],["3.5386","12.1614",1],["3.5387","90.89",1],["3.54","4.54",1],["3.5423","90.8",1],["3.5436","0.1",1],["3.5454","853.4156",1],["3.5468","142.0656",1],["3.5491","0.0008",1],["3.55","14478.8206",6],["3.5537","21521",1],["3.5555","11.53",1],["3.5573","50.6001",1],["3.5599","4591.4221",1],["3.56","1227.0002",4],["3.5603","2670",1],["3.5608","58.6638",1],["3.5613","0.1",1],["3.5621","45.9473",1],["3.57","2141.7274",3],["3.5712","2956.9816",1],["3.5717","27.9978",1],["3.5718","0.9285",1],["3.5739","299.73",1],["3.5761","864",1],["3.579","22.5225",1],["3.5791","38.26",2],["3.58","7618.4634",5],["3.5801","457.2184",1],["3.582","24.5",1],["3.5822","1572.6425",1],["3.5845","14.1438",1],["3.585","527.169",1],["3.5865","20",1],["3.5867","4490",1],["3.5876","39.0493",1],["3.5879","392.9083",1],["3.5888","436.42",2],["3.5896","50",1],["3.59","2608.9128",8],["3.5913","19.5246",1],["3.5938","7082",1],["3.597","0.1",1],["3.5979","399",1],["3.5995","315.1509",1],["3.5999","2566.2648",1],["3.6","18511.2292",35],["3.603","22.3379",2],["3.605","499.5",1],["3.6055","100",1],["3.6058","499.5",1],["3.608","1021.1485",1],["3.61","11755.4596",13],["3.611","42.8571",1],["3.6131","6690",1],["3.6157","19.5247",1],["3.618","2500",1],["3.6197","525.7146",1],["3.6198","0.4455",1],["3.62","6440.6295",8],["3.6219","0.4175",1],["3.6237","168",1],["3.6265","0.1001",1],["3.628","64.9345",1],["3.63","4435.4985",6],["3.6308","1.7815",1],["3.6331","0.1",1],["3.6338","355.527",2],["3.6358","50",1],["3.6363","2074.7096",1],["3.6376","4000",1],["3.6396","11090",1],["3.6399","0.4055",1],["3.64","4161.9805",4],["3.6437","117.6524",1],["3.648","190",1],["3.6488","200",1],["3.65","11740.5045",25],["3.6512","0.1",1],["3.6521","728",1],["3.6555","100",1],["3.6598","36.6914",1],["3.66","4331.2148",6],["3.6638","200",1],["3.6673","100",1],["3.6679","38",1],["3.6688","2",1],["3.6695","0.1",1],["3.67","7984.698",6],["3.672","300",1],["3.6777","257.8247",1],["3.6789","393.4217",2],["3.68","9202.3222",11],["3.6818","500",1],["3.6823","299.7",1],["3.6839","422.3748",1],["3.685","100",1],["3.6878","0.1",1],["3.6888","72.0958",2],["3.6889","2876",1],["3.689","28",1],["3.6891","28",1],["3.6892","28",1],["3.6895","28",1],["3.6898","28",1],["3.69","643.96",7],["3.6908","118",2],["3.691","28",1],["3.6916","28",1],["3.6918","28",1],["3.6926","28",1],["3.6928","28",1],["3.6932","28",1],["3.6933","200",1],["3.6935","28",1],["3.6936","28",1],["3.6938","28",1],["3.694","28",1],["3.698","1498.5",1],["3.6988","2014.2004",2],["3.7","21904.2689",22],["3.7029","71.95",1],["3.704","3690.1362",1],["3.7055","100",1],["3.7063","0.1",1],["3.71","4421.3468",4],["3.719","17.3491",1],["3.72","1304.5995",3],["3.7211","10",1],["3.7248","0.1",1],["3.725","1900",1],["3.73","31.1785",2],["3.7375","38",1]],"bids":[["3.5182","151.5343",6],["3.5181","0.3691",1],["3.518","271.3967",2],["3.5179","257.8352",1],["3.5178","12.3811",1],["3.5173","34.1921",2],["3.5171","1013.8256",2],["3.517","272.1119",2],["3.5168","395.3376",1],["3.5166","317.1756",2],["3.5165","348.302",3],["3.5164","142.0414",1],["3.5163","96.8933",2],["3.516","600.1034",3],["3.5159","27.481",1],["3.5158","27.33",1],["3.5157","583.1898",2],["3.5156","24.6819",2],["3.5154","25",1],["3.5153","0.429",1],["3.5152","453.9204",3],["3.5151","2131.592",4],["3.515","335",3],["3.5149","37.1586",1],["3.5147","41.6759",1],["3.5146","54.569",1],["3.5145","70.3515",1],["3.5143","68.206",3],["3.5142","359.4538",2],["3.5139","45.4123",2],["3.5137","71.673",2],["3.5136","25",1],["3.5135","300",1],["3.5134","442.57",2],["3.5132","83.3518",1],["3.513","1245.2529",3],["3.5127","20",1],["3.512","284.1353",1],["3.5119","1136.8319",1],["3.5113","56.9351",1],["3.5111","588.1898",2],["3.5109","255.0946",1],["3.5105","48.65",1],["3.5103","50.2",1],["3.5098","720",1],["3.5096","148.95",1],["3.5094","570.5758",2],["3.509","2.386",1],["3.5089","0.4065",1],["3.5087","282.3859",2],["3.5086","145.036",2],["3.5084","2.386",1],["3.5082","90.98",1],["3.5081","2.386",1],["3.5079","2.386",1],["3.5078","857.6229",2],["3.5075","2.386",1],["3.5074","284.1877",1],["3.5073","100",1],["3.5071","100",1],["3.507","768.4159",3],["3.5069","313.0863",2],["3.5068","426.2938",1],["3.5066","568.3594",1],["3.5063","1136.6865",1],["3.5059","0.3",1],["3.5054","9.9999",1],["3.5053","0.2",1],["3.5051","392.428",1],["3.505","13.79",1],["3.5048","99.5497",2],["3.5047","78.5331",2],["3.5046","2153",1],["3.5041","5983.999",1],["3.5037","668.5682",1],["3.5036","160.5948",1],["3.5024","534.8075",1],["3.5014","28.5604",1],["3.5011","91",1],["3.5","1058.8771",2],["3.4997","50.2",1],["3.4985","3430.0414",1],["3.4949","232.0591",1],["3.4942","21521",1],["3.493","2",1],["3.4928","2",1],["3.4925","0.44",1],["3.4917","142.0656",1],["3.49","2051.8826",4],["3.488","280.7459",1],["3.4852","643.4038",1],["3.4851","86.0807",1],["3.485","213.2436",1],["3.484","0.1",1],["3.4811","144.3399",1],["3.4808","89",1],["3.4803","12.1999",1],["3.4801","2390",1],["3.48","930.8453",9],["3.4791","310",1],["3.4768","206",1],["3.4767","0.9415",1],["3.4754","1.4387",1],["3.4728","20",1],["3.4701","1219.2873",1],["3.47","1904.3139",7],["3.468","0.4035",1],["3.4667","0.1",1],["3.4666","3020.0101",1],["3.465","10",1],["3.464","0.4485",1],["3.462","2119.6556",1],["3.46","1305.6113",8],["3.4589","8.0228",1],["3.457","100",1],["3.456","70.3859",2],["3.4538","20",1],["3.4536","4323.9486",2],["3.4531","827.0427",1],["3.4528","0.439",1],["3.4522","8.0381",1],["3.4513","441.1873",1],["3.4512","50.707",1],["3.451","87.0902",1],["3.4509","200",1],["3.4506","100",1],["3.4505","86.4045",2],["3.45","12409.4595",28],["3.4494","0.5365",2],["3.449","10761",1],["3.4482","8.0476",1],["3.4469","0.449",1],["3.445","2000",1],["3.4427","14",1],["3.4421","100",1],["3.4416","8.0631",1],["3.4404","1",1],["3.44","4580.733",11],["3.4388","1868.2085",1],["3.438","937.7246",2],["3.4367","1500",1],["3.4366","62",1],["3.436","29.8743",1],["3.4356","25.4801",1],["3.4349","4.3086",1],["3.4343","43.2402",1],["3.433","2.0688",1],["3.4322","2.7335",2],["3.432","93.3233",1],["3.4302","328.8301",2],["3.43","4440.8158",11],["3.4288","754.574",2],["3.4283","125.7043",2],["3.428","744.3154",2],["3.4273","5460",1],["3.4258","50",1],["3.4255","109.005",1],["3.4248","100",1],["3.4241","129.2048",2],["3.4233","5.3598",1],["3.4228","4498.866",1],["3.4222","3.5435",1],["3.4217","404.3252",2],["3.4211","1000",1],["3.4208","31",1],["3.42","1834.024",9],["3.4175","300",1],["3.4162","400",1],["3.4152","0.1",1],["3.4151","4.3336",1],["3.415","1.5974",1],["3.414","1146",1],["3.4134","306.4246",1],["3.4129","7.5556",1],["3.4111","198.5188",1],["3.4109","500",1],["3.4106","4305",1],["3.41","2150.7635",13],["3.4085","4.342",1],["3.4054","5.6985",1],["3.4019","5.438",1],["3.4015","1010.846",1],["3.4009","8610",1],["3.4005","1.9122",1],["3.4004","1",1],["3.4","27081.1806",67],["3.3955","3.2682",1],["3.3953","5.4486",1],["3.3937","1591.3805",1],["3.39","3221.4155",8],["3.3899","3.2736",1],["3.3888","1500",2],["3.3887","5.4592",1],["3.385","117.0969",2],["3.3821","5.4699",1],["3.382","100.0529",1],["3.3818","172.0164",1],["3.3815","165.6288",1],["3.381","887.3115",1],["3.3808","100",1]],"timestamp":"2019-03-04T00:15:04.155Z","checksum":-2036653089}]}` + var dataResponse WebsocketOrderBook err := json.Unmarshal([]byte(orderbookPartialJSON), &dataResponse) if err != nil { t.Error(err) @@ -884,7 +1016,6 @@ func TestOrderBookPartialChecksumCalculator(t *testing.T) { } } -// Function tests ---------------------------------------------------------------------------------------------- func setFeeBuilder() *exchange.FeeBuilder { return &exchange.FeeBuilder{ Amount: 1, @@ -899,8 +1030,8 @@ func setFeeBuilder() *exchange.FeeBuilder { } } -// TestGetFeeByTypeOfflineTradeFee logic test func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { + t.Parallel() var feeBuilder = setFeeBuilder() _, err := o.GetFeeByType(context.Background(), feeBuilder) if err != nil { @@ -918,6 +1049,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { + t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic if _, err := o.GetFee(context.Background(), feeBuilder); err != nil { @@ -963,8 +1095,8 @@ func TestGetFee(t *testing.T) { } } -// TestFormatWithdrawPermissions helper test func TestFormatWithdrawPermissions(t *testing.T) { + t.Parallel() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText withdrawPermissions := o.FormatWithdrawPermissions() if withdrawPermissions != expectedResult { @@ -974,9 +1106,11 @@ func TestFormatWithdrawPermissions(t *testing.T) { // Wrapper tests -------------------------------------------------------------------------------------------------- -// TestSubmitOrder Wrapper test func TestSubmitOrder(t *testing.T) { - TestSetRealOrderDefaults(t) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } var orderSubmission = &order.Submit{ Exchange: o.Name, Pair: currency.Pair{ @@ -998,48 +1132,67 @@ func TestSubmitOrder(t *testing.T) { } } -// TestCancelExchangeOrder Wrapper test func TestCancelExchangeOrder(t *testing.T) { - TestSetRealOrderDefaults(t) - currencyPair := currency.NewPair(currency.LTC, currency.BTC) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currencyPair, + Pair: spotCurrency, } err := o.CancelOrder(context.Background(), &orderCancellation) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestCancelAllExchangeOrders Wrapper test func TestCancelAllExchangeOrders(t *testing.T) { - TestSetRealOrderDefaults(t) - currencyPair := currency.NewPair(currency.LTC, currency.BTC) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currencyPair, + Pair: spotCurrency, } resp, err := o.CancelAllOrders(context.Background(), &orderCancellation) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } if len(resp.Status) > 0 { t.Errorf("%v orders failed to cancel", len(resp.Status)) } } -// TestGetAccountInfo Wrapper test func TestGetAccountInfo(t *testing.T) { + t.Parallel() _, err := o.UpdateAccountInfo(context.Background(), asset.Spot) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestModifyOrder Wrapper test func TestModifyOrder(t *testing.T) { - TestSetRealOrderDefaults(t) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } _, err := o.ModifyOrder(context.Background(), &order.Modify{AssetType: asset.Spot}) if err != common.ErrFunctionNotSupported { @@ -1047,9 +1200,11 @@ func TestModifyOrder(t *testing.T) { } } -// TestWithdraw Wrapper test func TestWithdraw(t *testing.T) { - TestSetRealOrderDefaults(t) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } withdrawCryptoRequest := withdraw.Request{ Exchange: o.Name, @@ -1065,12 +1220,19 @@ func TestWithdraw(t *testing.T) { _, err := o.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest) - testStandardErrorHandling(t, err) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } } -// TestWithdrawFiat Wrapper test func TestWithdrawFiat(t *testing.T) { - TestSetRealOrderDefaults(t) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } var withdrawFiatRequest = withdraw.Request{} _, err := o.WithdrawFiatFunds(context.Background(), &withdrawFiatRequest) if err != common.ErrFunctionNotSupported { @@ -1078,9 +1240,11 @@ func TestWithdrawFiat(t *testing.T) { } } -// TestSubmitOrder Wrapper test func TestWithdrawInternationalBank(t *testing.T) { - TestSetRealOrderDefaults(t) + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") + } var withdrawFiatRequest = withdraw.Request{} _, err := o.WithdrawFiatFundsToInternationalBank(context.Background(), &withdrawFiatRequest) @@ -1089,70 +1253,52 @@ func TestWithdrawInternationalBank(t *testing.T) { } } -// TestGetOrderbook logic test func TestGetOrderbook(t *testing.T) { t.Parallel() _, err := o.GetOrderBook(context.Background(), - &okgroup.GetOrderBookRequest{InstrumentID: "BTC-USDT"}, + &GetOrderBookRequest{InstrumentID: spotCurrencyUpperStr}, asset.Spot) if err != nil { t.Error(err) } _, err = o.GetOrderBook(context.Background(), - &okgroup.GetOrderBookRequest{InstrumentID: "Payload"}, + &GetOrderBookRequest{InstrumentID: "Payload"}, asset.Futures) if err == nil { t.Error("error cannot be nil") } - - _, err = o.GetOrderBook(context.Background(), - &okgroup.GetOrderBookRequest{InstrumentID: "BTC-USD-SWAP"}, - asset.PerpetualSwap) - if err == nil { - t.Error("error cannot be nil") - } } func TestGetHistoricCandles(t *testing.T) { - currencyPair, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Fatal(err) - } + t.Parallel() startTime := time.Unix(1588636800, 0) - _, err = o.GetHistoricCandles(context.Background(), - currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin) + _, err := o.GetHistoricCandles(context.Background(), + spotCurrency, asset.Spot, startTime, time.Now(), kline.OneMin) if err != nil { t.Fatal(err) } } func TestGetHistoricCandlesExtended(t *testing.T) { - currencyPair, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Fatal(err) - } + t.Parallel() startTime := time.Unix(1588636800, 0) - _, err = o.GetHistoricCandlesExtended(context.Background(), - currencyPair, asset.Spot, startTime, time.Now(), kline.OneWeek) + _, err := o.GetHistoricCandlesExtended(context.Background(), + spotCurrency, asset.Spot, startTime, time.Now(), kline.OneWeek) if err != nil { t.Fatal(err) } _, err = o.GetHistoricCandles(context.Background(), - currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7)) + spotCurrency, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7)) if err == nil { - t.Fatal("unexpected result") + t.Error("error cannot be nil") } } func TestGetRecentTrades(t *testing.T) { t.Parallel() - currencyPair, err := currency.NewPairFromString("BTC-USDT") - if err != nil { - t.Fatal(err) - } - _, err = o.GetRecentTrades(context.Background(), currencyPair, asset.Spot) + _, err := o.GetRecentTrades(context.Background(), spotCurrency, asset.Spot) if err != nil { t.Error(err) } @@ -1160,12 +1306,8 @@ func TestGetRecentTrades(t *testing.T) { func TestGetHistoricTrades(t *testing.T) { t.Parallel() - currencyPair, err := currency.NewPairFromString("BTC-USDT") - if err != nil { - t.Fatal(err) - } - _, err = o.GetHistoricTrades(context.Background(), - currencyPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) + _, err := o.GetHistoricTrades(context.Background(), + spotCurrency, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) if err != nil && err != common.ErrFunctionNotSupported { t.Error(err) } @@ -1173,11 +1315,7 @@ func TestGetHistoricTrades(t *testing.T) { func TestUpdateTicker(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Fatal(err) - } - _, err = o.UpdateTicker(context.Background(), cp, asset.Spot) + _, err := o.UpdateTicker(context.Background(), spotCurrency, asset.Spot) if err != nil { t.Error(err) } @@ -1190,3 +1328,20 @@ func TestUpdateTickers(t *testing.T) { t.Error(err) } } + +func TestGetMarginLoanHistory(t *testing.T) { + t.Parallel() + _, err := o.GetMarginLoanHistory(context.Background(), &GetMarginLoanHistoryRequest{ + InstrumentID: spotCurrencyUpperStr, + Status: 1, + From: 1, + To: 1, + Limit: 1, + }) + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") + } + if areTestAPIKeysSet() && err != nil { + t.Error(err) + } +} diff --git a/exchanges/okgroup/okgroup_types.go b/exchanges/okcoin/okcoin_types.go similarity index 81% rename from exchanges/okgroup/okgroup_types.go rename to exchanges/okcoin/okcoin_types.go index 5e1b102a..6a82bc0c 100644 --- a/exchanges/okgroup/okgroup_types.go +++ b/exchanges/okcoin/okcoin_types.go @@ -1,18 +1,17 @@ -package okgroup +package okcoin import ( + "errors" + "sync" "time" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// Order types -const ( - NormalOrder = iota - PostOnlyOrder - FillOrKillOrder - ImmediateOrCancelOrder +var ( + errNoAccountDepositAddress = errors.New("no account deposit address") + errIncorrectCandleDataLength = errors.New("incorrect candles data length") ) // PerpSwapInstrumentData stores instrument data for perpetual swap contracts @@ -106,9 +105,11 @@ type PerpSwapFundingRates struct { type GetAccountCurrenciesResponse struct { Name string `json:"name"` Currency string `json:"currency"` - CanDeposit int `json:"can_deposit,string"` - CanWithdraw int `json:"can_withdraw,string"` - MinWithdrawal float64 `json:"min_withdrawal,string"` + Chain string `json:"chain"` + CanInternal bool `json:"can_internal,string"` + CanWithdraw bool `json:"can_withdraw,string"` + CanDeposit bool `json:"can_deposit,string"` + MinWithdrawal float64 `json:"min_withdrawal"` } // WalletInformationResponse response data for WalletInformation @@ -435,7 +436,8 @@ type GetMarketDataRequest struct { // low string Lowest price // close string Close price // volume string Trading volume -type GetMarketDataResponse []interface{} +type GetMarketDataResponse struct { +} // GetMarginAccountsResponse response data for GetMarginAccounts type GetMarginAccountsResponse struct { @@ -1354,7 +1356,7 @@ type GetETTSettlementPriceHistoryResponse struct { Price float64 `json:"price"` } -// OrderStatus Holds OKGroup order status values +// OrderStatus Holds Okcoin order status values var OrderStatus = map[int64]string{ -3: "pending cancel", -2: "cancelled", @@ -1517,3 +1519,294 @@ type WebsocketErrorResponse struct { Message string `json:"message"` ErrorCode int64 `json:"errorCode"` } + +// List of all websocket channels to subscribe to +const ( + // Orderbook events + okcoinWsOrderbookUpdate = "update" + okcoinWsOrderbookPartial = "partial" + // API subsections + okcoinWsSwapSubsection = "swap/" + okcoinWsIndexSubsection = "index/" + okcoinWsFuturesSubsection = "futures/" + okcoinWsSpotSubsection = "spot/" + // Shared API endpoints + okcoinWsCandle = "candle" + okcoinWsCandle60s = okcoinWsCandle + "60s" + okcoinWsCandle180s = okcoinWsCandle + "180s" + okcoinWsCandle300s = okcoinWsCandle + "300s" + okcoinWsCandle900s = okcoinWsCandle + "900s" + okcoinWsCandle1800s = okcoinWsCandle + "1800s" + okcoinWsCandle3600s = okcoinWsCandle + "3600s" + okcoinWsCandle7200s = okcoinWsCandle + "7200s" + okcoinWsCandle14400s = okcoinWsCandle + "14400s" + okcoinWsCandle21600s = okcoinWsCandle + "21600" + okcoinWsCandle43200s = okcoinWsCandle + "43200s" + okcoinWsCandle86400s = okcoinWsCandle + "86400s" + okcoinWsCandle604900s = okcoinWsCandle + "604800s" + okcoinWsTicker = "ticker" + okcoinWsTrade = "trade" + okcoinWsDepth = "depth" + okcoinWsDepth5 = "depth5" + okcoinWsAccount = "account" + okcoinWsMarginAccount = "margin_account" + okcoinWsOrder = "order" + okcoinWsFundingRate = "funding_rate" + okcoinWsPriceRange = "price_range" + okcoinWsMarkPrice = "mark_price" + okcoinWsPosition = "position" + okcoinWsEstimatedPrice = "estimated_price" + // Spot endpoints + okcoinWsSpotTicker = okcoinWsSpotSubsection + okcoinWsTicker + okcoinWsSpotCandle60s = okcoinWsSpotSubsection + okcoinWsCandle60s + okcoinWsSpotCandle180s = okcoinWsSpotSubsection + okcoinWsCandle180s + okcoinWsSpotCandle300s = okcoinWsSpotSubsection + okcoinWsCandle300s + okcoinWsSpotCandle900s = okcoinWsSpotSubsection + okcoinWsCandle900s + okcoinWsSpotCandle1800s = okcoinWsSpotSubsection + okcoinWsCandle1800s + okcoinWsSpotCandle3600s = okcoinWsSpotSubsection + okcoinWsCandle3600s + okcoinWsSpotCandle7200s = okcoinWsSpotSubsection + okcoinWsCandle7200s + okcoinWsSpotCandle14400s = okcoinWsSpotSubsection + okcoinWsCandle14400s + okcoinWsSpotCandle21600s = okcoinWsSpotSubsection + okcoinWsCandle21600s + okcoinWsSpotCandle43200s = okcoinWsSpotSubsection + okcoinWsCandle43200s + okcoinWsSpotCandle86400s = okcoinWsSpotSubsection + okcoinWsCandle86400s + okcoinWsSpotCandle604900s = okcoinWsSpotSubsection + okcoinWsCandle604900s + okcoinWsSpotTrade = okcoinWsSpotSubsection + okcoinWsTrade + okcoinWsSpotDepth = okcoinWsSpotSubsection + okcoinWsDepth + okcoinWsSpotDepth5 = okcoinWsSpotSubsection + okcoinWsDepth5 + okcoinWsSpotAccount = okcoinWsSpotSubsection + okcoinWsAccount + okcoinWsSpotMarginAccount = okcoinWsSpotSubsection + okcoinWsMarginAccount + okcoinWsSpotOrder = okcoinWsSpotSubsection + okcoinWsOrder + // Swap endpoints + okcoinWsSwapTicker = okcoinWsSwapSubsection + okcoinWsTicker + okcoinWsSwapCandle60s = okcoinWsSwapSubsection + okcoinWsCandle60s + okcoinWsSwapCandle180s = okcoinWsSwapSubsection + okcoinWsCandle180s + okcoinWsSwapCandle300s = okcoinWsSwapSubsection + okcoinWsCandle300s + okcoinWsSwapCandle900s = okcoinWsSwapSubsection + okcoinWsCandle900s + okcoinWsSwapCandle1800s = okcoinWsSwapSubsection + okcoinWsCandle1800s + okcoinWsSwapCandle3600s = okcoinWsSwapSubsection + okcoinWsCandle3600s + okcoinWsSwapCandle7200s = okcoinWsSwapSubsection + okcoinWsCandle7200s + okcoinWsSwapCandle14400s = okcoinWsSwapSubsection + okcoinWsCandle14400s + okcoinWsSwapCandle21600s = okcoinWsSwapSubsection + okcoinWsCandle21600s + okcoinWsSwapCandle43200s = okcoinWsSwapSubsection + okcoinWsCandle43200s + okcoinWsSwapCandle86400s = okcoinWsSwapSubsection + okcoinWsCandle86400s + okcoinWsSwapCandle604900s = okcoinWsSwapSubsection + okcoinWsCandle604900s + okcoinWsSwapTrade = okcoinWsSwapSubsection + okcoinWsTrade + okcoinWsSwapDepth = okcoinWsSwapSubsection + okcoinWsDepth + okcoinWsSwapDepth5 = okcoinWsSwapSubsection + okcoinWsDepth5 + okcoinWsSwapFundingRate = okcoinWsSwapSubsection + okcoinWsFundingRate + okcoinWsSwapPriceRange = okcoinWsSwapSubsection + okcoinWsPriceRange + okcoinWsSwapMarkPrice = okcoinWsSwapSubsection + okcoinWsMarkPrice + okcoinWsSwapPosition = okcoinWsSwapSubsection + okcoinWsPosition + okcoinWsSwapAccount = okcoinWsSwapSubsection + okcoinWsAccount + okcoinWsSwapOrder = okcoinWsSwapSubsection + okcoinWsOrder + // Index endpoints + okcoinWsIndexTicker = okcoinWsIndexSubsection + okcoinWsTicker + okcoinWsIndexCandle60s = okcoinWsIndexSubsection + okcoinWsCandle60s + okcoinWsIndexCandle180s = okcoinWsIndexSubsection + okcoinWsCandle180s + okcoinWsIndexCandle300s = okcoinWsIndexSubsection + okcoinWsCandle300s + okcoinWsIndexCandle900s = okcoinWsIndexSubsection + okcoinWsCandle900s + okcoinWsIndexCandle1800s = okcoinWsIndexSubsection + okcoinWsCandle1800s + okcoinWsIndexCandle3600s = okcoinWsIndexSubsection + okcoinWsCandle3600s + okcoinWsIndexCandle7200s = okcoinWsIndexSubsection + okcoinWsCandle7200s + okcoinWsIndexCandle14400s = okcoinWsIndexSubsection + okcoinWsCandle14400s + okcoinWsIndexCandle21600s = okcoinWsIndexSubsection + okcoinWsCandle21600s + okcoinWsIndexCandle43200s = okcoinWsIndexSubsection + okcoinWsCandle43200s + okcoinWsIndexCandle86400s = okcoinWsIndexSubsection + okcoinWsCandle86400s + okcoinWsIndexCandle604900s = okcoinWsIndexSubsection + okcoinWsCandle604900s + // Futures endpoints + okcoinWsFuturesTicker = okcoinWsFuturesSubsection + okcoinWsTicker + okcoinWsFuturesCandle60s = okcoinWsFuturesSubsection + okcoinWsCandle60s + okcoinWsFuturesCandle180s = okcoinWsFuturesSubsection + okcoinWsCandle180s + okcoinWsFuturesCandle300s = okcoinWsFuturesSubsection + okcoinWsCandle300s + okcoinWsFuturesCandle900s = okcoinWsFuturesSubsection + okcoinWsCandle900s + okcoinWsFuturesCandle1800s = okcoinWsFuturesSubsection + okcoinWsCandle1800s + okcoinWsFuturesCandle3600s = okcoinWsFuturesSubsection + okcoinWsCandle3600s + okcoinWsFuturesCandle7200s = okcoinWsFuturesSubsection + okcoinWsCandle7200s + okcoinWsFuturesCandle14400s = okcoinWsFuturesSubsection + okcoinWsCandle14400s + okcoinWsFuturesCandle21600s = okcoinWsFuturesSubsection + okcoinWsCandle21600s + okcoinWsFuturesCandle43200s = okcoinWsFuturesSubsection + okcoinWsCandle43200s + okcoinWsFuturesCandle86400s = okcoinWsFuturesSubsection + okcoinWsCandle86400s + okcoinWsFuturesCandle604900s = okcoinWsFuturesSubsection + okcoinWsCandle604900s + okcoinWsFuturesTrade = okcoinWsFuturesSubsection + okcoinWsTrade + okcoinWsFuturesEstimatedPrice = okcoinWsFuturesSubsection + okcoinWsTrade + okcoinWsFuturesPriceRange = okcoinWsFuturesSubsection + okcoinWsPriceRange + okcoinWsFuturesDepth = okcoinWsFuturesSubsection + okcoinWsDepth + okcoinWsFuturesDepth5 = okcoinWsFuturesSubsection + okcoinWsDepth5 + okcoinWsFuturesMarkPrice = okcoinWsFuturesSubsection + okcoinWsMarkPrice + okcoinWsFuturesAccount = okcoinWsFuturesSubsection + okcoinWsAccount + okcoinWsFuturesPosition = okcoinWsFuturesSubsection + okcoinWsPosition + okcoinWsFuturesOrder = okcoinWsFuturesSubsection + okcoinWsOrder + + okcoinWsRateLimit = 30 + + allowableIterations = 25 + delimiterColon = ":" + delimiterDash = "-" + + maxConnByteLen = 4096 +) + +// orderbookMutex Ensures if two entries arrive at once, only one can be +// processed at a time +var orderbookMutex sync.Mutex + +var defaultSpotSubscribedChannels = []string{okcoinWsSpotDepth, + okcoinWsSpotCandle300s, + okcoinWsSpotTicker, + okcoinWsSpotTrade} + +var defaultFuturesSubscribedChannels = []string{okcoinWsFuturesDepth, + okcoinWsFuturesCandle300s, + okcoinWsFuturesTicker, + okcoinWsFuturesTrade} + +var defaultIndexSubscribedChannels = []string{okcoinWsIndexCandle300s, + okcoinWsIndexTicker} + +var defaultSwapSubscribedChannels = []string{okcoinWsSwapDepth, + okcoinWsSwapCandle300s, + okcoinWsSwapTicker, + okcoinWsSwapTrade, + okcoinWsSwapFundingRate, + okcoinWsSwapMarkPrice} + +// SetErrorDefaults sets the full error default list +func (o *OKCoin) SetErrorDefaults() { + o.ErrorCodes = map[string]error{ + "0": errors.New("successful"), + "1": errors.New("invalid parameter in url normally"), + "30001": errors.New("request header \"OK_ACCESS_KEY\" cannot be blank"), + "30002": errors.New("request header \"OK_ACCESS_SIGN\" cannot be blank"), + "30003": errors.New("request header \"OK_ACCESS_TIMESTAMP\" cannot be blank"), + "30004": errors.New("request header \"OK_ACCESS_PASSPHRASE\" cannot be blank"), + "30005": errors.New("invalid OK_ACCESS_TIMESTAMP"), + "30006": errors.New("invalid OK_ACCESS_KEY"), + "30007": errors.New("invalid Content_Type, please use \"application/json\" format"), + "30008": errors.New("timestamp request expired"), + "30009": errors.New("system error"), + "30010": errors.New("api validation failed"), + "30011": errors.New("invalid IP"), + "30012": errors.New("invalid authorization"), + "30013": errors.New("invalid sign"), + "30014": errors.New("request too frequent"), + "30015": errors.New("request header \"OK_ACCESS_PASSPHRASE\" incorrect"), + "30016": errors.New("you are using v1 apiKey, please use v1 endpoint. If you would like to use v3 endpoint, please subscribe to v3 apiKey"), + "30017": errors.New("apikey's broker id does not match"), + "30018": errors.New("apikey's domain does not match"), + "30020": errors.New("body cannot be blank"), + "30021": errors.New("json data format error"), + "30023": errors.New("required parameter cannot be blank"), + "30024": errors.New("parameter value error"), + "30025": errors.New("parameter category error"), + "30026": errors.New("requested too frequent; endpoint limit exceeded"), + "30027": errors.New("login failure"), + "30028": errors.New("unauthorized execution"), + "30029": errors.New("account suspended"), + "30030": errors.New("endpoint request failed. Please try again"), + "30031": errors.New("token does not exist"), + "30032": errors.New("pair does not exist"), + "30033": errors.New("exchange domain does not exist"), + "30034": errors.New("exchange ID does not exist"), + "30035": errors.New("trading is not supported in this website"), + "30036": errors.New("no relevant data"), + "30037": errors.New("endpoint is offline or unavailable"), + "30038": errors.New("user does not exist"), + "32001": errors.New("futures account suspended"), + "32002": errors.New("futures account does not exist"), + "32003": errors.New("canceling, please wait"), + "32004": errors.New("you have no unfilled orders"), + "32005": errors.New("max order quantity"), + "32006": errors.New("the order price or trigger price exceeds USD 1 million"), + "32007": errors.New("leverage level must be the same for orders on the same side of the contract"), + "32008": errors.New("max. positions to open (cross margin)"), + "32009": errors.New("max. positions to open (fixed margin)"), + "32010": errors.New("leverage cannot be changed with open positions"), + "32011": errors.New("futures status error"), + "32012": errors.New("futures order update error"), + "32013": errors.New("token type is blank"), + "32014": errors.New("your number of contracts closing is larger than the number of contracts available"), + "32015": errors.New("margin ratio is lower than 100% before opening positions"), + "32016": errors.New("margin ratio is lower than 100% after opening position"), + "32017": errors.New("no BBO"), + "32018": errors.New("the order quantity is less than 1, please try again"), + "32019": errors.New("the order price deviates from the price of the previous minute by more than 3%"), + "32020": errors.New("the price is not in the range of the price limit"), + "32021": errors.New("leverage error"), + "32022": errors.New("this function is not supported in your country or region according to the regulations"), + "32023": errors.New("this account has outstanding loan"), + "32024": errors.New("order cannot be placed during delivery"), + "32025": errors.New("order cannot be placed during settlement"), + "32026": errors.New("your account is restricted from opening positions"), + "32027": errors.New("cancelled over 20 orders"), + "32028": errors.New("account is suspended and liquidated"), + "32029": errors.New("order info does not exist"), + "33001": errors.New("margin account for this pair is not enabled yet"), + "33002": errors.New("margin account for this pair is suspended"), + "33003": errors.New("no loan balance"), + "33004": errors.New("loan amount cannot be smaller than the minimum limit"), + "33005": errors.New("repayment amount must exceed 0"), + "33006": errors.New("loan order not found"), + "33007": errors.New("status not found"), + "33008": errors.New("loan amount cannot exceed the maximum limit"), + "33009": errors.New("user ID is blank"), + "33010": errors.New("you cannot cancel an order during session 2 of call auction"), + "33011": errors.New("no new market data"), + "33012": errors.New("order cancellation failed"), + "33013": errors.New("order placement failed"), + "33014": errors.New("order does not exist"), + "33015": errors.New("exceeded maximum limit"), + "33016": errors.New("margin trading is not open for this token"), + "33017": errors.New("insufficient balance"), + "33018": errors.New("this parameter must be smaller than 1"), + "33020": errors.New("request not supported"), + "33021": errors.New("token and the pair do not match"), + "33022": errors.New("pair and the order do not match"), + "33023": errors.New("you can only place market orders during call auction"), + "33024": errors.New("trading amount too small"), + "33025": errors.New("base token amount is blank"), + "33026": errors.New("transaction completed"), + "33027": errors.New("cancelled order or order cancelling"), + "33028": errors.New("the decimal places of the trading price exceeded the limit"), + "33029": errors.New("the decimal places of the trading size exceeded the limit"), + "34001": errors.New("withdrawal suspended"), + "34002": errors.New("please add a withdrawal address"), + "34003": errors.New("sorry, this token cannot be withdrawn to xx at the moment"), + "34004": errors.New("withdrawal fee is smaller than minimum limit"), + "34005": errors.New("withdrawal fee exceeds the maximum limit"), + "34006": errors.New("withdrawal amount is lower than the minimum limit"), + "34007": errors.New("withdrawal amount exceeds the maximum limit"), + "34008": errors.New("insufficient balance"), + "34009": errors.New("your withdrawal amount exceeds the daily limit"), + "34010": errors.New("transfer amount must be larger than 0"), + "34011": errors.New("conditions not met"), + "34012": errors.New("the minimum withdrawal amount for NEO is 1, and the amount must be an integer"), + "34013": errors.New("please transfer"), + "34014": errors.New("transfer limited"), + "34015": errors.New("subaccount does not exist"), + "34016": errors.New("transfer suspended"), + "34017": errors.New("account suspended"), + "34018": errors.New("incorrect trades password"), + "34019": errors.New("please bind your email before withdrawal"), + "34020": errors.New("please bind your funds password before withdrawal"), + "34021": errors.New("not verified address"), + "34022": errors.New("withdrawals are not available for sub accounts"), + "35001": errors.New("contract subscribing does not exist"), + "35002": errors.New("contract is being settled"), + "35003": errors.New("contract is being paused"), + "35004": errors.New("pending contract settlement"), + "35005": errors.New("perpetual swap trading is not enabled"), + "35008": errors.New("margin ratio too low when placing order"), + "35010": errors.New("closing position size larger than available size"), + "35012": errors.New("placing an order with less than 1 contract"), + "35014": errors.New("order size is not in acceptable range"), + "35015": errors.New("leverage level unavailable"), + "35017": errors.New("changing leverage level"), + "35019": errors.New("order size exceeds limit"), + "35020": errors.New("order price exceeds limit"), + "35021": errors.New("order size exceeds limit of the current tier"), + "35022": errors.New("contract is paused or closed"), + "35030": errors.New("place multiple orders"), + "35031": errors.New("cancel multiple orders"), + "35061": errors.New("invalid instrument_id"), + } +} diff --git a/exchanges/okgroup/okgroup_websocket.go b/exchanges/okcoin/okcoin_websocket.go similarity index 54% rename from exchanges/okgroup/okgroup_websocket.go rename to exchanges/okcoin/okcoin_websocket.go index e5b34eec..5ae3ac7f 100644 --- a/exchanges/okgroup/okgroup_websocket.go +++ b/exchanges/okcoin/okcoin_websocket.go @@ -1,4 +1,4 @@ -package okgroup +package okcoin import ( "context" @@ -9,7 +9,6 @@ import ( "net/http" "strconv" "strings" - "sync" "time" "github.com/gorilla/websocket" @@ -24,158 +23,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/log" ) -// List of all websocket channels to subscribe to -const ( - // Orderbook events - okGroupWsOrderbookUpdate = "update" - okGroupWsOrderbookPartial = "partial" - // API subsections - okGroupWsSwapSubsection = "swap/" - okGroupWsIndexSubsection = "index/" - okGroupWsFuturesSubsection = "futures/" - okGroupWsSpotSubsection = "spot/" - // Shared API endpoints - okGroupWsCandle = "candle" - okGroupWsCandle60s = okGroupWsCandle + "60s" - okGroupWsCandle180s = okGroupWsCandle + "180s" - okGroupWsCandle300s = okGroupWsCandle + "300s" - okGroupWsCandle900s = okGroupWsCandle + "900s" - okGroupWsCandle1800s = okGroupWsCandle + "1800s" - okGroupWsCandle3600s = okGroupWsCandle + "3600s" - okGroupWsCandle7200s = okGroupWsCandle + "7200s" - okGroupWsCandle14400s = okGroupWsCandle + "14400s" - okGroupWsCandle21600s = okGroupWsCandle + "21600" - okGroupWsCandle43200s = okGroupWsCandle + "43200s" - okGroupWsCandle86400s = okGroupWsCandle + "86400s" - okGroupWsCandle604900s = okGroupWsCandle + "604800s" - okGroupWsTicker = "ticker" - okGroupWsTrade = "trade" - okGroupWsDepth = "depth" - okGroupWsDepth5 = "depth5" - okGroupWsAccount = "account" - okGroupWsMarginAccount = "margin_account" - okGroupWsOrder = "order" - okGroupWsFundingRate = "funding_rate" - okGroupWsPriceRange = "price_range" - okGroupWsMarkPrice = "mark_price" - okGroupWsPosition = "position" - okGroupWsEstimatedPrice = "estimated_price" - // Spot endpoints - okGroupWsSpotTicker = okGroupWsSpotSubsection + okGroupWsTicker - okGroupWsSpotCandle60s = okGroupWsSpotSubsection + okGroupWsCandle60s - okGroupWsSpotCandle180s = okGroupWsSpotSubsection + okGroupWsCandle180s - okGroupWsSpotCandle300s = okGroupWsSpotSubsection + okGroupWsCandle300s - okGroupWsSpotCandle900s = okGroupWsSpotSubsection + okGroupWsCandle900s - okGroupWsSpotCandle1800s = okGroupWsSpotSubsection + okGroupWsCandle1800s - okGroupWsSpotCandle3600s = okGroupWsSpotSubsection + okGroupWsCandle3600s - okGroupWsSpotCandle7200s = okGroupWsSpotSubsection + okGroupWsCandle7200s - okGroupWsSpotCandle14400s = okGroupWsSpotSubsection + okGroupWsCandle14400s - okGroupWsSpotCandle21600s = okGroupWsSpotSubsection + okGroupWsCandle21600s - okGroupWsSpotCandle43200s = okGroupWsSpotSubsection + okGroupWsCandle43200s - okGroupWsSpotCandle86400s = okGroupWsSpotSubsection + okGroupWsCandle86400s - okGroupWsSpotCandle604900s = okGroupWsSpotSubsection + okGroupWsCandle604900s - okGroupWsSpotTrade = okGroupWsSpotSubsection + okGroupWsTrade - okGroupWsSpotDepth = okGroupWsSpotSubsection + okGroupWsDepth - okGroupWsSpotDepth5 = okGroupWsSpotSubsection + okGroupWsDepth5 - okGroupWsSpotAccount = okGroupWsSpotSubsection + okGroupWsAccount - okGroupWsSpotMarginAccount = okGroupWsSpotSubsection + okGroupWsMarginAccount - okGroupWsSpotOrder = okGroupWsSpotSubsection + okGroupWsOrder - // Swap endpoints - okGroupWsSwapTicker = okGroupWsSwapSubsection + okGroupWsTicker - okGroupWsSwapCandle60s = okGroupWsSwapSubsection + okGroupWsCandle60s - okGroupWsSwapCandle180s = okGroupWsSwapSubsection + okGroupWsCandle180s - okGroupWsSwapCandle300s = okGroupWsSwapSubsection + okGroupWsCandle300s - okGroupWsSwapCandle900s = okGroupWsSwapSubsection + okGroupWsCandle900s - okGroupWsSwapCandle1800s = okGroupWsSwapSubsection + okGroupWsCandle1800s - okGroupWsSwapCandle3600s = okGroupWsSwapSubsection + okGroupWsCandle3600s - okGroupWsSwapCandle7200s = okGroupWsSwapSubsection + okGroupWsCandle7200s - okGroupWsSwapCandle14400s = okGroupWsSwapSubsection + okGroupWsCandle14400s - okGroupWsSwapCandle21600s = okGroupWsSwapSubsection + okGroupWsCandle21600s - okGroupWsSwapCandle43200s = okGroupWsSwapSubsection + okGroupWsCandle43200s - okGroupWsSwapCandle86400s = okGroupWsSwapSubsection + okGroupWsCandle86400s - okGroupWsSwapCandle604900s = okGroupWsSwapSubsection + okGroupWsCandle604900s - okGroupWsSwapTrade = okGroupWsSwapSubsection + okGroupWsTrade - okGroupWsSwapDepth = okGroupWsSwapSubsection + okGroupWsDepth - okGroupWsSwapDepth5 = okGroupWsSwapSubsection + okGroupWsDepth5 - okGroupWsSwapFundingRate = okGroupWsSwapSubsection + okGroupWsFundingRate - okGroupWsSwapPriceRange = okGroupWsSwapSubsection + okGroupWsPriceRange - okGroupWsSwapMarkPrice = okGroupWsSwapSubsection + okGroupWsMarkPrice - okGroupWsSwapPosition = okGroupWsSwapSubsection + okGroupWsPosition - okGroupWsSwapAccount = okGroupWsSwapSubsection + okGroupWsAccount - okGroupWsSwapOrder = okGroupWsSwapSubsection + okGroupWsOrder - // Index endpoints - okGroupWsIndexTicker = okGroupWsIndexSubsection + okGroupWsTicker - okGroupWsIndexCandle60s = okGroupWsIndexSubsection + okGroupWsCandle60s - okGroupWsIndexCandle180s = okGroupWsIndexSubsection + okGroupWsCandle180s - okGroupWsIndexCandle300s = okGroupWsIndexSubsection + okGroupWsCandle300s - okGroupWsIndexCandle900s = okGroupWsIndexSubsection + okGroupWsCandle900s - okGroupWsIndexCandle1800s = okGroupWsIndexSubsection + okGroupWsCandle1800s - okGroupWsIndexCandle3600s = okGroupWsIndexSubsection + okGroupWsCandle3600s - okGroupWsIndexCandle7200s = okGroupWsIndexSubsection + okGroupWsCandle7200s - okGroupWsIndexCandle14400s = okGroupWsIndexSubsection + okGroupWsCandle14400s - okGroupWsIndexCandle21600s = okGroupWsIndexSubsection + okGroupWsCandle21600s - okGroupWsIndexCandle43200s = okGroupWsIndexSubsection + okGroupWsCandle43200s - okGroupWsIndexCandle86400s = okGroupWsIndexSubsection + okGroupWsCandle86400s - okGroupWsIndexCandle604900s = okGroupWsIndexSubsection + okGroupWsCandle604900s - // Futures endpoints - okGroupWsFuturesTicker = okGroupWsFuturesSubsection + okGroupWsTicker - okGroupWsFuturesCandle60s = okGroupWsFuturesSubsection + okGroupWsCandle60s - okGroupWsFuturesCandle180s = okGroupWsFuturesSubsection + okGroupWsCandle180s - okGroupWsFuturesCandle300s = okGroupWsFuturesSubsection + okGroupWsCandle300s - okGroupWsFuturesCandle900s = okGroupWsFuturesSubsection + okGroupWsCandle900s - okGroupWsFuturesCandle1800s = okGroupWsFuturesSubsection + okGroupWsCandle1800s - okGroupWsFuturesCandle3600s = okGroupWsFuturesSubsection + okGroupWsCandle3600s - okGroupWsFuturesCandle7200s = okGroupWsFuturesSubsection + okGroupWsCandle7200s - okGroupWsFuturesCandle14400s = okGroupWsFuturesSubsection + okGroupWsCandle14400s - okGroupWsFuturesCandle21600s = okGroupWsFuturesSubsection + okGroupWsCandle21600s - okGroupWsFuturesCandle43200s = okGroupWsFuturesSubsection + okGroupWsCandle43200s - okGroupWsFuturesCandle86400s = okGroupWsFuturesSubsection + okGroupWsCandle86400s - okGroupWsFuturesCandle604900s = okGroupWsFuturesSubsection + okGroupWsCandle604900s - okGroupWsFuturesTrade = okGroupWsFuturesSubsection + okGroupWsTrade - okGroupWsFuturesEstimatedPrice = okGroupWsFuturesSubsection + okGroupWsTrade - okGroupWsFuturesPriceRange = okGroupWsFuturesSubsection + okGroupWsPriceRange - okGroupWsFuturesDepth = okGroupWsFuturesSubsection + okGroupWsDepth - okGroupWsFuturesDepth5 = okGroupWsFuturesSubsection + okGroupWsDepth5 - okGroupWsFuturesMarkPrice = okGroupWsFuturesSubsection + okGroupWsMarkPrice - okGroupWsFuturesAccount = okGroupWsFuturesSubsection + okGroupWsAccount - okGroupWsFuturesPosition = okGroupWsFuturesSubsection + okGroupWsPosition - okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder - - okGroupWsRateLimit = 30 - - allowableIterations = 25 - delimiterColon = ":" - delimiterDash = "-" - - maxConnByteLen = 4096 -) - -// orderbookMutex Ensures if two entries arrive at once, only one can be -// processed at a time -var orderbookMutex sync.Mutex - -var defaultSpotSubscribedChannels = []string{okGroupWsSpotDepth, - okGroupWsSpotCandle300s, - okGroupWsSpotTicker, - okGroupWsSpotTrade} - -var defaultFuturesSubscribedChannels = []string{okGroupWsFuturesDepth, - okGroupWsFuturesCandle300s, - okGroupWsFuturesTicker, - okGroupWsFuturesTrade} - -var defaultIndexSubscribedChannels = []string{okGroupWsIndexCandle300s, - okGroupWsIndexTicker} - -var defaultSwapSubscribedChannels = []string{okGroupWsSwapDepth, - okGroupWsSwapCandle300s, - okGroupWsSwapTicker, - okGroupWsSwapTrade, - okGroupWsSwapFundingRate, - okGroupWsSwapMarkPrice} - // WsConnect initiates a websocket connection -func (o *OKGroup) WsConnect() error { +func (o *OKCoin) WsConnect() error { if !o.Websocket.IsEnabled() || !o.IsEnabled() { return errors.New(stream.WebsocketNotEnabled) } @@ -208,7 +57,7 @@ func (o *OKGroup) WsConnect() error { } // WsLogin sends a login request to websocket to enable access to authenticated endpoints -func (o *OKGroup) WsLogin(ctx context.Context) error { +func (o *OKCoin) WsLogin(ctx context.Context) error { creds, err := o.GetCredentials(ctx) if err != nil { return err @@ -242,7 +91,7 @@ func (o *OKGroup) WsLogin(ctx context.Context) error { } // WsReadData receives and passes on websocket messages for processing -func (o *OKGroup) WsReadData() { +func (o *OKCoin) WsReadData() { defer o.Websocket.Wg.Done() for { @@ -258,7 +107,7 @@ func (o *OKGroup) WsReadData() { } // WsHandleData will read websocket raw data and pass to appropriate handler -func (o *OKGroup) WsHandleData(respRaw []byte) error { +func (o *OKCoin) WsHandleData(respRaw []byte) error { var dataResponse WebsocketDataResponse err := json.Unmarshal(respRaw, &dataResponse) if err != nil { @@ -266,18 +115,18 @@ func (o *OKGroup) WsHandleData(respRaw []byte) error { } if len(dataResponse.Data) > 0 { switch o.GetWsChannelWithoutOrderType(dataResponse.Table) { - case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, - okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s, - okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s, - okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: + case okcoinWsCandle60s, okcoinWsCandle180s, okcoinWsCandle300s, + okcoinWsCandle900s, okcoinWsCandle1800s, okcoinWsCandle3600s, + okcoinWsCandle7200s, okcoinWsCandle14400s, okcoinWsCandle21600s, + okcoinWsCandle43200s, okcoinWsCandle86400s, okcoinWsCandle604900s: return o.wsProcessCandles(respRaw) - case okGroupWsDepth, okGroupWsDepth5: + case okcoinWsDepth, okcoinWsDepth5: return o.WsProcessOrderBook(respRaw) - case okGroupWsTicker: + case okcoinWsTicker: return o.wsProcessTickers(respRaw) - case okGroupWsTrade: + case okcoinWsTrade: return o.wsProcessTrades(respRaw) - case okGroupWsOrder: + case okcoinWsOrder: return o.wsProcessOrder(respRaw) } o.Websocket.DataHandler <- stream.UnhandledMessageWarning{ @@ -332,7 +181,7 @@ func StringToOrderStatus(num int64) (order.Status, error) { } } -func (o *OKGroup) wsProcessOrder(respRaw []byte) error { +func (o *OKCoin) wsProcessOrder(respRaw []byte) error { var resp WebsocketSpotOrderResponse err := json.Unmarshal(respRaw, &resp) if err != nil { @@ -398,7 +247,7 @@ func (o *OKGroup) wsProcessOrder(respRaw []byte) error { } // wsProcessTickers converts ticker data and sends it to the datahandler -func (o *OKGroup) wsProcessTickers(respRaw []byte) error { +func (o *OKCoin) wsProcessTickers(respRaw []byte) error { var response WebsocketTickerData err := json.Unmarshal(respRaw, &response) if err != nil { @@ -408,15 +257,7 @@ func (o *OKGroup) wsProcessTickers(respRaw []byte) error { for i := range response.Data { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) - var c currency.Pair - switch a { - case asset.Futures, asset.PerpetualSwap: - c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], - f[2], - currency.UnderscoreDelimiter) - default: - c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) - } + c := currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) baseVolume := response.Data[i].BaseVolume24h if response.Data[i].ContractVolume24h != 0 { @@ -439,7 +280,7 @@ func (o *OKGroup) wsProcessTickers(respRaw []byte) error { Bid: response.Data[i].BestBid, Ask: response.Data[i].BestAsk, Last: response.Data[i].Last, - AssetType: o.GetAssetTypeFromTableName(response.Table), + AssetType: a, Pair: c, LastUpdated: response.Data[i].Timestamp, } @@ -448,7 +289,7 @@ func (o *OKGroup) wsProcessTickers(respRaw []byte) error { } // wsProcessTrades converts trade data and sends it to the datahandler -func (o *OKGroup) wsProcessTrades(respRaw []byte) error { +func (o *OKCoin) wsProcessTrades(respRaw []byte) error { if !o.IsSaveTradeDataEnabled() { return nil } @@ -462,16 +303,7 @@ func (o *OKGroup) wsProcessTrades(respRaw []byte) error { trades := make([]trade.Data, len(response.Data)) for i := range response.Data { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) - - var c currency.Pair - switch a { - case asset.Futures, asset.PerpetualSwap: - c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], - f[2], - currency.UnderscoreDelimiter) - default: - c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) - } + c := currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) tSide, err := order.StringToOrderSide(response.Data[i].Side) if err != nil { @@ -487,7 +319,7 @@ func (o *OKGroup) wsProcessTrades(respRaw []byte) error { } trades[i] = trade.Data{ Amount: amount, - AssetType: o.GetAssetTypeFromTableName(response.Table), + AssetType: a, CurrencyPair: c, Exchange: o.Name, Price: response.Data[i].Price, @@ -500,7 +332,7 @@ func (o *OKGroup) wsProcessTrades(respRaw []byte) error { } // wsProcessCandles converts candle data and sends it to the data handler -func (o *OKGroup) wsProcessCandles(respRaw []byte) error { +func (o *OKCoin) wsProcessCandles(respRaw []byte) error { var response WebsocketCandleResponse err := json.Unmarshal(respRaw, &response) if err != nil { @@ -510,16 +342,7 @@ func (o *OKGroup) wsProcessCandles(respRaw []byte) error { a := o.GetAssetTypeFromTableName(response.Table) for i := range response.Data { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) - - var c currency.Pair - switch a { - case asset.Futures, asset.PerpetualSwap: - c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], - f[2], - currency.UnderscoreDelimiter) - default: - c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) - } + c := currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) timeData, err := time.Parse(time.RFC3339Nano, response.Data[i].Candle[0]) @@ -529,11 +352,11 @@ func (o *OKGroup) wsProcessCandles(respRaw []byte) error { response.Data[i].Candle[0]) } - candleIndex := strings.LastIndex(response.Table, okGroupWsCandle) - candleInterval := response.Table[candleIndex+len(okGroupWsCandle):] + candleIndex := strings.LastIndex(response.Table, okcoinWsCandle) + candleInterval := response.Table[candleIndex+len(okcoinWsCandle):] klineData := stream.KlineData{ - AssetType: o.GetAssetTypeFromTableName(response.Table), + AssetType: a, Pair: c, Exchange: o.Name, Timestamp: timeData, @@ -565,7 +388,7 @@ func (o *OKGroup) wsProcessCandles(respRaw []byte) error { } // WsProcessOrderBook Validates the checksum and updates internal orderbook values -func (o *OKGroup) WsProcessOrderBook(respRaw []byte) error { +func (o *OKCoin) WsProcessOrderBook(respRaw []byte) error { var response WebsocketOrderBooksData err := json.Unmarshal(respRaw, &response) if err != nil { @@ -576,18 +399,9 @@ func (o *OKGroup) WsProcessOrderBook(respRaw []byte) error { a := o.GetAssetTypeFromTableName(response.Table) for i := range response.Data { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c := currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) - var c currency.Pair - switch a { - case asset.Futures, asset.PerpetualSwap: - c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], - f[2], - currency.UnderscoreDelimiter) - default: - c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) - } - - if response.Action == okGroupWsOrderbookPartial { + if response.Action == okcoinWsOrderbookPartial { err := o.WsProcessPartialOrderBook(&response.Data[i], c, a) if err != nil { err2 := o.wsResubscribeToOrderbook(&response) @@ -596,7 +410,7 @@ func (o *OKGroup) WsProcessOrderBook(respRaw []byte) error { } return err } - } else if response.Action == okGroupWsOrderbookUpdate { + } else if response.Action == okcoinWsOrderbookUpdate { if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 { return nil } @@ -613,18 +427,12 @@ func (o *OKGroup) WsProcessOrderBook(respRaw []byte) error { return nil } -func (o *OKGroup) wsResubscribeToOrderbook(response *WebsocketOrderBooksData) error { +func (o *OKCoin) wsResubscribeToOrderbook(response *WebsocketOrderBooksData) error { a := o.GetAssetTypeFromTableName(response.Table) for i := range response.Data { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) - var c currency.Pair - switch a { - case asset.Futures, asset.PerpetualSwap: - c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterDash) - default: - c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) - } + c := currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) channelToResubscribe := &stream.ChannelSubscription{ Channel: response.Table, @@ -641,7 +449,7 @@ func (o *OKGroup) wsResubscribeToOrderbook(response *WebsocketOrderBooksData) er // AppendWsOrderbookItems adds websocket orderbook data bid/asks into an // orderbook item array -func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) { +func (o *OKCoin) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) { items := make([]orderbook.Item, len(entries)) for j := range entries { amount, err := strconv.ParseFloat(entries[j][1].(string), 64) @@ -659,7 +467,7 @@ func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.I // WsProcessPartialOrderBook takes websocket orderbook data and creates an // orderbook Calculates checksum to ensure it is valid -func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error { +func (o *OKCoin) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error { signedChecksum, err := o.CalculatePartialOrderbookChecksum(wsEventData) if err != nil { return fmt.Errorf("%s channel: %s. Orderbook unable to calculate partial orderbook checksum: %s", @@ -705,7 +513,7 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, ins // WsProcessUpdateOrderbook updates an existing orderbook using websocket data // After merging WS data, it will sort, validate and finally update the existing // orderbook -func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error { +func (o *OKCoin) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error { update := orderbook.Update{ Asset: a, Pair: instrument, @@ -748,7 +556,7 @@ func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, inst // quantity with a semicolon (:) deliminating them. This will also work when // there are less than 25 entries (for whatever reason) // eg Bid:Ask:Bid:Ask:Ask:Ask -func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketOrderBook) (int32, error) { +func (o *OKCoin) CalculatePartialOrderbookChecksum(orderbookData *WebsocketOrderBook) (int32, error) { var checksum strings.Builder for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { @@ -789,7 +597,7 @@ func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketOrde // quantity with a semicolon (:) deliminating them. This will also work when // there are less than 25 entries (for whatever reason) // eg Bid:Ask:Bid:Ask:Ask:Ask -func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 { +func (o *OKCoin) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 { var checksum strings.Builder for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { @@ -809,7 +617,7 @@ func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be // handled by ManageSubscriptions() -func (o *OKGroup) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { +func (o *OKCoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { var subscriptions []stream.ChannelSubscription assets := o.GetAssetTypes(true) for x := range assets { @@ -818,146 +626,46 @@ func (o *OKGroup) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, return nil, err } - switch assets[x] { - case asset.Spot: - channels := defaultSpotSubscribedChannels - if o.IsWebsocketAuthenticationSupported() { - channels = append(channels, - okGroupWsSpotMarginAccount, - okGroupWsSpotAccount, - okGroupWsSpotOrder) + if assets[x] != asset.Spot { + o.Websocket.DataHandler <- fmt.Errorf("%w %v", asset.ErrNotSupported, assets[x]) + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assets[x]) + } + channels := defaultSpotSubscribedChannels + if o.IsWebsocketAuthenticationSupported() { + channels = append(channels, + okcoinWsSpotMarginAccount, + okcoinWsSpotAccount, + okcoinWsSpotOrder) + } + for i := range pairs { + p, err := o.FormatExchangeCurrency(pairs[i], asset.Spot) + if err != nil { + return nil, err } - - for i := range pairs { - p, err := o.FormatExchangeCurrency(pairs[i], asset.Spot) - if err != nil { - return nil, err - } - for y := range channels { - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: channels[y], - Currency: p, - Asset: asset.Spot, - }) - } + for y := range channels { + subscriptions = append(subscriptions, + stream.ChannelSubscription{ + Channel: channels[y], + Currency: p, + Asset: asset.Spot, + }) } - case asset.Futures: - channels := defaultFuturesSubscribedChannels - if o.IsWebsocketAuthenticationSupported() { - channels = append(channels, - okGroupWsFuturesAccount, - okGroupWsFuturesPosition, - okGroupWsFuturesOrder) - } - var futuresAccountPairs currency.Pairs - var futuresAccountCodes currency.Currencies - - for i := range pairs { - p, err := o.FormatExchangeCurrency(pairs[i], asset.Futures) - if err != nil { - return nil, err - } - for y := range channels { - if channels[y] == okGroupWsFuturesAccount { - currencyString := strings.Split(pairs[i].String(), - currency.UnderscoreDelimiter)[0] - newP, err := currency.NewPairFromString(currencyString) - if err != nil { - return nil, err - } - - if !futuresAccountCodes.Contains(newP.Base) { - // subscribe to coin-margin futures trading mode - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: channels[y], - Currency: currency.NewPair(newP.Base, currency.EMPTYCODE), - Asset: asset.Futures, - }) - futuresAccountCodes = append(futuresAccountCodes, newP.Base) - } - - if newP.Quote != currency.USDT { - // Only allows subscription to USDT margined pair - continue - } - - if !futuresAccountPairs.Contains(newP, true) { - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: channels[y], - Currency: newP, - Asset: asset.Futures, - }) - futuresAccountPairs = futuresAccountPairs.Add(newP) - } - - continue - } - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: channels[y], - Currency: p, - Asset: asset.Futures, - }) - } - } - case asset.PerpetualSwap: - channels := defaultSwapSubscribedChannels - if o.IsWebsocketAuthenticationSupported() { - channels = append(channels, - okGroupWsSwapAccount, - okGroupWsSwapPosition, - okGroupWsSwapOrder) - } - for i := range pairs { - p, err := o.FormatExchangeCurrency(pairs[i], asset.PerpetualSwap) - if err != nil { - return nil, err - } - for y := range channels { - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: channels[y], - Currency: p, - Asset: asset.PerpetualSwap, - }) - } - } - case asset.Index: - for i := range pairs { - p, err := o.FormatExchangeCurrency(pairs[i], asset.Index) - if err != nil { - return nil, err - } - for y := range defaultIndexSubscribedChannels { - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: defaultIndexSubscribedChannels[y], - Currency: p, - Asset: asset.Index, - }) - } - } - default: - o.Websocket.DataHandler <- errors.New("unhandled asset type") } } return subscriptions, nil } // Subscribe sends a websocket message to receive data from the channel -func (o *OKGroup) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { +func (o *OKCoin) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { return o.handleSubscriptions("subscribe", channelsToSubscribe) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (o *OKGroup) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { +func (o *OKCoin) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { return o.handleSubscriptions("unsubscribe", channelsToUnsubscribe) } -func (o *OKGroup) handleSubscriptions(operation string, subs []stream.ChannelSubscription) error { +func (o *OKCoin) handleSubscriptions(operation string, subs []stream.ChannelSubscription) error { request := WebsocketEventRequest{ Operation: operation, } @@ -972,7 +680,7 @@ func (o *OKGroup) handleSubscriptions(operation string, subs []stream.ChannelSub copy(temp.Arguments, request.Arguments) arg := subs[i].Channel + delimiterColon - if strings.EqualFold(subs[i].Channel, okGroupWsSpotAccount) { + if strings.EqualFold(subs[i].Channel, okcoinWsSpotAccount) { arg += subs[i].Currency.Base.String() } else { arg += subs[i].Currency.String() @@ -1026,7 +734,7 @@ func (o *OKGroup) handleSubscriptions(operation string, subs []stream.ChannelSub // GetWsChannelWithoutOrderType takes WebsocketDataResponse.Table and returns // The base channel name eg receive "spot/depth5:BTC-USDT" return "depth5" -func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string { +func (o *OKCoin) GetWsChannelWithoutOrderType(table string) string { index := strings.Index(table, "/") if index == -1 { return table @@ -1043,17 +751,11 @@ func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string { // GetAssetTypeFromTableName gets the asset type from the table name // eg "spot/ticker:BTCUSD" results in "SPOT" -func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item { +func (o *OKCoin) GetAssetTypeFromTableName(table string) asset.Item { assetIndex := strings.Index(table, "/") switch table[:assetIndex] { - case asset.Futures.String(): - return asset.Futures case asset.Spot.String(): return asset.Spot - case "swap": - return asset.PerpetualSwap - case asset.Index.String(): - return asset.Index default: log.Warnf(log.ExchangeSys, "%s unhandled asset type %s", o.Name, diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 4ee78457..553b3cf1 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -4,22 +4,28 @@ import ( "context" "fmt" "sort" + "strconv" + "strings" "sync" + "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "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" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) // GetDefaultConfig returns a default exchange config @@ -48,7 +54,6 @@ func (o *OKCoin) GetDefaultConfig() (*config.Exchange, error) { // SetDefaults method assignes the default values for OKCoin func (o *OKCoin) SetDefaults() { o.SetErrorDefaults() - o.SetCheckVarDefaults() o.Name = okCoinExchangeName o.Enabled = true o.Verbose = true @@ -149,14 +154,53 @@ func (o *OKCoin) SetDefaults() { if err != nil { log.Errorln(log.ExchangeSys, err) } - o.APIVersion = okCoinAPIVersion o.Websocket = stream.New() o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout o.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit } -// Start starts the OKGroup go routine +// Setup sets user exchange configuration settings +func (o *OKCoin) Setup(exch *config.Exchange) error { + err := exch.Validate() + if err != nil { + return err + } + if !exch.Enabled { + o.SetEnabled(false) + return nil + } + err = o.SetupDefaults(exch) + if err != nil { + return err + } + + wsEndpoint, err := o.API.Endpoints.GetURL(exchange.WebsocketSpot) + if err != nil { + return err + } + err = o.Websocket.Setup(&stream.WebsocketSetup{ + ExchangeConfig: exch, + DefaultURL: wsEndpoint, + RunningURL: wsEndpoint, + Connector: o.WsConnect, + Subscriber: o.Subscribe, + Unsubscriber: o.Unsubscribe, + GenerateSubscriptions: o.GenerateDefaultSubscriptions, + Features: &o.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + return o.Websocket.SetupNewConnection(stream.ConnectionSetup{ + RateLimit: okcoinWsRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + }) +} + +// Start starts the OKCoin go routine func (o *OKCoin) Start(wg *sync.WaitGroup) error { if wg == nil { return fmt.Errorf("%T %w", wg, common.ErrNilPointer) @@ -173,15 +217,17 @@ func (o *OKCoin) Start(wg *sync.WaitGroup) error { func (o *OKCoin) Run() { if o.Verbose { log.Debugf(log.ExchangeSys, - "%s Websocket: %s. (url: %s).\n", + "%s Websocket: %s.", o.Name, - common.IsEnabled(o.Websocket.IsEnabled()), - o.WebsocketURL) + common.IsEnabled(o.Websocket.IsEnabled())) + o.PrintEnabledPairs() } forceUpdate := false + var err error if !o.BypassConfigFormatUpgrades { - format, err := o.GetPairFormat(asset.Spot, false) + var format currency.PairFormat + format, err = o.GetPairFormat(asset.Spot, false) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", @@ -189,7 +235,8 @@ func (o *OKCoin) Run() { err) return } - enabled, err := o.CurrencyPairs.GetPairs(asset.Spot, true) + var enabled, avail currency.Pairs + enabled, err = o.CurrencyPairs.GetPairs(asset.Spot, true) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", @@ -198,7 +245,7 @@ func (o *OKCoin) Run() { return } - avail, err := o.CurrencyPairs.GetPairs(asset.Spot, false) + avail, err = o.CurrencyPairs.GetPairs(asset.Spot, false) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", @@ -237,7 +284,7 @@ func (o *OKCoin) Run() { return } - err := o.UpdateTradablePairs(context.TODO(), forceUpdate) + err = o.UpdateTradablePairs(context.TODO(), forceUpdate) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", @@ -247,7 +294,7 @@ func (o *OKCoin) Run() { } // FetchTradablePairs returns a list of the exchanges tradable pairs -func (o *OKCoin) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { +func (o *OKCoin) FetchTradablePairs(ctx context.Context, _ asset.Item) (currency.Pairs, error) { prods, err := o.GetSpotTokenPairDetails(ctx) if err != nil { return nil, err @@ -323,12 +370,12 @@ func (o *OKCoin) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item } // FetchTicker returns the ticker for a currency pair -func (o *OKCoin) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (tickerData *ticker.Price, err error) { - tickerData, err = ticker.GetTicker(o.Name, p, assetType) +func (o *OKCoin) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { + tickerData, err := ticker.GetTicker(o.Name, p, assetType) if err != nil { return o.UpdateTicker(ctx, p, assetType) } - return + return tickerData, nil } // GetRecentTrades returns the most recent trades for a currency and asset @@ -341,9 +388,9 @@ func (o *OKCoin) GetRecentTrades(ctx context.Context, p currency.Pair, assetType var resp []trade.Data switch assetType { case asset.Spot: - var tradeData []okgroup.GetSpotFilledOrdersInformationResponse + var tradeData []GetSpotFilledOrdersInformationResponse tradeData, err = o.GetSpotFilledOrdersInformation(ctx, - okgroup.GetSpotFilledOrdersInformationRequest{ + &GetSpotFilledOrdersInformationRequest{ InstrumentID: p.String(), }) if err != nil { @@ -379,6 +426,680 @@ func (o *OKCoin) GetRecentTrades(ctx context.Context, p currency.Pair, assetType } // CancelBatchOrders cancels an orders by their corresponding ID numbers -func (o *OKCoin) CancelBatchOrders(ctx context.Context, orders []order.Cancel) (order.CancelBatchResponse, error) { +func (o *OKCoin) CancelBatchOrders(_ context.Context, _ []order.Cancel) (order.CancelBatchResponse, error) { return order.CancelBatchResponse{}, common.ErrNotYetImplemented } + +// FetchOrderbook returns orderbook base on the currency pair +func (o *OKCoin) FetchOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + fPair, err := o.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } + ob, err := orderbook.Get(o.Name, fPair, assetType) + if err != nil { + return o.UpdateOrderbook(ctx, fPair, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (o *OKCoin) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ + Exchange: o.Name, + Pair: p, + Asset: a, + VerifyOrderbook: o.CanVerifyOrderbook, + } + + fPair, err := o.FormatExchangeCurrency(p, a) + if err != nil { + return book, err + } + + orderbookNew, err := o.GetOrderBook(ctx, + &GetOrderBookRequest{ + InstrumentID: fPair.String(), + Size: 200, + }, a) + if err != nil { + return book, err + } + + book.Bids = make(orderbook.Items, len(orderbookNew.Bids)) + for x := range orderbookNew.Bids { + amount, convErr := strconv.ParseFloat(orderbookNew.Bids[x][1], 64) + if convErr != nil { + return book, err + } + price, convErr := strconv.ParseFloat(orderbookNew.Bids[x][0], 64) + if convErr != nil { + return book, err + } + + var liquidationOrders, orderCount int64 + // Contract specific variables + if len(orderbookNew.Bids[x]) == 4 { + liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Bids[x][2], 10, 64) + if convErr != nil { + return book, err + } + + orderCount, convErr = strconv.ParseInt(orderbookNew.Bids[x][3], 10, 64) + if convErr != nil { + return book, err + } + } + + book.Bids[x] = orderbook.Item{ + Amount: amount, + Price: price, + LiquidationOrders: liquidationOrders, + OrderCount: orderCount, + } + } + + book.Asks = make(orderbook.Items, len(orderbookNew.Asks)) + for x := range orderbookNew.Asks { + amount, convErr := strconv.ParseFloat(orderbookNew.Asks[x][1], 64) + if convErr != nil { + return book, err + } + price, convErr := strconv.ParseFloat(orderbookNew.Asks[x][0], 64) + if convErr != nil { + return book, err + } + + var liquidationOrders, orderCount int64 + // Contract specific variables + if len(orderbookNew.Asks[x]) == 4 { + liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Asks[x][2], 10, 64) + if convErr != nil { + return book, err + } + + orderCount, convErr = strconv.ParseInt(orderbookNew.Asks[x][3], 10, 64) + if convErr != nil { + return book, err + } + } + + book.Asks[x] = orderbook.Item{ + Amount: amount, + Price: price, + LiquidationOrders: liquidationOrders, + OrderCount: orderCount, + } + } + + err = book.Process() + if err != nil { + return book, err + } + + return orderbook.Get(o.Name, fPair, a) +} + +// UpdateAccountInfo retrieves balances for all enabled currencies +func (o *OKCoin) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { + currencies, err := o.GetSpotTradingAccounts(ctx) + if err != nil { + return account.Holdings{}, err + } + + var resp account.Holdings + resp.Exchange = o.Name + currencyAccount := account.SubAccount{AssetType: assetType} + + for i := range currencies { + hold, parseErr := strconv.ParseFloat(currencies[i].Hold, 64) + if parseErr != nil { + return resp, parseErr + } + totalValue, parseErr := strconv.ParseFloat(currencies[i].Balance, 64) + if parseErr != nil { + return resp, parseErr + } + currencyAccount.Currencies = append(currencyAccount.Currencies, + account.Balance{ + CurrencyName: currency.NewCode(currencies[i].Currency), + Total: totalValue, + Hold: hold, + Free: totalValue - hold, + }) + } + + resp.Accounts = append(resp.Accounts, currencyAccount) + + creds, err := o.GetCredentials(ctx) + if err != nil { + return account.Holdings{}, err + } + err = account.Process(&resp, creds) + if err != nil { + return resp, err + } + + return resp, nil +} + +// FetchAccountInfo retrieves balances for all enabled currencies +func (o *OKCoin) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { + creds, err := o.GetCredentials(ctx) + if err != nil { + return account.Holdings{}, err + } + acc, err := account.GetHoldings(o.Name, creds, assetType) + if err != nil { + return o.UpdateAccountInfo(ctx, assetType) + } + return acc, nil +} + +// GetFundingHistory returns funding history, deposits and +// withdrawals +func (o *OKCoin) GetFundingHistory(ctx context.Context) ([]exchange.FundHistory, error) { + accountDepositHistory, err := o.GetAccountDepositHistory(ctx, "") + if err != nil { + return nil, err + } + accountWithdrawlHistory, err := o.GetAccountWithdrawalHistory(ctx, "") + if err != nil { + return nil, err + } + resp := make([]exchange.FundHistory, len(accountDepositHistory)+len(accountWithdrawlHistory)) + for x := range accountDepositHistory { + orderStatus := "" + switch accountDepositHistory[x].Status { + case 0: + orderStatus = "waiting" + case 1: + orderStatus = "confirmation account" + case 2: + orderStatus = "recharge success" + } + + resp[x] = exchange.FundHistory{ + Amount: accountDepositHistory[x].Amount, + Currency: accountDepositHistory[x].Currency, + ExchangeName: o.Name, + Status: orderStatus, + Timestamp: accountDepositHistory[x].Timestamp, + TransferID: accountDepositHistory[x].TransactionID, + TransferType: "deposit", + } + } + + for i := range accountWithdrawlHistory { + resp[len(accountDepositHistory)+i] = exchange.FundHistory{ + Amount: accountWithdrawlHistory[i].Amount, + Currency: accountWithdrawlHistory[i].Currency, + ExchangeName: o.Name, + Status: OrderStatus[accountWithdrawlHistory[i].Status], + Timestamp: accountWithdrawlHistory[i].Timestamp, + TransferID: accountWithdrawlHistory[i].TransactionID, + TransferType: "withdrawal", + } + } + return resp, nil +} + +// SubmitOrder submits a new order +func (o *OKCoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { + err := s.Validate() + if err != nil { + return nil, err + } + + fPair, err := o.FormatExchangeCurrency(s.Pair, s.AssetType) + if err != nil { + return nil, err + } + + req := PlaceOrderRequest{ + ClientOID: s.ClientID, + InstrumentID: fPair.String(), + Side: s.Side.Lower(), + Type: s.Type.Lower(), + Size: strconv.FormatFloat(s.Amount, 'f', -1, 64), + } + if s.Type == order.Limit { + req.Price = strconv.FormatFloat(s.Price, 'f', -1, 64) + } + + orderResponse, err := o.PlaceSpotOrder(ctx, &req) + if err != nil { + return nil, err + } + + if !orderResponse.Result { + return nil, order.ErrUnableToPlaceOrder + } + return s.DeriveSubmitResponse(orderResponse.OrderID) +} + +// ModifyOrder will allow of changing orderbook placement and limit to +// market conversion +func (o *OKCoin) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// CancelOrder cancels an order by its corresponding ID number +func (o *OKCoin) CancelOrder(ctx context.Context, cancel *order.Cancel) error { + err := cancel.Validate(cancel.StandardCancel()) + if err != nil { + return err + } + + orderID, err := strconv.ParseInt(cancel.OrderID, 10, 64) + if err != nil { + return err + } + + fpair, err := o.FormatExchangeCurrency(cancel.Pair, + cancel.AssetType) + if err != nil { + return err + } + + orderCancellationResponse, err := o.CancelSpotOrder(ctx, + &CancelSpotOrderRequest{ + InstrumentID: fpair.String(), + OrderID: orderID, + }) + if err != nil { + return err + } + if !orderCancellationResponse.Result { + return fmt.Errorf("order %d failed to be cancelled", + orderCancellationResponse.OrderID) + } + + return nil +} + +// CancelAllOrders cancels all orders associated with a currency pair +func (o *OKCoin) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + if err := orderCancellation.Validate(); err != nil { + return order.CancelAllResponse{}, err + } + + orderIDs := strings.Split(orderCancellation.OrderID, ",") + resp := order.CancelAllResponse{} + resp.Status = make(map[string]string) + orderIDNumbers := make([]int64, 0, len(orderIDs)) + for i := range orderIDs { + orderIDNumber, err := strconv.ParseInt(orderIDs[i], 10, 64) + if err != nil { + resp.Status[orderIDs[i]] = err.Error() + continue + } + orderIDNumbers = append(orderIDNumbers, orderIDNumber) + } + + fpair, err := o.FormatExchangeCurrency(orderCancellation.Pair, + orderCancellation.AssetType) + if err != nil { + return resp, err + } + + cancelOrdersResponse, err := o.CancelMultipleSpotOrders(ctx, + &CancelMultipleSpotOrdersRequest{ + InstrumentID: fpair.String(), + OrderIDs: orderIDNumbers, + }) + if err != nil { + return resp, err + } + + for x := range cancelOrdersResponse { + for y := range cancelOrdersResponse[x] { + resp.Status[strconv.FormatInt(cancelOrdersResponse[x][y].OrderID, 10)] = strconv.FormatBool(cancelOrdersResponse[x][y].Result) + } + } + + return resp, err +} + +// GetOrderInfo returns order information based on order ID +func (o *OKCoin) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) { + var resp order.Detail + if assetType != asset.Spot { + return resp, fmt.Errorf("%s %w", assetType, asset.ErrNotSupported) + } + + mOrder, err := o.GetSpotOrder(ctx, &GetSpotOrderRequest{OrderID: orderID}) + if err != nil { + return resp, err + } + + format, err := o.GetPairFormat(assetType, false) + if err != nil { + return resp, err + } + + p, err := currency.NewPairDelimiter(mOrder.InstrumentID, format.Delimiter) + if err != nil { + return resp, err + } + + status, err := order.StringToOrderStatus(mOrder.Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + + side, err := order.StringToOrderSide(mOrder.Side) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + resp = order.Detail{ + Amount: mOrder.Size, + Pair: p, + Exchange: o.Name, + Date: mOrder.Timestamp, + ExecutedAmount: mOrder.FilledSize, + Status: status, + Side: side, + } + return resp, nil +} + +// GetDepositAddress returns a deposit address for a specified currency +func (o *OKCoin) GetDepositAddress(ctx context.Context, c currency.Code, _, _ string) (*deposit.Address, error) { + wallet, err := o.GetAccountDepositAddressForCurrency(ctx, c.Lower().String()) + if err != nil { + return nil, err + } + if len(wallet) == 0 { + return nil, fmt.Errorf("%w for currency %s", + errNoAccountDepositAddress, + c) + } + return &deposit.Address{ + Address: wallet[0].Address, + Tag: wallet[0].Tag, + }, nil +} + +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is +// submitted +func (o *OKCoin) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { + if err := withdrawRequest.Validate(); err != nil { + return nil, err + } + withdrawal, err := o.AccountWithdraw(ctx, + &AccountWithdrawRequest{ + Amount: withdrawRequest.Amount, + Currency: withdrawRequest.Currency.Lower().String(), + Destination: 4, // 1, 2, 3 are all internal + Fee: withdrawRequest.Crypto.FeeAmount, + ToAddress: withdrawRequest.Crypto.Address, + TradePwd: withdrawRequest.TradePassword, + }) + if err != nil { + return nil, err + } + if !withdrawal.Result { + return nil, + fmt.Errorf("could not withdraw currency %s to %s, no error specified", + withdrawRequest.Currency, + withdrawRequest.Crypto.Address) + } + + return &withdraw.ExchangeResponse{ + ID: strconv.FormatInt(withdrawal.WithdrawalID, 10), + }, nil +} + +// WithdrawFiatFunds returns a withdrawal ID when a +// withdrawal is submitted +func (o *OKCoin) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a +// withdrawal is submitted +func (o *OKCoin) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetWithdrawalsHistory returns previous withdrawals data +func (o *OKCoin) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) { + return nil, common.ErrNotYetImplemented +} + +// GetActiveOrders retrieves any orders that are active/open +func (o *OKCoin) GetActiveOrders(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) { + err := req.Validate() + if err != nil { + return nil, err + } + + var resp []order.Detail + for x := range req.Pairs { + var fPair currency.Pair + fPair, err = o.FormatExchangeCurrency(req.Pairs[x], asset.Spot) + if err != nil { + return nil, err + } + var spotOpenOrders []GetSpotOrderResponse + spotOpenOrders, err = o.GetSpotOpenOrders(ctx, + &GetSpotOpenOrdersRequest{ + InstrumentID: fPair.String(), + }) + if err != nil { + return nil, err + } + for i := range spotOpenOrders { + var status order.Status + status, err = order.StringToOrderStatus(spotOpenOrders[i].Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + var side order.Side + side, err = order.StringToOrderSide(spotOpenOrders[i].Side) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + var orderType order.Type + orderType, err = order.StringToOrderType(spotOpenOrders[i].Type) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + resp = append(resp, order.Detail{ + OrderID: spotOpenOrders[i].OrderID, + Price: spotOpenOrders[i].Price, + Amount: spotOpenOrders[i].Size, + Pair: req.Pairs[x], + Exchange: o.Name, + Side: side, + Type: orderType, + ExecutedAmount: spotOpenOrders[i].FilledSize, + Date: spotOpenOrders[i].Timestamp, + Status: status, + }) + } + } + return req.Filter(o.Name, resp), nil +} + +// GetOrderHistory retrieves account order information +// Can Limit response to specific order status +func (o *OKCoin) GetOrderHistory(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) { + err := req.Validate() + if err != nil { + return nil, err + } + + var resp []order.Detail + for x := range req.Pairs { + var fPair currency.Pair + fPair, err = o.FormatExchangeCurrency(req.Pairs[x], asset.Spot) + if err != nil { + return nil, err + } + var spotOrders []GetSpotOrderResponse + spotOrders, err = o.GetSpotOrders(ctx, + &GetSpotOrdersRequest{ + Status: strings.Join([]string{"filled", "cancelled", "failure"}, "|"), + InstrumentID: fPair.String(), + }) + if err != nil { + return nil, err + } + for i := range spotOrders { + var status order.Status + status, err = order.StringToOrderStatus(spotOrders[i].Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + var side order.Side + side, err = order.StringToOrderSide(spotOrders[i].Side) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + var orderType order.Type + orderType, err = order.StringToOrderType(spotOrders[i].Type) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) + } + detail := order.Detail{ + OrderID: spotOrders[i].OrderID, + Price: spotOrders[i].Price, + AverageExecutedPrice: spotOrders[i].PriceAvg, + Amount: spotOrders[i].Size, + ExecutedAmount: spotOrders[i].FilledSize, + RemainingAmount: spotOrders[i].Size - spotOrders[i].FilledSize, + Pair: req.Pairs[x], + Exchange: o.Name, + Side: side, + Type: orderType, + Date: spotOrders[i].Timestamp, + Status: status, + } + detail.InferCostsAndTimes() + resp = append(resp, detail) + } + } + return req.Filter(o.Name, resp), nil +} + +// GetFeeByType returns an estimate of fee based on type of transaction +func (o *OKCoin) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + if feeBuilder == nil { + return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) + } + if !o.AreCredentialsValid(ctx) && // Todo check connection status + feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { + feeBuilder.FeeType = exchange.OfflineTradeFee + } + return o.GetFee(ctx, feeBuilder) +} + +// GetWithdrawCapabilities returns the types of withdrawal methods permitted by the exchange +func (o *OKCoin) GetWithdrawCapabilities() uint32 { + return o.GetWithdrawPermissions() +} + +// AuthenticateWebsocket sends an authentication message to the websocket +func (o *OKCoin) AuthenticateWebsocket(ctx context.Context) error { + return o.WsLogin(ctx) +} + +// ValidateCredentials validates current credentials used for wrapper +// functionality +func (o *OKCoin) ValidateCredentials(ctx context.Context, assetType asset.Item) error { + _, err := o.UpdateAccountInfo(ctx, assetType) + return o.CheckTransientError(err) +} + +// GetHistoricTrades returns historic trade data within the timeframe provided +func (o *OKCoin) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetHistoricCandles returns candles between a time period for a set time interval +func (o *OKCoin) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { + if err := o.ValidateKline(pair, a, interval); err != nil { + return kline.Item{}, err + } + + formattedPair, err := o.FormatExchangeCurrency(pair, a) + if err != nil { + return kline.Item{}, err + } + + req := &GetMarketDataRequest{ + Asset: a, + Start: start.UTC().Format(time.RFC3339), + End: end.UTC().Format(time.RFC3339), + Granularity: o.FormatExchangeKlineInterval(interval), + InstrumentID: formattedPair.String(), + } + + ret := kline.Item{ + Exchange: o.Name, + Pair: pair, + Asset: a, + Interval: interval, + } + ret.Candles, err = o.GetMarketData(ctx, req) + if err != nil { + return kline.Item{}, err + } + + ret.RemoveDuplicates() + ret.RemoveOutsideRange(start, end) + ret.SortCandlesByTimestamp(false) + return ret, nil +} + +// GetHistoricCandlesExtended returns candles between a time period for a set time interval +func (o *OKCoin) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { + if err := o.ValidateKline(pair, a, interval); err != nil { + return kline.Item{}, err + } + + ret := kline.Item{ + Exchange: o.Name, + Pair: pair, + Asset: a, + Interval: interval, + } + + dates, err := kline.CalculateCandleDateRanges(start, end, interval, o.Features.Enabled.Kline.ResultLimit) + if err != nil { + return kline.Item{}, err + } + formattedPair, err := o.FormatExchangeCurrency(pair, a) + if err != nil { + return kline.Item{}, err + } + + for x := range dates.Ranges { + req := &GetMarketDataRequest{ + Asset: a, + Start: dates.Ranges[x].Start.Time.UTC().Format(time.RFC3339), + End: dates.Ranges[x].End.Time.UTC().Format(time.RFC3339), + Granularity: o.FormatExchangeKlineInterval(interval), + InstrumentID: formattedPair.String(), + } + + var candles []kline.Candle + candles, err = o.GetMarketData(ctx, req) + if err != nil { + return kline.Item{}, err + } + ret.Candles = append(ret.Candles, candles...) + } + + dates.SetHasDataFromCandles(ret.Candles) + summary := dates.DataSummary(false) + if len(summary) > 0 { + log.Warnf(log.ExchangeSys, "%v - %v", o.Base.Name, summary) + } + ret.RemoveDuplicates() + ret.RemoveOutsideRange(start, end) + ret.SortCandlesByTimestamp(false) + return ret, nil +} diff --git a/exchanges/okgroup/README.md b/exchanges/okgroup/README.md deleted file mode 100644 index 44e5e797..00000000 --- a/exchanges/okgroup/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# GoCryptoTrader package Okgroup - - - - -[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) -[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/okgroup) -[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) -[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) - - -This OKCoin package is part of the GoCryptoTrader codebase. - -## This is still in active development - -You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). - -Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://gocryptotrader.herokuapp.com/) - -## OKCoin Exchange - -### Current Features - -+ REST Support - -### How to enable - -+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) - -+ Individual package example below: - -```go - // Exchanges will be abstracted out in further updates and examples will be - // supplied then -``` - -### How to do REST public/private calls - -+ If enabled via "configuration".json file the exchange will be added to the -IBotExchange array in the ```go var bot Bot``` and you will only be able to use -the wrapper interface functions for accessing exchange data. View routines.go -for an example of integration usage with GoCryptoTrader. Rudimentary example -below: - -main.go -```go -var o exchange.IBotExchange - -for i := range Bot.Exchanges { - if Bot.Exchanges[i].GetName() == "OKCoin" { - o = Bot.Exchanges[i] - } -} - -// Public calls - wrapper functions - -// Fetches current ticker information -tick, err := o.FetchTicker() -if err != nil { - // Handle error -} - -// Fetches current orderbook information -ob, err := o.FetchOrderbook() -if err != nil { - // Handle error -} - -// Private calls - wrapper functions - make sure your APIKEY and APISECRET are -// set and AuthenticatedAPISupport is set to true - -// Fetches current account information -accountInfo, err := o.GetAccountInfo() -if err != nil { - // Handle error -} -``` - -+ If enabled via individually importing package, rudimentary example below: - -```go -// Public calls - -// Fetches current ticker information -ticker, err := o.GetSpotTicker() -if err != nil { - // Handle error -} - -// Fetches current orderbook information -ob, err := o.GetSpotMarketDepth() -if err != nil { - // Handle error -} - -// Private calls - make sure your APIKEY and APISECRET are set and -// AuthenticatedAPISupport is set to true - -// GetContractPosition returns contract positioning -accountInfo, err := o.GetContractPosition(...) -if err != nil { - // Handle error -} - -// Submits an order and the exchange and returns its tradeID -tradeID, err := o.PlaceContractOrders(...) -if err != nil { - // Handle error -} -``` - -### Please click GoDocs chevron above to view current GoDoc information for this package - -## Contribution - -Please feel free to submit any pull requests or suggest any desired features to be added. - -When submitting a PR, please abide by our coding guidelines: - -+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). -+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. -+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). -+ Pull requests need to be based on and opened against the `master` branch. - -## Donations - - - -If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: - -***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** - diff --git a/exchanges/okgroup/okgroup.go b/exchanges/okgroup/okgroup.go deleted file mode 100644 index 817b5f42..00000000 --- a/exchanges/okgroup/okgroup.go +++ /dev/null @@ -1,842 +0,0 @@ -package okgroup - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "strconv" - "strings" - "time" - - "github.com/google/go-querystring/query" - "github.com/thrasher-corp/gocryptotrader/common/crypto" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/log" -) - -const ( - // OKGroupAPIPath const to help with api url formatting - OKGroupAPIPath = "api/" - // API subsections - okGroupAccountSubsection = "account" - okGroupTokenSubsection = "spot" - okGroupMarginTradingSubsection = "margin" - okGroupFuturesTradingSubSection = "futures" - oKGroupSwapTradingSubSection = "swap" - // OKGroupAccounts common api endpoint - OKGroupAccounts = "accounts" - // OKGroupLedger common api endpoint - OKGroupLedger = "ledger" - // OKGroupOrders common api endpoint - OKGroupOrders = "orders" - // OKGroupBatchOrders common api endpoint - OKGroupBatchOrders = "batch_orders" - // OKGroupCancelOrders common api endpoint - OKGroupCancelOrders = "cancel_orders" - // OKGroupCancelOrder common api endpoint - OKGroupCancelOrder = "cancel_order" - // OKGroupCancelBatchOrders common api endpoint - OKGroupCancelBatchOrders = "cancel_batch_orders" - // OKGroupPendingOrders common api endpoint - OKGroupPendingOrders = "orders_pending" - // OKGroupTrades common api endpoint - OKGroupTrades = "trades" - // OKGroupTicker common api endpoint - OKGroupTicker = "ticker" - // OKGroupInstruments common api endpoint - OKGroupInstruments = "instruments" - // OKGroupLiquidation common api endpoint - OKGroupLiquidation = "liquidation" - // OKGroupMarkPrice common api endpoint - OKGroupMarkPrice = "mark_price" - // OKGroupGetAccountDepositHistory common api endpoint - OKGroupGetAccountDepositHistory = "deposit/history" - // OKGroupGetSpotTransactionDetails common api endpoint - OKGroupGetSpotTransactionDetails = "fills" - // OKGroupGetSpotOrderBook common api endpoint - OKGroupGetSpotOrderBook = "book" - // OKGroupGetSpotMarketData common api endpoint - OKGroupGetSpotMarketData = "candles" - // OKGroupPriceLimit common api endpoint - OKGroupPriceLimit = "price_limit" - // Account based endpoints - okGroupGetAccountCurrencies = "currencies" - okGroupGetAccountWalletInformation = "wallet" - okGroupFundsTransfer = "transfer" - okGroupWithdraw = "withdrawal" - okGroupGetWithdrawalFees = "withdrawal/fee" - okGroupGetWithdrawalHistory = "withdrawal/history" - okGroupGetDepositAddress = "deposit/address" - // Margin based endpoints - okGroupGetMarketAvailability = "availability" - okGroupGetLoanHistory = "borrowed" - okGroupGetLoan = "borrow" - okGroupGetRepayment = "repayment" -) - -// OKGroup is the overaching type across the all of OKCoin's exchange methods -type OKGroup struct { - exchange.Base - ExchangeName string - // Spot and contract market error codes - ErrorCodes map[string]error - // Stores for corresponding variable checks - ContractTypes []string - CurrencyPairsDefaults []string - ContractPosition []string - Types []string - // URLs to be overridden by implementations of OKGroup - APIURL string - APIVersion string - WebsocketURL string -} - -// GetAccountCurrencies returns a list of tradable spot instruments and their properties -func (o *OKGroup) GetAccountCurrencies(ctx context.Context) (resp []GetAccountCurrenciesResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, okGroupGetAccountCurrencies, nil, &resp, true) -} - -// GetAccountWalletInformation returns a list of wallets and their properties -func (o *OKGroup) GetAccountWalletInformation(ctx context.Context, currency string) (resp []WalletInformationResponse, _ error) { - var requestURL string - if currency != "" { - requestURL = fmt.Sprintf("%v/%v", okGroupGetAccountWalletInformation, currency) - } else { - requestURL = okGroupGetAccountWalletInformation - } - - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) -} - -// TransferAccountFunds the transfer of funds between wallet, trading accounts, main account and sub accounts. -func (o *OKGroup) TransferAccountFunds(ctx context.Context, request TransferAccountFundsRequest) (resp TransferAccountFundsResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupAccountSubsection, okGroupFundsTransfer, request, &resp, true) -} - -// AccountWithdraw withdrawal of tokens to OKCoin International or other addresses. -func (o *OKGroup) AccountWithdraw(ctx context.Context, request AccountWithdrawRequest) (resp AccountWithdrawResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupAccountSubsection, okGroupWithdraw, request, &resp, true) -} - -// GetAccountWithdrawalFee retrieves the information about the recommended network transaction fee for withdrawals to digital asset addresses. The higher the fees are, the sooner the confirmations you will get. -func (o *OKGroup) GetAccountWithdrawalFee(ctx context.Context, currency string) (resp []GetAccountWithdrawalFeeResponse, _ error) { - var requestURL string - if currency != "" { - requestURL = fmt.Sprintf("%v?currency=%v", okGroupGetWithdrawalFees, currency) - } else { - requestURL = okGroupGetAccountWalletInformation - } - - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) -} - -// GetAccountWithdrawalHistory retrieves all recent withdrawal records. -func (o *OKGroup) GetAccountWithdrawalHistory(ctx context.Context, currency string) (resp []WithdrawalHistoryResponse, _ error) { - var requestURL string - if currency != "" { - requestURL = fmt.Sprintf("%v/%v", okGroupGetWithdrawalHistory, currency) - } else { - requestURL = okGroupGetWithdrawalHistory - } - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) -} - -// GetAccountBillDetails retrieves the bill details of the wallet. All the information will be paged and sorted in reverse chronological order, -// which means the latest will be at the top. Please refer to the pagination section for additional records after the first page. -// 3 months recent records will be returned at maximum -func (o *OKGroup) GetAccountBillDetails(ctx context.Context, request GetAccountBillDetailsRequest) (resp []GetAccountBillDetailsResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupLedger, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) -} - -// GetAccountDepositAddressForCurrency retrieves the deposit addresses of different tokens, including previously used addresses. -func (o *OKGroup) GetAccountDepositAddressForCurrency(ctx context.Context, currency string) (resp []GetDepositAddressResponse, _ error) { - urlValues := url.Values{} - urlValues.Set("currency", currency) - requestURL := fmt.Sprintf("%v?%v", okGroupGetDepositAddress, urlValues.Encode()) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) -} - -// GetAccountDepositHistory retrieves the deposit history of all tokens.100 recent records will be returned at maximum -func (o *OKGroup) GetAccountDepositHistory(ctx context.Context, currency string) (resp []GetAccountDepositHistoryResponse, _ error) { - var requestURL string - if currency != "" { - requestURL = fmt.Sprintf("%v/%v", OKGroupGetAccountDepositHistory, currency) - } else { - requestURL = OKGroupGetAccountDepositHistory - } - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) -} - -// GetSpotTradingAccounts retrieves the list of assets(only show pairs with balance larger than 0), the balances, amount available/on hold in spot accounts. -func (o *OKGroup) GetSpotTradingAccounts(ctx context.Context) (resp []GetSpotTradingAccountResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, OKGroupAccounts, nil, &resp, true) -} - -// GetSpotTradingAccountForCurrency This endpoint supports getting the balance, amount available/on hold of a token in spot account. -func (o *OKGroup) GetSpotTradingAccountForCurrency(ctx context.Context, currency string) (resp GetSpotTradingAccountResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupAccounts, currency) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, true) -} - -// GetSpotBillDetailsForCurrency This endpoint supports getting the balance, amount available/on hold of a token in spot account. -func (o *OKGroup) GetSpotBillDetailsForCurrency(ctx context.Context, request GetSpotBillDetailsForCurrencyRequest) (resp []GetSpotBillDetailsForCurrencyResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupAccounts, request.Currency, OKGroupLedger, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, true) -} - -// PlaceSpotOrder token trading only supports limit and market orders (more order types will become available in the future). -// You can place an order only if you have enough funds. -// Once your order is placed, the amount will be put on hold. -func (o *OKGroup) PlaceSpotOrder(ctx context.Context, request *PlaceOrderRequest) (resp PlaceOrderResponse, _ error) { - if request.OrderType == "" { - request.OrderType = strconv.Itoa(NormalOrder) - } - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupTokenSubsection, OKGroupOrders, request, &resp, true) -} - -// PlaceMultipleSpotOrders supports placing multiple orders for specific trading pairs -// up to 4 trading pairs, maximum 4 orders for each pair -func (o *OKGroup) PlaceMultipleSpotOrders(ctx context.Context, request []PlaceOrderRequest) (map[string][]PlaceOrderResponse, []error) { - currencyPairOrders := make(map[string]int) - resp := make(map[string][]PlaceOrderResponse) - - for i := range request { - if request[i].OrderType == "" { - request[i].OrderType = strconv.Itoa(NormalOrder) - } - currencyPairOrders[request[i].InstrumentID]++ - } - - if len(currencyPairOrders) > 4 { - return resp, []error{errors.New("up to 4 trading pairs")} - } - for _, orderCount := range currencyPairOrders { - if orderCount > 4 { - return resp, []error{errors.New("maximum 4 orders for each pair")} - } - } - - err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupTokenSubsection, OKGroupBatchOrders, request, &resp, true) - if err != nil { - return resp, []error{err} - } - - var orderErrors []error - for currency, orderResponse := range resp { - for i := range orderResponse { - if !orderResponse[i].Result { - orderErrors = append(orderErrors, fmt.Errorf("order for currency %v failed to be placed", currency)) - } - } - } - - return resp, orderErrors -} - -// CancelSpotOrder Cancelling an unfilled order. -func (o *OKGroup) CancelSpotOrder(ctx context.Context, request CancelSpotOrderRequest) (resp CancelSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupCancelOrders, request.OrderID) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupTokenSubsection, requestURL, request, &resp, true) -} - -// CancelMultipleSpotOrders Cancelling multiple unfilled orders. -func (o *OKGroup) CancelMultipleSpotOrders(ctx context.Context, request CancelMultipleSpotOrdersRequest) (resp map[string][]CancelMultipleSpotOrdersResponse, err error) { - resp = make(map[string][]CancelMultipleSpotOrdersResponse) - if len(request.OrderIDs) > 4 { - return resp, errors.New("maximum 4 order cancellations for each pair") - } - - err = o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupTokenSubsection, OKGroupCancelBatchOrders, []CancelMultipleSpotOrdersRequest{request}, &resp, true) - if err != nil { - return - } - - for currency, orderResponse := range resp { - for i := range orderResponse { - cancellationResponse := CancelMultipleSpotOrdersResponse{ - OrderID: orderResponse[i].OrderID, - Result: orderResponse[i].Result, - ClientOID: orderResponse[i].ClientOID, - } - - if !orderResponse[i].Result { - cancellationResponse.Error = fmt.Errorf("order %v for currency %v failed to be cancelled", orderResponse[i].OrderID, currency) - } - - resp[currency] = append(resp[currency], cancellationResponse) - } - } - - return -} - -// GetSpotOrders List your orders. Cursor pagination is used. -// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetSpotOrders(ctx context.Context, request GetSpotOrdersRequest) (resp []GetSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupOrders, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, true) -} - -// GetSpotOpenOrders List all your current open orders. Cursor pagination is used. -// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetSpotOpenOrders(ctx context.Context, request GetSpotOpenOrdersRequest) (resp []GetSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupPendingOrders, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, true) -} - -// GetSpotOrder Get order details by order ID. -func (o *OKGroup) GetSpotOrder(ctx context.Context, request GetSpotOrderRequest) (resp GetSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v%v", OKGroupOrders, request.OrderID, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, request, &resp, true) -} - -// GetSpotTransactionDetails Get details of the recent filled orders. Cursor pagination is used. -// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetSpotTransactionDetails(ctx context.Context, request GetSpotTransactionDetailsRequest) (resp []GetSpotTransactionDetailsResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupGetSpotTransactionDetails, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) -} - -// GetSpotTokenPairDetails Get market data. This endpoint provides the snapshots of market data and can be used without verifications. -// List trading pairs and get the trading limit, price, and more information of different trading pairs. -func (o *OKGroup) GetSpotTokenPairDetails(ctx context.Context) (resp []GetSpotTokenPairDetailsResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, OKGroupInstruments, nil, &resp, false) -} - -// GetOrderBook Getting the order book of a trading pair. Pagination is not -// supported here. The whole book will be returned for one request. Websocket is -// recommended here. -func (o *OKGroup) GetOrderBook(ctx context.Context, request *GetOrderBookRequest, a asset.Item) (resp *GetOrderBookResponse, _ error) { - var requestType, endpoint string - switch a { - case asset.Spot: - endpoint = OKGroupGetSpotOrderBook - requestType = okGroupTokenSubsection - case asset.Futures: - endpoint = OKGroupGetSpotOrderBook - requestType = "futures" - case asset.PerpetualSwap: - endpoint = "depth" - requestType = "swap" - default: - return resp, errors.New("unhandled asset type") - } - requestURL := fmt.Sprintf("%v/%v/%v%v", - OKGroupInstruments, - request.InstrumentID, - endpoint, - FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, - requestType, - requestURL, - nil, - &resp, - false) -} - -// GetSpotAllTokenPairsInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all trading pairs. -func (o *OKGroup) GetSpotAllTokenPairsInformation(ctx context.Context) (resp []GetSpotTokenPairsInformationResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupInstruments, OKGroupTicker) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) -} - -// GetSpotAllTokenPairsInformationForCurrency Get the last traded price, best bid/ask price, 24 hour trading volume and more info of a currency -func (o *OKGroup) GetSpotAllTokenPairsInformationForCurrency(ctx context.Context, currency string) (resp GetSpotTokenPairsInformationResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v", OKGroupInstruments, currency, OKGroupTicker) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) -} - -// GetSpotFilledOrdersInformation Get the recent 60 transactions of all trading pairs. -// Cursor pagination is used. All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetSpotFilledOrdersInformation(ctx context.Context, request GetSpotFilledOrdersInformationRequest) (resp []GetSpotFilledOrdersInformationResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupTrades, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) -} - -// GetMarketData Get the charts of the trading pairs. Charts are returned in grouped buckets based on requested granularity. -func (o *OKGroup) GetMarketData(ctx context.Context, request *GetMarketDataRequest) (resp GetMarketDataResponse, err error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupGetSpotMarketData, FormatParameters(request)) - var requestType string - switch request.Asset { - case asset.Spot, asset.Margin: - requestType = okGroupTokenSubsection - case asset.Futures: - requestType = okGroupFuturesTradingSubSection - case asset.PerpetualSwap: - requestType = oKGroupSwapTradingSubSection - default: - return nil, errors.New("asset not supported") - } - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, requestType, requestURL, nil, &resp, false) -} - -// GetMarginTradingAccounts List all assets under token margin trading account, including information such as balance, amount on hold and more. -func (o *OKGroup) GetMarginTradingAccounts(ctx context.Context) (resp []GetMarginAccountsResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, OKGroupAccounts, nil, &resp, true) -} - -// GetMarginTradingAccountsForCurrency Get the balance, amount on hold and more useful information. -func (o *OKGroup) GetMarginTradingAccountsForCurrency(ctx context.Context, currency string) (resp GetMarginAccountsResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupAccounts, currency) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// GetMarginBillDetails List all bill details. Pagination is used here. -// before and after cursor arguments should not be confused with before and after in chronological time. -// Most paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetMarginBillDetails(ctx context.Context, request GetMarginBillDetailsRequest) (resp []GetSpotBillDetailsForCurrencyResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupAccounts, request.InstrumentID, OKGroupLedger, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// GetMarginAccountSettings Get all information of the margin trading account, -// including the maximum loan amount, interest rate, and maximum leverage. -func (o *OKGroup) GetMarginAccountSettings(ctx context.Context, currency string) (resp []GetMarginAccountSettingsResponse, _ error) { - var requestURL string - if currency != "" { - requestURL = fmt.Sprintf("%v/%v/%v", OKGroupAccounts, currency, okGroupGetMarketAvailability) - } else { - requestURL = fmt.Sprintf("%v/%v", OKGroupAccounts, okGroupGetMarketAvailability) - } - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// GetMarginLoanHistory Get loan history of the margin trading account. -// Pagination is used here. before and after cursor arguments should not be confused with before and after in chronological time. -// Most paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetMarginLoanHistory(ctx context.Context, request GetMarginLoanHistoryRequest) (resp []GetMarginLoanHistoryResponse, _ error) { - var requestURL string - if len(request.InstrumentID) > 0 { - requestURL = fmt.Sprintf("%v/%v/%v", OKGroupAccounts, request.InstrumentID, okGroupGetLoan) - } else { - requestURL = fmt.Sprintf("%v/%v", OKGroupAccounts, okGroupGetLoan) - } - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// OpenMarginLoan Borrowing tokens in a margin trading account. -func (o *OKGroup) OpenMarginLoan(ctx context.Context, request OpenMarginLoanRequest) (resp OpenMarginLoanResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupAccounts, okGroupGetLoan) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupMarginTradingSubsection, requestURL, request, &resp, true) -} - -// RepayMarginLoan Repaying tokens in a margin trading account. -func (o *OKGroup) RepayMarginLoan(ctx context.Context, request RepayMarginLoanRequest) (resp RepayMarginLoanResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupAccounts, okGroupGetRepayment) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupMarginTradingSubsection, requestURL, request, &resp, true) -} - -// PlaceMarginOrder You can place an order only if you have enough funds. Once your order is placed, the amount will be put on hold. -func (o *OKGroup) PlaceMarginOrder(ctx context.Context, request *PlaceOrderRequest) (resp PlaceOrderResponse, _ error) { - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupMarginTradingSubsection, OKGroupOrders, request, &resp, true) -} - -// PlaceMultipleMarginOrders Place multiple orders for specific trading pairs (up to 4 trading pairs, maximum 4 orders each) -func (o *OKGroup) PlaceMultipleMarginOrders(ctx context.Context, request []PlaceOrderRequest) (map[string][]PlaceOrderResponse, []error) { - currencyPairOrders := make(map[string]int) - resp := make(map[string][]PlaceOrderResponse) - for i := range request { - currencyPairOrders[request[i].InstrumentID]++ - } - if len(currencyPairOrders) > 4 { - return resp, []error{errors.New("up to 4 trading pairs")} - } - for _, orderCount := range currencyPairOrders { - if orderCount > 4 { - return resp, []error{errors.New("maximum 4 orders for each pair")} - } - } - - err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupMarginTradingSubsection, OKGroupBatchOrders, request, &resp, true) - if err != nil { - return resp, []error{err} - } - - var orderErrors []error - for currency, orderResponse := range resp { - for i := range orderResponse { - if !orderResponse[i].Result { - orderErrors = append(orderErrors, fmt.Errorf("order for currency %v failed to be placed", currency)) - } - } - } - - return resp, orderErrors -} - -// CancelMarginOrder Cancelling an unfilled order. -func (o *OKGroup) CancelMarginOrder(ctx context.Context, request CancelSpotOrderRequest) (resp CancelSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v", OKGroupCancelOrders, request.OrderID) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupMarginTradingSubsection, requestURL, request, &resp, true) -} - -// CancelMultipleMarginOrders Cancelling multiple unfilled orders. -func (o *OKGroup) CancelMultipleMarginOrders(ctx context.Context, request CancelMultipleSpotOrdersRequest) (map[string][]CancelMultipleSpotOrdersResponse, []error) { - resp := make(map[string][]CancelMultipleSpotOrdersResponse) - if len(request.OrderIDs) > 4 { - return resp, []error{errors.New("maximum 4 order cancellations for each pair")} - } - - err := o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, okGroupMarginTradingSubsection, OKGroupCancelBatchOrders, []CancelMultipleSpotOrdersRequest{request}, &resp, true) - if err != nil { - return resp, []error{err} - } - - var orderErrors []error - for currency, orderResponse := range resp { - for i := range orderResponse { - if !orderResponse[i].Result { - orderErrors = append(orderErrors, fmt.Errorf("order %v for currency %v failed to be cancelled", orderResponse[i].OrderID, currency)) - } - } - } - - return resp, orderErrors -} - -// GetMarginOrders List your orders. Cursor pagination is used. All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetMarginOrders(ctx context.Context, request GetSpotOrdersRequest) (resp []GetSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupOrders, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// GetMarginOpenOrders List all your current open orders. Cursor pagination is used. All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetMarginOpenOrders(ctx context.Context, request GetSpotOpenOrdersRequest) (resp []GetSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupPendingOrders, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// GetMarginOrder Get order details by order ID. -func (o *OKGroup) GetMarginOrder(ctx context.Context, request GetSpotOrderRequest) (resp GetSpotOrderResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v%v", OKGroupOrders, request.OrderID, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, request, &resp, true) -} - -// GetMarginTransactionDetails Get details of the recent filled orders. Cursor pagination is used. -// All paginated requests return the latest information (newest) as the first page sorted by newest (in chronological time) first. -func (o *OKGroup) GetMarginTransactionDetails(ctx context.Context, request GetSpotTransactionDetailsRequest) (resp []GetSpotTransactionDetailsResponse, _ error) { - requestURL := fmt.Sprintf("%v%v", OKGroupGetSpotTransactionDetails, FormatParameters(request)) - return resp, o.SendHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, okGroupMarginTradingSubsection, requestURL, nil, &resp, true) -} - -// FormatParameters Formats URL parameters, useful for optional parameters due to OKCoin signature check -func FormatParameters(request interface{}) (parameters string) { - v, err := query.Values(request) - if err != nil { - log.Errorf(log.ExchangeSys, "Could not parse %v to URL values. Check that the type has url fields", reflect.TypeOf(request).Name()) - return - } - if urlEncodedValues := v.Encode(); len(urlEncodedValues) > 0 { - parameters = fmt.Sprintf("?%v", urlEncodedValues) - } - return -} - -// GetErrorCode returns an error code -func (o *OKGroup) GetErrorCode(code interface{}) error { - var assertedCode string - - switch d := code.(type) { - case float64: - assertedCode = strconv.FormatFloat(d, 'f', -1, 64) - case string: - assertedCode = d - default: - return errors.New("unusual type returned") - } - - if i, ok := o.ErrorCodes[assertedCode]; ok { - return i - } - return errors.New("unable to find SPOT error code") -} - -// SendHTTPRequest sends an authenticated http request to a desired -// path with a JSON payload (of present) -// URL arguments must be in the request path and not as url.URL values -func (o *OKGroup) SendHTTPRequest(ctx context.Context, ep exchange.URL, httpMethod, requestType, requestPath string, data, result interface{}, authenticated bool) (err error) { - endpoint, err := o.API.Endpoints.GetURL(ep) - if err != nil { - return err - } - - var intermediary json.RawMessage - newRequest := func() (*request.Item, error) { - utcTime := time.Now().UTC().Format(time.RFC3339) - payload := []byte("") - - if data != nil { - payload, err = json.Marshal(data) - if err != nil { - return nil, err - } - } - - path := endpoint + requestType + o.APIVersion + requestPath - headers := make(map[string]string) - headers["Content-Type"] = "application/json" - if authenticated { - var creds *account.Credentials - creds, err = o.GetCredentials(ctx) - if err != nil { - return nil, err - } - signPath := fmt.Sprintf("/%v%v%v%v", OKGroupAPIPath, - requestType, o.APIVersion, requestPath) - - var hmac []byte - hmac, err = crypto.GetHMAC(crypto.HashSHA256, - []byte(utcTime+httpMethod+signPath+string(payload)), - []byte(creds.Secret)) - if err != nil { - return nil, err - } - headers["OK-ACCESS-KEY"] = creds.Key - headers["OK-ACCESS-SIGN"] = crypto.Base64Encode(hmac) - headers["OK-ACCESS-TIMESTAMP"] = utcTime - headers["OK-ACCESS-PASSPHRASE"] = creds.ClientID - } - - return &request.Item{ - Method: strings.ToUpper(httpMethod), - Path: path, - Headers: headers, - Body: bytes.NewBuffer(payload), - Result: &intermediary, - AuthRequest: authenticated, - Verbose: o.Verbose, - HTTPDebugging: o.HTTPDebugging, - HTTPRecording: o.HTTPRecording, - }, nil - } - - err = o.SendPayload(ctx, request.Unset, newRequest) - if err != nil { - return err - } - - type errCapFormat struct { - Error int64 `json:"error_code,omitempty"` - ErrorMessage string `json:"error_message,omitempty"` - Result bool `json:"result,string,omitempty"` - } - errCap := errCapFormat{Result: true} - - err = json.Unmarshal(intermediary, &errCap) - if err == nil { - if errCap.ErrorMessage != "" { - return fmt.Errorf("error: %v", errCap.ErrorMessage) - } - if errCap.Error > 0 { - return fmt.Errorf("sendHTTPRequest error - %s", - o.ErrorCodes[strconv.FormatInt(errCap.Error, 10)]) - } - if !errCap.Result { - return errors.New("unspecified error occurred") - } - } - - return json.Unmarshal(intermediary, result) -} - -// SetCheckVarDefaults sets main variables that will be used in requests because -// api does not return an error if there are misspellings in strings. So better -// to check on this, this end. -func (o *OKGroup) SetCheckVarDefaults() { - o.ContractTypes = []string{"this_week", "next_week", "quarter"} - o.CurrencyPairsDefaults = []string{"btc_usd", "ltc_usd", "eth_usd", "etc_usd", "bch_usd"} - o.Types = []string{"1min", "3min", "5min", "15min", "30min", "1day", "3day", - "1week", "1hour", "2hour", "4hour", "6hour", "12hour"} - o.ContractPosition = []string{"1", "2", "3", "4"} -} - -// GetFee returns an estimate of fee based on type of transaction -func (o *OKGroup) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (fee float64, _ error) { - switch feeBuilder.FeeType { - case exchange.CryptocurrencyTradeFee: - fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount, feeBuilder.IsMaker) - case exchange.CryptocurrencyWithdrawalFee: - withdrawFees, err := o.GetAccountWithdrawalFee(ctx, feeBuilder.FiatCurrency.String()) - if err != nil { - return -1, err - } - for _, withdrawFee := range withdrawFees { - if withdrawFee.Currency == feeBuilder.FiatCurrency.String() { - fee = withdrawFee.MinFee - break - } - } - case exchange.OfflineTradeFee: - fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) - } - if fee < 0 { - fee = 0 - } - - return fee, nil -} - -// getOfflineTradeFee calculates the worst case-scenario trading fee -func getOfflineTradeFee(price, amount float64) float64 { - return 0.0015 * price * amount -} - -func calculateTradingFee(purchasePrice, amount float64, isMaker bool) (fee float64) { - // TODO volume based fees - if isMaker { - fee = 0.0005 - } else { - fee = 0.0015 - } - return fee * amount * purchasePrice -} - -// SetErrorDefaults sets the full error default list -func (o *OKGroup) SetErrorDefaults() { - o.ErrorCodes = map[string]error{ - "0": errors.New("successful"), - "1": errors.New("invalid parameter in url normally"), - "30001": errors.New("request header \"OK_ACCESS_KEY\" cannot be blank"), - "30002": errors.New("request header \"OK_ACCESS_SIGN\" cannot be blank"), - "30003": errors.New("request header \"OK_ACCESS_TIMESTAMP\" cannot be blank"), - "30004": errors.New("request header \"OK_ACCESS_PASSPHRASE\" cannot be blank"), - "30005": errors.New("invalid OK_ACCESS_TIMESTAMP"), - "30006": errors.New("invalid OK_ACCESS_KEY"), - "30007": errors.New("invalid Content_Type, please use \"application/json\" format"), - "30008": errors.New("timestamp request expired"), - "30009": errors.New("system error"), - "30010": errors.New("api validation failed"), - "30011": errors.New("invalid IP"), - "30012": errors.New("invalid authorization"), - "30013": errors.New("invalid sign"), - "30014": errors.New("request too frequent"), - "30015": errors.New("request header \"OK_ACCESS_PASSPHRASE\" incorrect"), - "30016": errors.New("you are using v1 apiKey, please use v1 endpoint. If you would like to use v3 endpoint, please subscribe to v3 apiKey"), - "30017": errors.New("apikey's broker id does not match"), - "30018": errors.New("apikey's domain does not match"), - "30020": errors.New("body cannot be blank"), - "30021": errors.New("json data format error"), - "30023": errors.New("required parameter cannot be blank"), - "30024": errors.New("parameter value error"), - "30025": errors.New("parameter category error"), - "30026": errors.New("requested too frequent; endpoint limit exceeded"), - "30027": errors.New("login failure"), - "30028": errors.New("unauthorized execution"), - "30029": errors.New("account suspended"), - "30030": errors.New("endpoint request failed. Please try again"), - "30031": errors.New("token does not exist"), - "30032": errors.New("pair does not exist"), - "30033": errors.New("exchange domain does not exist"), - "30034": errors.New("exchange ID does not exist"), - "30035": errors.New("trading is not supported in this website"), - "30036": errors.New("no relevant data"), - "30037": errors.New("endpoint is offline or unavailable"), - "30038": errors.New("user does not exist"), - "32001": errors.New("futures account suspended"), - "32002": errors.New("futures account does not exist"), - "32003": errors.New("canceling, please wait"), - "32004": errors.New("you have no unfilled orders"), - "32005": errors.New("max order quantity"), - "32006": errors.New("the order price or trigger price exceeds USD 1 million"), - "32007": errors.New("leverage level must be the same for orders on the same side of the contract"), - "32008": errors.New("max. positions to open (cross margin)"), - "32009": errors.New("max. positions to open (fixed margin)"), - "32010": errors.New("leverage cannot be changed with open positions"), - "32011": errors.New("futures status error"), - "32012": errors.New("futures order update error"), - "32013": errors.New("token type is blank"), - "32014": errors.New("your number of contracts closing is larger than the number of contracts available"), - "32015": errors.New("margin ratio is lower than 100% before opening positions"), - "32016": errors.New("margin ratio is lower than 100% after opening position"), - "32017": errors.New("no BBO"), - "32018": errors.New("the order quantity is less than 1, please try again"), - "32019": errors.New("the order price deviates from the price of the previous minute by more than 3%"), - "32020": errors.New("the price is not in the range of the price limit"), - "32021": errors.New("leverage error"), - "32022": errors.New("this function is not supported in your country or region according to the regulations"), - "32023": errors.New("this account has outstanding loan"), - "32024": errors.New("order cannot be placed during delivery"), - "32025": errors.New("order cannot be placed during settlement"), - "32026": errors.New("your account is restricted from opening positions"), - "32027": errors.New("cancelled over 20 orders"), - "32028": errors.New("account is suspended and liquidated"), - "32029": errors.New("order info does not exist"), - "33001": errors.New("margin account for this pair is not enabled yet"), - "33002": errors.New("margin account for this pair is suspended"), - "33003": errors.New("no loan balance"), - "33004": errors.New("loan amount cannot be smaller than the minimum limit"), - "33005": errors.New("repayment amount must exceed 0"), - "33006": errors.New("loan order not found"), - "33007": errors.New("status not found"), - "33008": errors.New("loan amount cannot exceed the maximum limit"), - "33009": errors.New("user ID is blank"), - "33010": errors.New("you cannot cancel an order during session 2 of call auction"), - "33011": errors.New("no new market data"), - "33012": errors.New("order cancellation failed"), - "33013": errors.New("order placement failed"), - "33014": errors.New("order does not exist"), - "33015": errors.New("exceeded maximum limit"), - "33016": errors.New("margin trading is not open for this token"), - "33017": errors.New("insufficient balance"), - "33018": errors.New("this parameter must be smaller than 1"), - "33020": errors.New("request not supported"), - "33021": errors.New("token and the pair do not match"), - "33022": errors.New("pair and the order do not match"), - "33023": errors.New("you can only place market orders during call auction"), - "33024": errors.New("trading amount too small"), - "33025": errors.New("base token amount is blank"), - "33026": errors.New("transaction completed"), - "33027": errors.New("cancelled order or order cancelling"), - "33028": errors.New("the decimal places of the trading price exceeded the limit"), - "33029": errors.New("the decimal places of the trading size exceeded the limit"), - "34001": errors.New("withdrawal suspended"), - "34002": errors.New("please add a withdrawal address"), - "34003": errors.New("sorry, this token cannot be withdrawn to xx at the moment"), - "34004": errors.New("withdrawal fee is smaller than minimum limit"), - "34005": errors.New("withdrawal fee exceeds the maximum limit"), - "34006": errors.New("withdrawal amount is lower than the minimum limit"), - "34007": errors.New("withdrawal amount exceeds the maximum limit"), - "34008": errors.New("insufficient balance"), - "34009": errors.New("your withdrawal amount exceeds the daily limit"), - "34010": errors.New("transfer amount must be larger than 0"), - "34011": errors.New("conditions not met"), - "34012": errors.New("the minimum withdrawal amount for NEO is 1, and the amount must be an integer"), - "34013": errors.New("please transfer"), - "34014": errors.New("transfer limited"), - "34015": errors.New("subaccount does not exist"), - "34016": errors.New("transfer suspended"), - "34017": errors.New("account suspended"), - "34018": errors.New("incorrect trades password"), - "34019": errors.New("please bind your email before withdrawal"), - "34020": errors.New("please bind your funds password before withdrawal"), - "34021": errors.New("not verified address"), - "34022": errors.New("withdrawals are not available for sub accounts"), - "35001": errors.New("contract subscribing does not exist"), - "35002": errors.New("contract is being settled"), - "35003": errors.New("contract is being paused"), - "35004": errors.New("pending contract settlement"), - "35005": errors.New("perpetual swap trading is not enabled"), - "35008": errors.New("margin ratio too low when placing order"), - "35010": errors.New("closing position size larger than available size"), - "35012": errors.New("placing an order with less than 1 contract"), - "35014": errors.New("order size is not in acceptable range"), - "35015": errors.New("leverage level unavailable"), - "35017": errors.New("changing leverage level"), - "35019": errors.New("order size exceeds limit"), - "35020": errors.New("order price exceeds limit"), - "35021": errors.New("order size exceeds limit of the current tier"), - "35022": errors.New("contract is paused or closed"), - "35030": errors.New("place multiple orders"), - "35031": errors.New("cancel multiple orders"), - "35061": errors.New("invalid instrument_id"), - } -} diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go deleted file mode 100644 index dc81d1c0..00000000 --- a/exchanges/okgroup/okgroup_wrapper.go +++ /dev/null @@ -1,810 +0,0 @@ -package okgroup - -import ( - "context" - "errors" - "fmt" - "strconv" - "strings" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/convert" - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" - "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "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/trade" - "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" -) - -// Note: GoCryptoTrader wrapper funcs currently only support SPOT trades. -// Therefore this OKGroup_Wrapper can be shared between OKCoin and OKCoin. -// When circumstances change, wrapper funcs can be split appropriately - -var errNoAccountDepositAddress = errors.New("no account deposit address") - -// Setup sets user exchange configuration settings -func (o *OKGroup) Setup(exch *config.Exchange) error { - err := exch.Validate() - if err != nil { - return err - } - if !exch.Enabled { - o.SetEnabled(false) - return nil - } - err = o.SetupDefaults(exch) - if err != nil { - return err - } - - wsEndpoint, err := o.API.Endpoints.GetURL(exchange.WebsocketSpot) - if err != nil { - return err - } - err = o.Websocket.Setup(&stream.WebsocketSetup{ - ExchangeConfig: exch, - DefaultURL: wsEndpoint, - RunningURL: wsEndpoint, - Connector: o.WsConnect, - Subscriber: o.Subscribe, - Unsubscriber: o.Unsubscribe, - GenerateSubscriptions: o.GenerateDefaultSubscriptions, - Features: &o.Features.Supports.WebsocketCapabilities, - }) - if err != nil { - return err - } - - return o.Websocket.SetupNewConnection(stream.ConnectionSetup{ - RateLimit: okGroupWsRateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - }) -} - -// FetchOrderbook returns orderbook base on the currency pair -func (o *OKGroup) FetchOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - fPair, err := o.FormatExchangeCurrency(p, assetType) - if err != nil { - return nil, err - } - ob, err := orderbook.Get(o.Name, fPair, assetType) - if err != nil { - return o.UpdateOrderbook(ctx, fPair, assetType) - } - return ob, nil -} - -// UpdateOrderbook updates and returns the orderbook for a currency pair -func (o *OKGroup) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset.Item) (*orderbook.Base, error) { - book := &orderbook.Base{ - Exchange: o.Name, - Pair: p, - Asset: a, - VerifyOrderbook: o.CanVerifyOrderbook, - } - - if a == asset.Index { - return book, errors.New("no orderbooks for index") - } - - fPair, err := o.FormatExchangeCurrency(p, a) - if err != nil { - return book, err - } - - orderbookNew, err := o.GetOrderBook(ctx, - &GetOrderBookRequest{ - InstrumentID: fPair.String(), - Size: 200, - }, a) - if err != nil { - return book, err - } - - book.Bids = make(orderbook.Items, len(orderbookNew.Bids)) - for x := range orderbookNew.Bids { - amount, convErr := strconv.ParseFloat(orderbookNew.Bids[x][1], 64) - if convErr != nil { - return book, err - } - price, convErr := strconv.ParseFloat(orderbookNew.Bids[x][0], 64) - if convErr != nil { - return book, err - } - - var liquidationOrders, orderCount int64 - // Contract specific variables - if len(orderbookNew.Bids[x]) == 4 { - liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Bids[x][2], 10, 64) - if convErr != nil { - return book, err - } - - orderCount, convErr = strconv.ParseInt(orderbookNew.Bids[x][3], 10, 64) - if convErr != nil { - return book, err - } - } - - book.Bids[x] = orderbook.Item{ - Amount: amount, - Price: price, - LiquidationOrders: liquidationOrders, - OrderCount: orderCount, - } - } - - book.Asks = make(orderbook.Items, len(orderbookNew.Asks)) - for x := range orderbookNew.Asks { - amount, convErr := strconv.ParseFloat(orderbookNew.Asks[x][1], 64) - if convErr != nil { - return book, err - } - price, convErr := strconv.ParseFloat(orderbookNew.Asks[x][0], 64) - if convErr != nil { - return book, err - } - - var liquidationOrders, orderCount int64 - // Contract specific variables - if len(orderbookNew.Asks[x]) == 4 { - liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Asks[x][2], 10, 64) - if convErr != nil { - return book, err - } - - orderCount, convErr = strconv.ParseInt(orderbookNew.Asks[x][3], 10, 64) - if convErr != nil { - return book, err - } - } - - book.Asks[x] = orderbook.Item{ - Amount: amount, - Price: price, - LiquidationOrders: liquidationOrders, - OrderCount: orderCount, - } - } - - err = book.Process() - if err != nil { - return book, err - } - - return orderbook.Get(o.Name, fPair, a) -} - -// UpdateAccountInfo retrieves balances for all enabled currencies -func (o *OKGroup) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - currencies, err := o.GetSpotTradingAccounts(ctx) - if err != nil { - return account.Holdings{}, err - } - - var resp account.Holdings - resp.Exchange = o.Name - currencyAccount := account.SubAccount{AssetType: assetType} - - for i := range currencies { - hold, parseErr := strconv.ParseFloat(currencies[i].Hold, 64) - if parseErr != nil { - return resp, parseErr - } - totalValue, parseErr := strconv.ParseFloat(currencies[i].Balance, 64) - if parseErr != nil { - return resp, parseErr - } - currencyAccount.Currencies = append(currencyAccount.Currencies, - account.Balance{ - CurrencyName: currency.NewCode(currencies[i].Currency), - Total: totalValue, - Hold: hold, - Free: totalValue - hold, - }) - } - - resp.Accounts = append(resp.Accounts, currencyAccount) - - creds, err := o.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&resp, creds) - if err != nil { - return resp, err - } - - return resp, nil -} - -// FetchAccountInfo retrieves balances for all enabled currencies -func (o *OKGroup) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - creds, err := o.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - acc, err := account.GetHoldings(o.Name, creds, assetType) - if err != nil { - return o.UpdateAccountInfo(ctx, assetType) - } - return acc, nil -} - -// GetFundingHistory returns funding history, deposits and -// withdrawals -func (o *OKGroup) GetFundingHistory(ctx context.Context) (resp []exchange.FundHistory, err error) { - accountDepositHistory, err := o.GetAccountDepositHistory(ctx, "") - if err != nil { - return - } - for x := range accountDepositHistory { - orderStatus := "" - switch accountDepositHistory[x].Status { - case 0: - orderStatus = "waiting" - case 1: - orderStatus = "confirmation account" - case 2: - orderStatus = "recharge success" - } - - resp = append(resp, exchange.FundHistory{ - Amount: accountDepositHistory[x].Amount, - Currency: accountDepositHistory[x].Currency, - ExchangeName: o.Name, - Status: orderStatus, - Timestamp: accountDepositHistory[x].Timestamp, - TransferID: accountDepositHistory[x].TransactionID, - TransferType: "deposit", - }) - } - accountWithdrawlHistory, err := o.GetAccountWithdrawalHistory(ctx, "") - for i := range accountWithdrawlHistory { - resp = append(resp, exchange.FundHistory{ - Amount: accountWithdrawlHistory[i].Amount, - Currency: accountWithdrawlHistory[i].Currency, - ExchangeName: o.Name, - Status: OrderStatus[accountWithdrawlHistory[i].Status], - Timestamp: accountWithdrawlHistory[i].Timestamp, - TransferID: accountWithdrawlHistory[i].TransactionID, - TransferType: "withdrawal", - }) - } - return resp, err -} - -// SubmitOrder submits a new order -func (o *OKGroup) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { - return nil, err - } - - fpair, err := o.FormatExchangeCurrency(s.Pair, s.AssetType) - if err != nil { - return nil, err - } - - request := PlaceOrderRequest{ - ClientOID: s.ClientID, - InstrumentID: fpair.String(), - Side: s.Side.Lower(), - Type: s.Type.Lower(), - Size: strconv.FormatFloat(s.Amount, 'f', -1, 64), - } - if s.Type == order.Limit { - request.Price = strconv.FormatFloat(s.Price, 'f', -1, 64) - } - - orderResponse, err := o.PlaceSpotOrder(ctx, &request) - if err != nil { - return nil, err - } - - if !orderResponse.Result { - return nil, order.ErrUnableToPlaceOrder - } - return s.DeriveSubmitResponse(orderResponse.OrderID) -} - -// ModifyOrder will allow of changing orderbook placement and limit to -// market conversion -func (o *OKGroup) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// CancelOrder cancels an order by its corresponding ID number -func (o *OKGroup) CancelOrder(ctx context.Context, cancel *order.Cancel) (err error) { - err = cancel.Validate(cancel.StandardCancel()) - if err != nil { - return - } - - orderID, err := strconv.ParseInt(cancel.OrderID, 10, 64) - if err != nil { - return - } - - fpair, err := o.FormatExchangeCurrency(cancel.Pair, - cancel.AssetType) - if err != nil { - return - } - - orderCancellationResponse, err := o.CancelSpotOrder(ctx, - CancelSpotOrderRequest{ - InstrumentID: fpair.String(), - OrderID: orderID, - }) - - if !orderCancellationResponse.Result { - err = fmt.Errorf("order %d failed to be cancelled", - orderCancellationResponse.OrderID) - } - - return -} - -// CancelAllOrders cancels all orders associated with a currency pair -func (o *OKGroup) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { - if err := orderCancellation.Validate(); err != nil { - return order.CancelAllResponse{}, err - } - - orderIDs := strings.Split(orderCancellation.OrderID, ",") - resp := order.CancelAllResponse{} - resp.Status = make(map[string]string) - orderIDNumbers := make([]int64, 0, len(orderIDs)) - for i := range orderIDs { - orderIDNumber, err := strconv.ParseInt(orderIDs[i], 10, 64) - if err != nil { - resp.Status[orderIDs[i]] = err.Error() - continue - } - orderIDNumbers = append(orderIDNumbers, orderIDNumber) - } - - fpair, err := o.FormatExchangeCurrency(orderCancellation.Pair, - orderCancellation.AssetType) - if err != nil { - return resp, err - } - - cancelOrdersResponse, err := o.CancelMultipleSpotOrders(ctx, - CancelMultipleSpotOrdersRequest{ - InstrumentID: fpair.String(), - OrderIDs: orderIDNumbers, - }) - if err != nil { - return resp, err - } - - for x := range cancelOrdersResponse { - for y := range cancelOrdersResponse[x] { - resp.Status[strconv.FormatInt(cancelOrdersResponse[x][y].OrderID, 10)] = strconv.FormatBool(cancelOrdersResponse[x][y].Result) - } - } - - return resp, err -} - -// GetOrderInfo returns order information based on order ID -func (o *OKGroup) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (resp order.Detail, err error) { - if assetType != asset.Spot { - return resp, fmt.Errorf("%s %w", assetType, asset.ErrNotSupported) - } - - mOrder, err := o.GetSpotOrder(ctx, GetSpotOrderRequest{OrderID: orderID}) - if err != nil { - return - } - - format, err := o.GetPairFormat(assetType, false) - if err != nil { - return resp, err - } - - p, err := currency.NewPairDelimiter(mOrder.InstrumentID, format.Delimiter) - if err != nil { - return resp, err - } - - status, err := order.StringToOrderStatus(mOrder.Status) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - - side, err := order.StringToOrderSide(mOrder.Side) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - resp = order.Detail{ - Amount: mOrder.Size, - Pair: p, - Exchange: o.Name, - Date: mOrder.Timestamp, - ExecutedAmount: mOrder.FilledSize, - Status: status, - Side: side, - } - return resp, nil -} - -// GetDepositAddress returns a deposit address for a specified currency -func (o *OKGroup) GetDepositAddress(ctx context.Context, c currency.Code, _, _ string) (*deposit.Address, error) { - wallet, err := o.GetAccountDepositAddressForCurrency(ctx, c.Lower().String()) - if err != nil { - return nil, err - } - if len(wallet) == 0 { - return nil, fmt.Errorf("%w for currency %s", - errNoAccountDepositAddress, - c) - } - return &deposit.Address{ - Address: wallet[0].Address, - Tag: wallet[0].Tag, - }, nil -} - -// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is -// submitted -func (o *OKGroup) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { - if err := withdrawRequest.Validate(); err != nil { - return nil, err - } - withdrawal, err := o.AccountWithdraw(ctx, - AccountWithdrawRequest{ - Amount: withdrawRequest.Amount, - Currency: withdrawRequest.Currency.Lower().String(), - Destination: 4, // 1, 2, 3 are all internal - Fee: withdrawRequest.Crypto.FeeAmount, - ToAddress: withdrawRequest.Crypto.Address, - TradePwd: withdrawRequest.TradePassword, - }) - if err != nil { - return nil, err - } - if !withdrawal.Result { - return nil, - fmt.Errorf("could not withdraw currency %s to %s, no error specified", - withdrawRequest.Currency, - withdrawRequest.Crypto.Address) - } - - return &withdraw.ExchangeResponse{ - ID: strconv.FormatInt(withdrawal.WithdrawalID, 10), - }, nil -} - -// WithdrawFiatFunds returns a withdrawal ID when a -// withdrawal is submitted -func (o *OKGroup) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a -// withdrawal is submitted -func (o *OKGroup) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// GetWithdrawalsHistory returns previous withdrawals data -func (o *OKGroup) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) (resp []exchange.WithdrawalHistory, err error) { - return nil, common.ErrNotYetImplemented -} - -// GetActiveOrders retrieves any orders that are active/open -func (o *OKGroup) GetActiveOrders(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) { - err := req.Validate() - if err != nil { - return nil, err - } - - var resp []order.Detail - for x := range req.Pairs { - var fPair currency.Pair - fPair, err = o.FormatExchangeCurrency(req.Pairs[x], asset.Spot) - if err != nil { - return nil, err - } - var spotOpenOrders []GetSpotOrderResponse - spotOpenOrders, err = o.GetSpotOpenOrders(ctx, - GetSpotOpenOrdersRequest{ - InstrumentID: fPair.String(), - }) - if err != nil { - return nil, err - } - for i := range spotOpenOrders { - var status order.Status - status, err = order.StringToOrderStatus(spotOpenOrders[i].Status) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - var side order.Side - side, err = order.StringToOrderSide(spotOpenOrders[i].Side) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - var orderType order.Type - orderType, err = order.StringToOrderType(spotOpenOrders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - resp = append(resp, order.Detail{ - OrderID: spotOpenOrders[i].OrderID, - Price: spotOpenOrders[i].Price, - Amount: spotOpenOrders[i].Size, - Pair: req.Pairs[x], - Exchange: o.Name, - Side: side, - Type: orderType, - ExecutedAmount: spotOpenOrders[i].FilledSize, - Date: spotOpenOrders[i].Timestamp, - Status: status, - }) - } - } - return req.Filter(o.Name, resp), nil -} - -// GetOrderHistory retrieves account order information -// Can Limit response to specific order status -func (o *OKGroup) GetOrderHistory(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) { - err := req.Validate() - if err != nil { - return nil, err - } - - var resp []order.Detail - for x := range req.Pairs { - var fPair currency.Pair - fPair, err = o.FormatExchangeCurrency(req.Pairs[x], asset.Spot) - if err != nil { - return nil, err - } - var spotOrders []GetSpotOrderResponse - spotOrders, err = o.GetSpotOrders(ctx, - GetSpotOrdersRequest{ - Status: strings.Join([]string{"filled", "cancelled", "failure"}, "|"), - InstrumentID: fPair.String(), - }) - if err != nil { - return nil, err - } - for i := range spotOrders { - var status order.Status - status, err = order.StringToOrderStatus(spotOrders[i].Status) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - var side order.Side - side, err = order.StringToOrderSide(spotOrders[i].Side) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - var orderType order.Type - orderType, err = order.StringToOrderType(spotOrders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", o.Name, err) - } - detail := order.Detail{ - OrderID: spotOrders[i].OrderID, - Price: spotOrders[i].Price, - AverageExecutedPrice: spotOrders[i].PriceAvg, - Amount: spotOrders[i].Size, - ExecutedAmount: spotOrders[i].FilledSize, - RemainingAmount: spotOrders[i].Size - spotOrders[i].FilledSize, - Pair: req.Pairs[x], - Exchange: o.Name, - Side: side, - Type: orderType, - Date: spotOrders[i].Timestamp, - Status: status, - } - detail.InferCostsAndTimes() - resp = append(resp, detail) - } - } - return req.Filter(o.Name, resp), nil -} - -// GetFeeByType returns an estimate of fee based on type of transaction -func (o *OKGroup) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { - if feeBuilder == nil { - return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) - } - if !o.AreCredentialsValid(ctx) && // Todo check connection status - feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { - feeBuilder.FeeType = exchange.OfflineTradeFee - } - return o.GetFee(ctx, feeBuilder) -} - -// GetWithdrawCapabilities returns the types of withdrawal methods permitted by the exchange -func (o *OKGroup) GetWithdrawCapabilities() uint32 { - return o.GetWithdrawPermissions() -} - -// AuthenticateWebsocket sends an authentication message to the websocket -func (o *OKGroup) AuthenticateWebsocket(ctx context.Context) error { - return o.WsLogin(ctx) -} - -// ValidateCredentials validates current credentials used for wrapper -// functionality -func (o *OKGroup) ValidateCredentials(ctx context.Context, assetType asset.Item) error { - _, err := o.UpdateAccountInfo(ctx, assetType) - return o.CheckTransientError(err) -} - -// GetHistoricTrades returns historic trade data within the timeframe provided -func (o *OKGroup) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { - return nil, common.ErrFunctionNotSupported -} - -// GetHistoricCandles returns candles between a time period for a set time interval -func (o *OKGroup) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { - if err := o.ValidateKline(pair, a, interval); err != nil { - return kline.Item{}, err - } - - formattedPair, err := o.FormatExchangeCurrency(pair, a) - if err != nil { - return kline.Item{}, err - } - - req := &GetMarketDataRequest{ - Asset: a, - Start: start.UTC().Format(time.RFC3339), - End: end.UTC().Format(time.RFC3339), - Granularity: o.FormatExchangeKlineInterval(interval), - InstrumentID: formattedPair.String(), - } - - candles, err := o.GetMarketData(ctx, req) - if err != nil { - return kline.Item{}, err - } - - ret := kline.Item{ - Exchange: o.Name, - Pair: pair, - Asset: a, - Interval: interval, - } - - for x := range candles { - t, ok := candles[x].([]interface{}) - if !ok { - return kline.Item{}, errors.New("unable to type asset candle data") - } - if len(t) < 6 { - return kline.Item{}, errors.New("incorrect candles data length") - } - v, ok := t[0].(string) - if !ok { - return kline.Item{}, errors.New("unable to type asset time data") - } - var tempCandle kline.Candle - if tempCandle.Time, err = time.Parse(time.RFC3339, v); err != nil { - return kline.Item{}, err - } - if tempCandle.Open, err = convert.FloatFromString(t[1]); err != nil { - return kline.Item{}, err - } - if tempCandle.High, err = convert.FloatFromString(t[2]); err != nil { - return kline.Item{}, err - } - if tempCandle.Low, err = convert.FloatFromString(t[3]); err != nil { - return kline.Item{}, err - } - if tempCandle.Close, err = convert.FloatFromString(t[4]); err != nil { - return kline.Item{}, err - } - if tempCandle.Volume, err = convert.FloatFromString(t[5]); err != nil { - return kline.Item{}, err - } - ret.Candles = append(ret.Candles, tempCandle) - } - - ret.SortCandlesByTimestamp(false) - return ret, nil -} - -// GetHistoricCandlesExtended returns candles between a time period for a set time interval -func (o *OKGroup) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { - if err := o.ValidateKline(pair, a, interval); err != nil { - return kline.Item{}, err - } - - ret := kline.Item{ - Exchange: o.Name, - Pair: pair, - Asset: a, - Interval: interval, - } - - dates, err := kline.CalculateCandleDateRanges(start, end, interval, o.Features.Enabled.Kline.ResultLimit) - if err != nil { - return kline.Item{}, err - } - formattedPair, err := o.FormatExchangeCurrency(pair, a) - if err != nil { - return kline.Item{}, err - } - - for x := range dates.Ranges { - req := &GetMarketDataRequest{ - Asset: a, - Start: dates.Ranges[x].Start.Time.UTC().Format(time.RFC3339), - End: dates.Ranges[x].End.Time.UTC().Format(time.RFC3339), - Granularity: o.FormatExchangeKlineInterval(interval), - InstrumentID: formattedPair.String(), - } - - var candles GetMarketDataResponse - candles, err = o.GetMarketData(ctx, req) - if err != nil { - return kline.Item{}, err - } - - for i := range candles { - t, ok := candles[i].([]interface{}) - if !ok { - return kline.Item{}, errors.New("unable to type assert candles data") - } - if len(t) < 6 { - return kline.Item{}, errors.New("candle data length invalid") - } - v, ok := t[0].(string) - if !ok { - return kline.Item{}, errors.New("unable to type assert time value") - } - var tempCandle kline.Candle - if tempCandle.Time, err = time.Parse(time.RFC3339, v); err != nil { - return kline.Item{}, err - } - if tempCandle.Open, err = convert.FloatFromString(t[1]); err != nil { - return kline.Item{}, err - } - if tempCandle.High, err = convert.FloatFromString(t[2]); err != nil { - return kline.Item{}, err - } - - if tempCandle.Low, err = convert.FloatFromString(t[3]); err != nil { - return kline.Item{}, err - } - - if tempCandle.Close, err = convert.FloatFromString(t[4]); err != nil { - return kline.Item{}, err - } - - if tempCandle.Volume, err = convert.FloatFromString(t[5]); err != nil { - return kline.Item{}, err - } - ret.Candles = append(ret.Candles, tempCandle) - } - } - - dates.SetHasDataFromCandles(ret.Candles) - summary := dates.DataSummary(false) - if len(summary) > 0 { - log.Warnf(log.ExchangeSys, "%v - %v", o.Base.Name, summary) - } - ret.RemoveDuplicates() - ret.RemoveOutsideRange(start, end) - ret.SortCandlesByTimestamp(false) - return ret, nil -}