exchanges: Remove OKGroup (#1103)

* removes okgroup

* fixes more things

* pointers, string concat, comments + test fixes

* lint

* addresses nits without testing yet
This commit is contained in:
Scott
2022-12-23 10:27:47 +11:00
committed by GitHub
parent 7fedfadcdd
commit d739c66a8a
8 changed files with 2385 additions and 2538 deletions

View File

@@ -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
}

File diff suppressed because one or more lines are too long

View File

@@ -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"),
}
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -1,133 +0,0 @@
# GoCryptoTrader package Okgroup
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/page-logo.png?raw=true" width="350px" height="350px" hspace="70">
[![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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -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"),
}
}

View File

@@ -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
}