package btse import ( "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/request" "github.com/thrasher-/gocryptotrader/exchanges/ticker" log "github.com/thrasher-/gocryptotrader/logger" ) // BTSE is the overarching type across this package type BTSE struct { exchange.Base WebsocketConn *websocket.Conn } const ( btseAPIURL = "https://api.btse.com/v1/restapi" btseAPIVersion = "1" // Public endpoints btseMarkets = "markets" btseTrades = "trades" btseTicker = "ticker" btseStats = "stats" btseTime = "time" // Authenticated endpoints btseAccount = "account" btseOrder = "order" btsePendingOrders = "pending" btseDeleteOrder = "deleteOrder" btseDeleteOrders = "deleteOrders" btseFills = "fills" ) // SetDefaults sets the basic defaults for BTSE func (b *BTSE) SetDefaults() { b.Name = "BTSE" b.Enabled = false b.Verbose = false b.RESTPollingDelay = 10 b.APIWithdrawPermissions = exchange.NoAPIWithdrawalMethods b.RequestCurrencyPairFormat.Delimiter = "-" b.RequestCurrencyPairFormat.Uppercase = true b.ConfigCurrencyPairFormat.Delimiter = "-" b.ConfigCurrencyPairFormat.Uppercase = true b.AssetTypes = []string{ticker.Spot} b.Requester = request.New(b.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = btseAPIURL b.APIUrl = b.APIUrlDefault b.SupportsAutoPairUpdating = true b.SupportsRESTTickerBatching = false b.WebsocketInit() b.Websocket.Functionality = exchange.WebsocketOrderbookSupported | exchange.WebsocketTickerSupported } // Setup takes in the supplied exchange configuration details and sets params func (b *BTSE) Setup(exch config.ExchangeConfig) { if !exch.Enabled { b.SetEnabled(false) } else { b.Enabled = true b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) b.SetHTTPClientTimeout(exch.HTTPTimeout) b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose b.Websocket.SetWsStatusAndConnection(exch.Websocket) b.BaseCurrencies = exch.BaseCurrencies b.AvailablePairs = exch.AvailablePairs b.EnabledPairs = exch.EnabledPairs err := b.SetCurrencyPairFormat() if err != nil { log.Fatal(err) } err = b.SetAssetTypes() if err != nil { log.Fatal(err) } err = b.SetAutoPairDefaults() if err != nil { log.Fatal(err) } err = b.SetAPIURL(exch) if err != nil { log.Fatal(err) } err = b.SetClientProxyAddress(exch.ProxyAddress) if err != nil { log.Fatal(err) } err = b.WebsocketSetup(b.WsConnect, exch.Name, exch.Websocket, btseWebsocket, exch.WebsocketURL) if err != nil { log.Fatal(err) } } } // GetMarkets returns a list of markets available on BTSE func (b *BTSE) GetMarkets() (*Markets, error) { var m Markets return &m, b.SendHTTPRequest(http.MethodGet, btseMarkets, &m) } // GetTrades returns a list of trades for the specified symbol func (b *BTSE) GetTrades(symbol string) (*Trades, error) { var t Trades endpoint := fmt.Sprintf("%s/%s", btseTrades, symbol) return &t, b.SendHTTPRequest(http.MethodGet, endpoint, &t) } // GetTicker returns the ticker for a specified symbol func (b *BTSE) GetTicker(symbol string) (*Ticker, error) { type tickerResponse struct { Price interface{} `json:"price"` Size float64 `json:"size,string"` Bid float64 `json:"bid,string"` Ask float64 `json:"ask,string"` Volume float64 `json:"volume,string"` Time string `json:"time"` } var r tickerResponse endpoint := fmt.Sprintf("%s/%s", btseTicker, symbol) err := b.SendHTTPRequest(http.MethodGet, endpoint, &r) if err != nil { return nil, err } p := strings.Replace(r.Price.(string), ",", "", -1) price, err := strconv.ParseFloat(p, 64) if err != nil { return nil, err } return &Ticker{ Price: price, Size: r.Size, Bid: r.Bid, Ask: r.Ask, Volume: r.Volume, Time: r.Time, }, nil } // GetMarketStatistics gets market statistics for a specificed market func (b *BTSE) GetMarketStatistics(symbol string) (*MarketStatistics, error) { var m MarketStatistics endpoint := fmt.Sprintf("%s/%s", btseStats, symbol) return &m, b.SendHTTPRequest(http.MethodGet, endpoint, &m) } // GetServerTime returns the exchanges server time func (b *BTSE) GetServerTime() (*ServerTime, error) { var s ServerTime return &s, b.SendHTTPRequest(http.MethodGet, btseTime, &s) } // GetAccountBalance returns the users account balance func (b *BTSE) GetAccountBalance() (*AccountBalance, error) { var a AccountBalance return &a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseAccount, nil, &a) } // CreateOrder creates an order func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeInForce, tag string) (*string, error) { req := make(map[string]interface{}) req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) req["price"] = strconv.FormatFloat(price, 'f', -1, 64) req["side"] = side req["type"] = orderType req["product_id"] = symbol if timeInForce != "" { req["time_in_force"] = timeInForce } if tag != "" { req["tag"] = tag } type orderResp struct { ID string `json:"id"` } var r orderResp return &r.ID, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseOrder, req, &r) } // GetOrders returns all pending orders func (b *BTSE) GetOrders(productID string) (*OpenOrders, error) { req := make(map[string]interface{}) if productID != "" { req["product_id"] = productID } var o OpenOrders return &o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, req, &o) } // CancelExistingOrder cancels an order func (b *BTSE) CancelExistingOrder(orderID, productID string) (*CancelOrder, error) { var c CancelOrder req := make(map[string]interface{}) req["order_id"] = orderID req["product_id"] = productID return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrder, req, &c) } // CancelOrders cancels all orders // productID optional. If product ID is sent, all orders of that specified market // will be cancelled. If not specified, all orders of all markets will be cancelled func (b *BTSE) CancelOrders(productID string) (*CancelOrder, error) { var c CancelOrder req := make(map[string]interface{}) if productID != "" { req["product_id"] = productID } return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrders, req, &c) } // GetFills gets all filled orders func (b *BTSE) GetFills(orderID, productID, before, after, limit string) (*FilledOrders, error) { if orderID != "" && productID != "" { return nil, errors.New("orderID and productID cannot co-exist in the same query") } else if orderID == "" && productID == "" { return nil, errors.New("orderID OR productID must be set") } req := make(map[string]interface{}) if orderID != "" { req["order_id"] = orderID } if productID != "" { req["product_id"] = productID } if before != "" { req["before"] = before } if after != "" { req["after"] = after } if limit != "" { req["limit"] = limit } var o FilledOrders return &o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseFills, req, &o) } // SendHTTPRequest sends an HTTP request to the desired endpoint func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}) error { p := fmt.Sprintf("%s/%s", btseAPIURL, endpoint) return b.SendPayload(method, p, nil, nil, &result, false, b.Verbose) } // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the desired endpoint func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[string]interface{}, result interface{}) error { if !b.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } payload, err := common.JSONEncode(req) if err != nil { return errors.New("sendAuthenticatedAPIRequest: unable to JSON request") } headers := make(map[string]string) headers["API-KEY"] = b.APIKey headers["API-PASSPHRASE"] = b.APISecret if len(payload) > 0 { headers["Content-Type"] = "application/json" } p := fmt.Sprintf("%s/%s", btseAPIURL, endpoint) if b.Verbose { log.Debugf("Sending %s request to URL %s with params %s\n", method, p, string(payload)) } return b.SendPayload(method, p, headers, strings.NewReader(string(payload)), &result, true, b.Verbose) } // GetFee returns an estimate of fee based on type of transaction func (b *BTSE) GetFee(feeBuilder exchange.FeeBuilder) (float64, error) { var fee float64 switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: fee = calculateTradingFee(feeBuilder.IsMaker) case exchange.CryptocurrencyWithdrawalFee: if feeBuilder.Pair.Base.Match(currency.BTC) { fee = 0.0005 } else if feeBuilder.Pair.Base.Match(currency.USDT) { fee = 5 } case exchange.InternationalBankDepositFee: fee = getInternationalBankDepositFee(feeBuilder.Amount) case exchange.InternationalBankWithdrawalFee: fee = getInternationalBankWithdrawalFee(feeBuilder.Amount) } return fee, nil } // getInternationalBankDepositFee returns international deposit fee // Only when the initial deposit amount is less than $1000 or equivalent, // BTSE will charge a small fee (0.25% or $3 USD equivalent, whichever is greater). // The small deposit fee is charged in whatever currency it comes in. func getInternationalBankDepositFee(amount float64) float64 { var fee float64 if amount <= 1000 { fee = amount * 0.0025 if fee < 3 { return 3 } } return fee } // getInternationalBankWithdrawalFee returns international withdrawal fee // 0.1% (min25 USD) func getInternationalBankWithdrawalFee(amount float64) float64 { fee := amount * 0.001 if fee < 25 { return 25 } return fee } // calculateTradingFee BTSE has fee tiers, but does not disclose them via API, // so the largest fee has to be assumed func calculateTradingFee(isMaker bool) float64 { fee := 0.00050 if !isMaker { fee = 0.0015 } return fee } func parseOrderTime(timeStr string) time.Time { t, _ := time.Parse("2006-01-02 15:04:04", timeStr) return t }