Files
gocryptotrader/exchanges/bithumb/bithumb.go
Adam 504c2fad6d Feature: Implement funding rates, futures and coin margin (exchange API coverage) (#530)
* ALMOST THERE

* more api wips

* more api thingz

* testing n more api wipz

* more apiz

* more wips

* what is goin on

* more wips

* whip n testing

* testing

* testing

no keys

* remove log

* kraken is broken

ugh

* still broken

* fixing auth funcs + usdtm api docs

* wip

* api stuffs

* whip

* more wips

* whip

* more wip

* api wip n testing

* wip

* wip

* unsaved

* wip n testing

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* whip

* wrapper authenticated functions

* adding asset type and fixing dependencies

* wip

* binance auth wrapper start

* wrapper functionality

* wip

* wip

* wip

* wrapper cancel functions

* order submission for wrappers

* wip

* more error fixing and nits

* websocket beginning n error fix

* wip

* WOW

* glorious n shazzy nits

* useless nits

* wip

* fixing things

* merge stuffs

* crapveyor

* crapveyor rebuild

* probably broke more things than he fixed

* rm lns n other thangs

* hope

* please

* stop it

* done

* ofcourse

* rm vb

* fix lbank

* appveyor please

* float lev

* DONT ASK RYAN FOR HELP EVER

* wip

* wip

* endpoint upgrades continued

* path upgrade

* NeeeNeeeNeeeNeeeNING

* fix stuffs

* fixing time issue

* fixing broken funcs

* glorious nits

* shaz changes

* fixing errors for fundmon

* more error fixing for fundmon

* test running past 30s

* basic changes

* THX AGAIN SHAZBERT

* path system upgrade

* config upgrade

* unsaved stuffs

* broken wip config upgrade

* path system upgrade contd.

* path system upgrade contd

* path upgrade ready for review

* testing verbose removed

* linter stuffs

* appveyor stuffs

* appveyor stuff

* fixed?

* bugfix

* wip

* broken stuff

* fix test

* wierd hack fix

* appveyor pls stop

* error found

* more useless nits

* bitmex err

* broken wip

* broken wip path upgrade change to uint32

* changed url lookups to uint

* WOW

* ready4review

* config fixed HOPEFULLY

* config fix and glorious changes

* efficient way of getting orders and open orders

* binance wrapper logic fixing

* testing, adding tests and fixing lot of errrrrs

* merge master

* appveyor stuffs

* appveyor stuffs

* fmt

* test

* octalLiteral issue fix?

* octalLiteral fix?

* rm vb

* prnt ln to restart

* adding testz

* test fixzzz

* READY FOR REVIEW

* Actually ready now

* FORMATTING

* addressing shazzy n glorious nits

* crapveyor

* rm vb

* small change

* fixing err

* shazbert nits

* review changes

* requested changes

* more requested changes

* noo

* last nit fixes

* restart appveyor

* improving test cov

* Update .golangci.yml

* shazbert changes

* moving pair formatting

* format pair update wip

* path upgrade complete

* error fix

* appveyor linters

* more linters

* remove testexch

* more formatting changes

* changes

* shazbert changes

* checking older requested changes to ensure completion

* wip

* fixing broken code

* error fix

* all fixed

* additional changes

* more changes

* remove commented code

* ftx margin api

* appveyor fixes

* more appveyor issues + test addition

* more appveyor issues + test addition

* remove unnecessary

* testing

* testing, fixing okex api, error fix

* git merge fix

* go sum

* glorious changes and error fix

* rm vb

* more glorious changes and go mod tidy

* fixed now

* okex testing upgrade

* old config migration and batch fetching fix

* added test

* glorious requested changes WIP

* tested and fixed

* go fmted

* go fmt and test fix

* additional funcs and tests for fundingRates

* OKEX tested and fixed

* appveyor fixes

* ineff assign

* 1 glorious change

* error fix

* typo

* shazbert changes

* glorious code changes and path fixing huobi WIP

* adding assetType to accountinfo functions

* fixing panic

* panic fix and updating account info wrappers WIP

* updateaccountinfo updated

* testing WIP binance USDT n Coin Margined and Kraken Futures

* auth functions tested and fixed

* added test

* config reverted

* shazbert and glorious changes

* shazbert and glorious changes

* latest changes and portfolio update

* go fmt change:

* remove commented codes

* improved error checking

* index out of range fix

* rm ln

* critical nit

* glorious latest changes

* appveyor changes

* shazbert change

* easier readability

* latest glorious changes

* shadow dec

* assetstore updated

* last change

* another last change

* merge changes

* go mod tidy

* thrasher requested changes wip

* improving struct layouts

* appveyor go fmt

* remove unnecessary code

* shazbert changes

* small change

* oopsie

* tidy

* configtest reverted

* error fix

* oopsie

* for what

* test patch fix

* insecurities

* fixing tests

* fix config
2021-02-12 16:19:18 +11:00

615 lines
18 KiB
Go

package bithumb
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
const (
apiURL = "https://api.bithumb.com"
noError = "0000"
publicTicker = "/public/ticker/"
publicOrderBook = "/public/orderbook/"
publicTransactionHistory = "/public/transaction_history/"
publicCandleStick = "/public/candlestick/"
privateAccInfo = "/info/account"
privateAccBalance = "/info/balance"
privateWalletAdd = "/info/wallet_address"
privateTicker = "/info/ticker"
privateOrders = "/info/orders"
privateUserTrans = "/info/user_transactions"
privatePlaceTrade = "/trade/place"
privateOrderDetail = "/info/order_detail"
privateCancelTrade = "/trade/cancel"
privateBTCWithdraw = "/trade/btc_withdrawal"
privateKRWDeposit = "/trade/krw_deposit"
privateKRWWithdraw = "/trade/krw_withdrawal"
privateMarketBuy = "/trade/market_buy"
privateMarketSell = "/trade/market_sell"
)
// Bithumb is the overarching type across the Bithumb package
type Bithumb struct {
exchange.Base
}
// GetTradablePairs returns a list of tradable currencies
func (b *Bithumb) GetTradablePairs() ([]string, error) {
result, err := b.GetAllTickers()
if err != nil {
return nil, err
}
var currencies []string
for x := range result {
currencies = append(currencies, x)
}
return currencies, nil
}
// GetTicker returns ticker information
//
// symbol e.g. "btc"
func (b *Bithumb) GetTicker(symbol string) (Ticker, error) {
var response TickerResponse
err := b.SendHTTPRequest(exchange.RestSpot, publicTicker+strings.ToUpper(symbol), &response)
if err != nil {
return response.Data, err
}
if response.Status != noError {
return response.Data, errors.New(response.Message)
}
return response.Data, nil
}
// GetAllTickers returns all ticker information
func (b *Bithumb) GetAllTickers() (map[string]Ticker, error) {
var response TickersResponse
err := b.SendHTTPRequest(exchange.RestSpot, publicTicker+"all", &response)
if err != nil {
return nil, err
}
if response.Status != noError {
return nil, errors.New(response.Message)
}
result := make(map[string]Ticker)
for k, v := range response.Data {
if k == "date" {
continue
}
var newTicker Ticker
err := json.Unmarshal(v, &newTicker)
if err != nil {
return nil, err
}
result[k] = newTicker
}
return result, nil
}
// GetOrderBook returns current orderbook
//
// symbol e.g. "btc"
func (b *Bithumb) GetOrderBook(symbol string) (Orderbook, error) {
response := Orderbook{}
err := b.SendHTTPRequest(exchange.RestSpot, publicOrderBook+strings.ToUpper(symbol), &response)
if err != nil {
return response, err
}
if response.Status != noError {
return response, errors.New(response.Message)
}
return response, nil
}
// GetTransactionHistory returns recent transactions
//
// symbol e.g. "btc"
func (b *Bithumb) GetTransactionHistory(symbol string) (TransactionHistory, error) {
response := TransactionHistory{}
path := publicTransactionHistory +
strings.ToUpper(symbol)
err := b.SendHTTPRequest(exchange.RestSpot, path, &response)
if err != nil {
return response, err
}
if response.Status != noError {
return response, errors.New(response.Message)
}
return response, nil
}
// GetAccountInformation returns account information based on the desired
// order/payment currencies
func (b *Bithumb) GetAccountInformation(orderCurrency, paymentCurrency string) (Account, error) {
var response Account
if orderCurrency == "" {
return response, errors.New("order currency must be set")
}
val := url.Values{}
val.Add("order_currency", orderCurrency)
if paymentCurrency != "" { // optional param, default is KRW
val.Add("payment_currency", paymentCurrency)
}
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateAccInfo, val, &response)
}
// GetAccountBalance returns customer wallet information
func (b *Bithumb) GetAccountBalance(c string) (FullBalance, error) {
var response Balance
var fullBalance = FullBalance{
make(map[string]float64),
make(map[string]float64),
make(map[string]float64),
make(map[string]float64),
make(map[string]float64),
}
vals := url.Values{}
if c != "" {
vals.Set("currency", c)
}
err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateAccBalance, vals, &response)
if err != nil {
return fullBalance, err
}
// Added due to increasing of the usuable currencies on exchange, usually
// without notificatation, so we dont need to update structs later on
for tag, datum := range response.Data {
splitTag := strings.Split(tag, "_")
c := splitTag[len(splitTag)-1]
var val float64
if reflect.TypeOf(datum).String() != "float64" {
val, err = strconv.ParseFloat(datum.(string), 64)
if err != nil {
return fullBalance, err
}
} else {
val = datum.(float64)
}
switch splitTag[0] {
case "available":
fullBalance.Available[c] = val
case "in":
fullBalance.InUse[c] = val
case "total":
fullBalance.Total[c] = val
case "misu":
fullBalance.Misu[c] = val
case "xcoin":
fullBalance.Xcoin[c] = val
default:
return fullBalance, fmt.Errorf("getaccountbalance error tag name %s unhandled",
splitTag)
}
}
return fullBalance, nil
}
// GetWalletAddress returns customer wallet address
//
// currency e.g. btc, ltc or "", will default to btc without currency specified
func (b *Bithumb) GetWalletAddress(currency string) (WalletAddressRes, error) {
response := WalletAddressRes{}
params := url.Values{}
params.Set("currency", strings.ToUpper(currency))
err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateWalletAdd, params, &response)
if err != nil {
return response, err
}
if response.Data.WalletAddress == "" {
return response,
fmt.Errorf("deposit address needs to be created via the Bithumb website before retreival for currency %s",
currency)
}
return response, nil
}
// GetLastTransaction returns customer last transaction
func (b *Bithumb) GetLastTransaction() (LastTransactionTicker, error) {
response := LastTransactionTicker{}
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateTicker, nil, &response)
}
// GetOrders returns order list
//
// orderID: order number registered for purchase/sales
// transactionType: transaction type(bid : purchase, ask : sell)
// count: Value : 1 ~1000 (default : 100)
// after: YYYY-MM-DD hh:mm:ss's UNIX Timestamp
// (2014-11-28 16:40:01 = 1417160401000)
func (b *Bithumb) GetOrders(orderID, transactionType, count, after, currency string) (Orders, error) {
response := Orders{}
params := url.Values{}
if len(orderID) > 0 {
params.Set("order_id", orderID)
}
if len(transactionType) > 0 {
params.Set("type", transactionType)
}
if len(count) > 0 {
params.Set("count", count)
}
if len(after) > 0 {
params.Set("after", after)
}
if len(currency) > 0 {
params.Set("currency", strings.ToUpper(currency))
}
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateOrders, params, &response)
}
// GetUserTransactions returns customer transactions
func (b *Bithumb) GetUserTransactions() (UserTransactions, error) {
response := UserTransactions{}
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateUserTrans, nil, &response)
}
// PlaceTrade executes a trade order
//
// orderCurrency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS
// (default value: BTC)
// transactionType: Transaction type(bid : purchase, ask : sales)
// units: Order quantity
// price: Transaction amount per currency
func (b *Bithumb) PlaceTrade(orderCurrency, transactionType string, units float64, price int64) (OrderPlace, error) {
response := OrderPlace{}
params := url.Values{}
params.Set("order_currency", strings.ToUpper(orderCurrency))
params.Set("Payment_currency", "KRW")
params.Set("type", strings.ToUpper(transactionType))
params.Set("units", strconv.FormatFloat(units, 'f', -1, 64))
params.Set("price", strconv.FormatInt(price, 10))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privatePlaceTrade, params, &response)
}
// ModifyTrade modifies an order already on the exchange books
func (b *Bithumb) ModifyTrade(orderID, orderCurrency, transactionType string, units float64, price int64) (OrderPlace, error) {
response := OrderPlace{}
params := url.Values{}
params.Set("order_currency", strings.ToUpper(orderCurrency))
params.Set("Payment_currency", "KRW")
params.Set("type", strings.ToUpper(transactionType))
params.Set("units", strconv.FormatFloat(units, 'f', -1, 64))
params.Set("price", strconv.FormatInt(price, 10))
params.Set("order_id", orderID)
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privatePlaceTrade, params, &response)
}
// GetOrderDetails returns specific order details
//
// orderID: Order number registered for purchase/sales
// transactionType: Transaction type(bid : purchase, ask : sales)
// currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS
// (default value: BTC)
func (b *Bithumb) GetOrderDetails(orderID, transactionType, currency string) (OrderDetails, error) {
response := OrderDetails{}
params := url.Values{}
params.Set("order_id", strings.ToUpper(orderID))
params.Set("type", transactionType)
params.Set("currency", strings.ToUpper(currency))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateOrderDetail, params, &response)
}
// CancelTrade cancels a customer purchase/sales transaction
// transactionType: Transaction type(bid : purchase, ask : sales)
// orderID: Order number registered for purchase/sales
// currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS
// (default value: BTC)
func (b *Bithumb) CancelTrade(transactionType, orderID, currency string) (ActionStatus, error) {
response := ActionStatus{}
params := url.Values{}
params.Set("order_id", strings.ToUpper(orderID))
params.Set("type", strings.ToUpper(transactionType))
params.Set("currency", strings.ToUpper(currency))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateCancelTrade, nil, &response)
}
// WithdrawCrypto withdraws a customer currency to an address
//
// address: Currency withdrawing address
// destination: Currency withdrawal Destination Tag (when withdraw XRP) OR
// Currency withdrawal Payment Id (when withdraw XMR)
// currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM
// (default value: BTC)
// units: Quantity to withdraw currency
func (b *Bithumb) WithdrawCrypto(address, destination, currency string, units float64) (ActionStatus, error) {
response := ActionStatus{}
params := url.Values{}
params.Set("address", address)
if len(destination) > 0 {
params.Set("destination", destination)
}
params.Set("currency", strings.ToUpper(currency))
params.Set("units", strconv.FormatFloat(units, 'f', -1, 64))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateBTCWithdraw, params, &response)
}
// RequestKRWDepositDetails returns Bithumb banking details for deposit
// information
func (b *Bithumb) RequestKRWDepositDetails() (KRWDeposit, error) {
response := KRWDeposit{}
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateKRWDeposit, nil, &response)
}
// RequestKRWWithdraw allows a customer KRW withdrawal request
//
// bank: Bankcode with bank name e.g. (bankcode)_(bankname)
// account: Withdrawing bank account number
// price: Withdrawing amount
func (b *Bithumb) RequestKRWWithdraw(bank, account string, price int64) (ActionStatus, error) {
response := ActionStatus{}
params := url.Values{}
params.Set("bank", bank)
params.Set("account", account)
params.Set("price", strconv.FormatInt(price, 10))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateKRWWithdraw, params, &response)
}
// MarketBuyOrder initiates a buy order through available order books
//
// currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS
// (default value: BTC)
// units: Order quantity
func (b *Bithumb) MarketBuyOrder(currency string, units float64) (MarketBuy, error) {
response := MarketBuy{}
params := url.Values{}
params.Set("currency", strings.ToUpper(currency))
params.Set("units", strconv.FormatFloat(units, 'f', -1, 64))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateMarketBuy, params, &response)
}
// MarketSellOrder initiates a sell order through available order books
//
// currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS
// (default value: BTC)
// units: Order quantity
func (b *Bithumb) MarketSellOrder(currency string, units float64) (MarketSell, error) {
response := MarketSell{}
params := url.Values{}
params.Set("currency", strings.ToUpper(currency))
params.Set("units", strconv.FormatFloat(units, 'f', -1, 64))
return response,
b.SendAuthenticatedHTTPRequest(exchange.RestSpot, privateMarketSell, params, &response)
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (b *Bithumb) SendHTTPRequest(ep exchange.URL, path string, result interface{}) error {
endpoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
return b.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
})
}
// SendAuthenticatedHTTPRequest sends an authenticated HTTP request to bithumb
func (b *Bithumb) SendAuthenticatedHTTPRequest(ep exchange.URL, path string, params url.Values, result interface{}) error {
if !b.AllowAuthenticatedRequest() {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name)
}
endpoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
if params == nil {
params = url.Values{}
}
n := b.Requester.GetNonceMilli().String()
params.Set("endpoint", path)
payload := params.Encode()
hmacPayload := path + string('\x00') + payload + string('\x00') + n
hmac := crypto.GetHMAC(crypto.HashSHA512,
[]byte(hmacPayload),
[]byte(b.API.Credentials.Secret))
hmacStr := crypto.HexEncodeToString(hmac)
headers := make(map[string]string)
headers["Api-Key"] = b.API.Credentials.Key
headers["Api-Sign"] = crypto.Base64Encode([]byte(hmacStr))
headers["Api-Nonce"] = n
headers["Content-Type"] = "application/x-www-form-urlencoded"
var intermediary json.RawMessage
errCapture := struct {
Status string `json:"status"`
Message string `json:"message"`
}{}
err = b.SendPayload(context.Background(), &request.Item{
Method: http.MethodPost,
Path: endpoint + path,
Headers: headers,
Body: bytes.NewBufferString(payload),
Result: &intermediary,
AuthRequest: true,
NonceEnabled: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
Endpoint: request.Auth})
if err != nil {
return err
}
err = json.Unmarshal(intermediary, &errCapture)
if err == nil {
if errCapture.Status != "" && errCapture.Status != noError {
return fmt.Errorf("sendAuthenticatedAPIRequest error code: %s message:%s",
errCapture.Status,
errCode[errCapture.Status])
}
}
return json.Unmarshal(intermediary, result)
}
// GetFee returns an estimate of fee based on type of transaction
func (b *Bithumb) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
case exchange.CyptocurrencyDepositFee:
fee = getDepositFee(feeBuilder.Pair.Base, feeBuilder.Amount)
case exchange.CryptocurrencyWithdrawalFee:
fee = getWithdrawalFee(feeBuilder.Pair.Base)
case exchange.InternationalBankWithdrawalFee:
fee = getWithdrawalFee(feeBuilder.FiatCurrency)
case exchange.OfflineTradeFee:
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// calculateTradingFee returns fee when performing a trade
func calculateTradingFee(purchasePrice, amount float64) float64 {
return 0.0025 * amount * purchasePrice
}
// getDepositFee returns fee on a currency when depositing small amounts to bithumb
func getDepositFee(c currency.Code, amount float64) float64 {
var fee float64
switch c {
case currency.BTC:
if amount <= 0.005 {
fee = 0.001
}
case currency.LTC:
if amount <= 0.3 {
fee = 0.01
}
case currency.DASH:
if amount <= 0.04 {
fee = 0.01
}
case currency.BCH:
if amount <= 0.03 {
fee = 0.001
}
case currency.ZEC:
if amount <= 0.02 {
fee = 0.001
}
case currency.BTG:
if amount <= 0.15 {
fee = 0.001
}
}
return fee
}
// getWithdrawalFee returns fee on a currency when withdrawing out of bithumb
func getWithdrawalFee(c currency.Code) float64 {
return WithdrawalFees[c]
}
var errCode = map[string]string{
"5100": "Bad Request",
"5200": "Not Member",
"5300": "Invalid Apikey",
"5302": "Method Not Allowed",
"5400": "Database Fail",
"5500": "Invalid Parameter",
"5600": "CUSTOM NOTICE (상황별 에러 메시지 출력) usually means transaction not allowed",
"5900": "Unknown Error",
}
// GetCandleStick returns candle stick data for requested pair
func (b *Bithumb) GetCandleStick(symbol, interval string) (resp OHLCVResponse, err error) {
path := publicCandleStick + symbol + "/" + interval
err = b.SendHTTPRequest(exchange.RestSpot, path, &resp)
return
}