exchanges/bittrex,kucoin: Remove exchange implementation and fix minor test issue (#1403)

* exchanges/Bittrex: Remove exchange implementation

* Kucoin: Fix TestProcessMarketSnapshot after pair removal update

* Kucoin: Fix race due to duplicate setupWS call

Unleash your inner Max Verstappen

* Kucoin: Actually test spot/margin market snapshot replication
This commit is contained in:
Adrian Gallagher
2023-11-22 10:20:09 +11:00
committed by GitHub
parent 86e091028f
commit 0fd433e865
29 changed files with 11 additions and 4152 deletions

View File

@@ -26,7 +26,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| Bithumb | Yes | Yes | NA |
| BitMEX | Yes | Yes | NA |
| Bitstamp | Yes | Yes | No |
| Bittrex | Yes | Yes | NA |
| BTCMarkets | Yes | Yes | NA |
| BTSE | Yes | Yes | NA |
| Bybit | Yes | Yes | NA |

View File

@@ -135,11 +135,11 @@ func TestGenerateReport(t *testing.T) {
},
},
{
Exchange: "Bittrex",
Exchange: "Bitstamp",
Asset: a,
Pair: currency.NewPair(currency.BTC, currency.USD),
Interval: gctkline.OneDay,
Watermark: "BITTREX - SPOT - BTC-USD - 1d",
Watermark: "BITSTAMP - SPOT - BTC-USD - 1d",
Candles: []DetailedCandle{
{
UnixMilli: time.Date(2020, 12, 12, 0, 0, 0, 0, time.UTC).UnixMilli(),

View File

@@ -72,17 +72,6 @@
},
"Disabled": false
},
{
"Name": "Bittrex",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "Bittrex/bittrex.github.io",
"Sha": "fc1ea9c10c48aa82c4dc2c6be74887ef61b5b31b"
}
},
"Disabled": false
},
{
"Name": "Coinut",
"CheckType": "GitHub Sha Check",

View File

@@ -72,17 +72,6 @@
},
"Disabled": false
},
{
"Name": "Bittrex",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "Bittrex/bittrex.github.io",
"Sha": "fc1ea9c10c48aa82c4dc2c6be74887ef61b5b31b"
}
},
"Disabled": false
},
{
"Name": "Coinut",
"CheckType": "GitHub Sha Check",

View File

@@ -1,103 +0,0 @@
{{define "exchanges bittrex" -}}
{{template "header" .}}
## Bittrex Exchange
### Current Features
+ REST Support
### Notes
- Bittrex used to have reversed market names: btc-ltc. The v3 API changed this to the more widely accepted format with first the base pair and then the quote pair: ltc-btc.
- Asset names and market names are not case sensitive.
### 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 b exchange.IBotExchange
for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "Bittrex" {
b = bot.Exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := b.FetchTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := b.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 := b.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := b.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := b.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := b.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := b.Trade(...)
if err != nil {
// Handle error
}
```
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -49,7 +49,6 @@ _b in this context is an `IBotExchange` implemented struct_
| Bithumb | Yes | Yes | No |
| BitMEX | Yes | Yes | Yes |
| Bitstamp | Yes | Yes | No |
| Bittrex | Yes | Yes | No |
| BTCMarkets | Yes | Yes | No |
| BTSE | Yes | Yes | No |
| Bybit | Yes | Yes | Yes |

View File

@@ -27,7 +27,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| Bithumb | Yes | Yes | NA |
| BitMEX | Yes | Yes | NA |
| Bitstamp | Yes | Yes | No |
| Bittrex | Yes | Yes | NA |
| BTCMarkets | Yes | Yes | NA |
| BTSE | Yes | Yes | NA |
| Bybit | Yes | Yes | NA |

View File

@@ -593,7 +593,6 @@ var unsupportedExchangeNames = []string{
"testexch",
"alphapoint",
"bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here
"bittrex", // the api is about to expire in March, and we haven't updated it yet
"itbit", // itbit has no way of retrieving pair data
"btse", // TODO rm once timeout issues resolved
"poloniex", // outdated API // TODO rm once updated

View File

@@ -956,84 +956,6 @@
}
]
},
{
"name": "Bittrex",
"enabled": true,
"verbose": false,
"httpTimeout": 15000000000,
"websocketResponseCheckTimeout": 30000000,
"websocketResponseMaxLimit": 7000000000,
"websocketTrafficTimeout": 30000000000,
"websocketOrderbookBufferLimit": 5,
"baseCurrencies": "USD",
"currencyPairs": {
"requestFormat": {
"uppercase": true,
"delimiter": "-"
},
"configFormat": {
"uppercase": true,
"delimiter": "-"
},
"useGlobalFormat": true,
"assetTypes": [
"spot"
],
"pairs": {
"spot": {
"enabled": "USDT-BTC",
"available": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-AR,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-XLM,USDT-BTC,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,BTC-POWR,ETH-POWR,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAXP,ETH-WAXP,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-DMT,ETH-DMT,USDT-TUSD,USDT-SC,USDT-TRX,BTC-STMX,ETH-STMX,BTC-AID,BTC-NGC,BTC-GTO,USDT-DCR,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MED,BTC-BSV,BTC-IOST,USDT-BSV,ETH-BSV,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-TTC,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-OCEAN,USDT-OCEAN,BTC-BWX,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-LAMB,BTC-STPT,BTC-DAI,ETH-DAI,USDT-DAI,BTC-FNB,BTC-PROM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-BRZ,BTC-TEMCO,BTC-SPIN,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP,BTC-HEDG,BTC-MRPH,BTC-HBAR,ETH-HBAR,USD-HBAR,USDT-HBAR,BTC-PLG,BTC-VET,USDT-VET,BTC-SIX,BTC-WGP,BTC-APM,BTC-FLETA,USD-DCR,BTC-BLTV,BTC-HDAC,BTC-HYC,BTC-LINK,USD-EOS,BTC-APIX,BTC-XTZ,ETH-XTZ,USD-XTZ,USDT-XTZ,BTC-XTP,BTC-XSR,BTC-CTC,USD-ATOM,BTC-IOTA,ETH-LINK,USD-LINK,USDT-LINK,BTC-VRA,BTC-ABBC,BTC-FRSP,BTC-WICC,USDT-WICC,USDT-NMR,USD-DASH,USD-RVN,USD-DAI,BTC-VANY,BTC-BOA,BTC-CPC,BTC-CKB,USDT-CKB,BTC-MOF,USDT-MOF,USD-WAXP,USDT-WAXP,BTC-UPT,BTC-UPUSD,BTC-UPEUR,BTC-CVT,BTC-HBD,BTC-HIVE,USDT-CRO,BTC-SXP,BTC-ELAMA,BTC-STC,BTC-IRIS,USDT-IRIS,USDT-BOA,EUR-BTC,EUR-ETH,EUR-USDT,EUR-BSV,EUR-BCH,EUR-TRX,USDT-APM,USDT-HXRO,BTC-OGN,ETH-OGN,BTC-ALGO,BTC-OXT,USDT-OXT,BTC-ICX,BTC-USDC,ETH-USDC,USD-USDC,USDT-USDC,USDT-UPUSD,USDT-BRZ,BTC-XUC,BTC-MDT,USDT-MDT,BTC-REV,USDT-XUC,USDT-REV,BTC-UCT,USDT-UCT,BTC-YOU,USD-HIVE,USDT-HIVE,USD-ENJ,ETH-ENJ,BTC-HDAO,USDT-HDAO,BTC-DNA,USDT-DNA,USDT-SOLVE,BTC-CNTM,USDT-LBC,BTC-LOON,BTC-TNC,USDT-LOON,USD-ALGO,USDT-ALGO,BTC-UBT,ETH-UBT,BTC-DEP,USDT-DEP,EUR-USD,BTC-CELO,ETH-CELO,USD-CELO,USDT-CELO,USDT-CNTM,BTC-VID,BTC-HNS,ETH-HNS,USDT-HNS,BTC-PHNX,BTC-UTI,USD-SOLVE,BTC-4ART,USDT-4ART,BTC-VLX,USDT-VLX,ETH-MET,ETH-TRAC,USDT-TRAC,BTC-ME,BTC-DAWN,BTC-KDA,USDT-KDA"
}
}
},
"api": {
"authenticatedSupport": false,
"authenticatedWebsocketApiSupport": false,
"endpoints": {
"url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
"urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
"websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API"
},
"credentials": {
"key": "Key",
"secret": "Secret"
},
"credentialsValidator": {
"requiresKey": true,
"requiresSecret": true
}
},
"features": {
"supports": {
"restAPI": true,
"restCapabilities": {
"tickerBatching": true,
"autoPairUpdates": true
},
"websocketAPI": false,
"websocketCapabilities": {}
},
"enabled": {
"autoPairUpdates": true,
"websocketAPI": false
}
},
"bankAccounts": [
{
"enabled": false,
"bankName": "",
"bankAddress": "",
"bankPostalCode": "",
"bankPostalCity": "",
"bankCountry": "",
"accountName": "",
"accountNumber": "",
"swiftCode": "",
"iban": "",
"supportedCurrencies": ""
}
]
},
{
"name": "BTSE",
"enabled": true,

View File

@@ -202,7 +202,6 @@ Yes means supported, No means not yet implemented and NA means protocol unsuppor
| Bithumb | Yes | NA | NA |
| BitMEX | Yes | Yes | NA |
| Bitstamp | Yes | Yes | No |
| Bittrex | Yes | Yes | NA |
| BTCMarkets | Yes | No | NA |
| BTSE | Yes | Yes | NA |
| COINUT | Yes | Yes | NA |
@@ -233,7 +232,6 @@ var Exchanges = []string{
"bithumb",
"bitmex",
"bitstamp",
"bittrex",
"btc markets",
"btse",
"coinbasepro",

View File

@@ -50,7 +50,6 @@ $ ./gctcli withdrawcryptofunds --exchange=binance --currency=USDT --address=TJU9
| Bithumb | No | No | |
| BitMEX | No | No | Supports BTC only |
| Bitstamp | No | No | |
| Bittrex | No | No | NA |
| BTCMarkets | No | No| NA |
| BTSE | No | No | Only through website |
| Bybit | Yes | Yes | |

View File

@@ -73,7 +73,6 @@ A helper tool [cmd/dbseed](../cmd/dbseed/README.md) has been created for assisti
| Bitmex | |
| Bitstamp | Y |
| BTC Markets | Y |
| Bittrex | |
| BTSE | Y |
| Bybit | Y |
| Coinbase Pro | Y |

View File

@@ -34,7 +34,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/bithumb"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitmex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp"
"github.com/thrasher-corp/gocryptotrader/exchanges/bittrex"
"github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets"
"github.com/thrasher-corp/gocryptotrader/exchanges/btse"
"github.com/thrasher-corp/gocryptotrader/exchanges/bybit"
@@ -1010,8 +1009,6 @@ func NewSupportedExchangeByName(name string) (exchange.IBotExchange, error) {
return new(bitmex.Bitmex), nil
case "bitstamp":
return new(bitstamp.Bitstamp), nil
case "bittrex":
return new(bittrex.Bittrex), nil
case "btc markets":
return new(btcmarkets.BTCMarkets), nil
case "btse":

View File

@@ -976,7 +976,7 @@ func (b *Binance) getMultiplier(ctx context.Context, isMaker bool) (float64, err
return multiplier, nil
}
// calculateTradingFee returns the fee for trading any currency on Bittrex
// calculateTradingFee returns the fee for trading any currency on Binance
func calculateTradingFee(purchasePrice, amount, multiplier float64) float64 {
return (multiplier / 100) * purchasePrice * amount
}

View File

@@ -966,7 +966,7 @@ func getOfflineTradeFee(price, amount float64) float64 {
return 0.000750 * price * amount
}
// calculateTradingFee returns the fee for trading any currency on Bittrex
// calculateTradingFee returns the fee for trading any currency on Bitmex
func calculateTradingFee(purchasePrice, amount float64, isMaker bool) float64 {
var fee = 0.000750
if isMaker {

View File

@@ -1,137 +0,0 @@
# GoCryptoTrader package Bittrex
<img src="/common/gctlogo.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/bittrex)
[![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 bittrex 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://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Bittrex Exchange
### Current Features
+ REST Support
### Notes
- Bittrex used to have reversed market names: btc-ltc. The v3 API changed this to the more widely accepted format with first the base pair and then the quote pair: ltc-btc.
- Asset names and market names are not case sensitive.
### 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 b exchange.IBotExchange
for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "Bittrex" {
b = bot.Exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := b.FetchTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := b.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 := b.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := b.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := b.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := b.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := b.Trade(...)
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,488 +0,0 @@
package bittrex
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// Bittrex is the overaching type across the bittrex methods
type Bittrex struct {
exchange.Base
WsSequenceOrders int64
obm *orderbookManager
tickerCache *TickerCache
}
const (
bittrexAPIRestURL = "https://api.bittrex.com/v3"
bittrexAPIDeprecatedURL = "https://bittrex.com/api/v1.1"
// Public endpoints
getMarkets = "/markets"
getMarketSummaries = "/markets/summaries"
getTicker = "/markets/%s/ticker"
getTickers = "/markets/tickers"
getMarketSummary = "/markets/%s/summary"
getMarketTrades = "/markets/%s/trades"
getOrderbook = "/markets/%s/orderbook?depth=%s"
getRecentCandles = "/markets/%s/candles/%s/%s/recent"
getHistoricalCandles = "/markets/%s/candles/%s/%s/historical/%s"
getCurrencies = "/currencies"
// Authenticated endpoints
getBalances = "/balances"
getBalance = "/balances/%s"
getDepositAddress = "/addresses/%s"
depositAddresses = "/addresses/"
getAllOpenOrders = "/orders/open"
getOpenOrders = "/orders/open?marketSymbol=%s"
getOrder = "/orders/%s"
getClosedOrders = "/orders/closed?marketSymbol=%s"
cancelOrder = "/orders/%s"
cancelOpenOrders = "/orders/open"
getClosedWithdrawals = "/withdrawals/closed"
getOpenWithdrawals = "/withdrawals/open"
submitWithdrawal = "/transfers"
getClosedDeposits = "/deposits/closed"
getOpenDeposits = "/deposits/open"
submitOrder = "/orders"
// Other Consts
ratePeriod = time.Minute
rateLimit = 60
orderbookDepth = 500 // ws uses REST snapshots and needs identical depths
)
// GetMarkets is used to get the open and available trading markets at Bittrex
// along with other meta data.
func (b *Bittrex) GetMarkets(ctx context.Context) ([]MarketData, error) {
var resp []MarketData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getMarkets, &resp, nil)
}
// GetCurrencies is used to get all supported currencies at Bittrex
func (b *Bittrex) GetCurrencies(ctx context.Context) ([]CurrencyData, error) {
var resp []CurrencyData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getCurrencies, &resp, nil)
}
// GetTicker sends a public get request and returns current ticker information
// on the supplied currency. Example currency input param "ltc-btc".
func (b *Bittrex) GetTicker(ctx context.Context, marketName string) (TickerData, error) {
var resp TickerData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getTicker, marketName), &resp, nil)
}
// GetTickers returns bittrex tickers
func (b *Bittrex) GetTickers(ctx context.Context) ([]TickerData, error) {
var resp []TickerData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getTickers, &resp, nil)
}
// GetMarketSummaries is used to get the last 24 hour summary of all active
// currencies
func (b *Bittrex) GetMarketSummaries(ctx context.Context) ([]MarketSummaryData, error) {
var resp []MarketSummaryData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getMarketSummaries, &resp, nil)
}
// GetMarketSummary is used to get the last 24 hour summary of all active
// exchanges by currency pair (ltc-btc).
func (b *Bittrex) GetMarketSummary(ctx context.Context, marketName string) (MarketSummaryData, error) {
var resp MarketSummaryData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getMarketSummary, marketName), &resp, nil)
}
// GetOrderbook method returns current order book information by currency and depth.
// "marketSymbol" ie ltc-btc
// "depth" is either 1, 25 or 500. Server side, the depth defaults to 25.
func (b *Bittrex) GetOrderbook(ctx context.Context, marketName string, depth int64) (*OrderbookData, int64, error) {
strDepth := strconv.FormatInt(depth, 10)
var resp OrderbookData
var sequence int64
resultHeader := http.Header{}
err := b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getOrderbook, marketName, strDepth), &resp, &resultHeader)
if err != nil {
return nil, 0, err
}
sequence, err = strconv.ParseInt(resultHeader.Get("sequence"), 10, 64)
if err != nil {
return nil, 0, err
}
return &resp, sequence, nil
}
// GetMarketHistory retrieves the latest trades that have occurred for a specific market
func (b *Bittrex) GetMarketHistory(ctx context.Context, currency string) ([]TradeData, error) {
var resp []TradeData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getMarketTrades, currency), &resp, nil)
}
// Order places an order
func (b *Bittrex) Order(ctx context.Context, marketName, side, orderType string, timeInForce TimeInForce, price, amount, ceiling float64) (OrderData, error) {
req := make(map[string]interface{})
req["marketSymbol"] = marketName
req["direction"] = side
req["type"] = orderType
req["quantity"] = strconv.FormatFloat(amount, 'f', -1, 64)
if orderType == "CEILING_LIMIT" || orderType == "CEILING_MARKET" {
req["ceiling"] = strconv.FormatFloat(ceiling, 'f', -1, 64)
}
if orderType == "LIMIT" {
req["limit"] = strconv.FormatFloat(price, 'f', -1, 64)
}
if timeInForce != "" {
req["timeInForce"] = timeInForce
} else {
req["timeInForce"] = GoodTilCancelled
}
var resp OrderData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, submitOrder, nil, req, &resp, nil)
}
// GetOpenOrders returns all orders that you currently have opened.
// A specific market can be requested for example "ltc-btc"
func (b *Bittrex) GetOpenOrders(ctx context.Context, marketName string) ([]OrderData, int64, error) {
var path string
if marketName == "" || marketName == " " {
path = getAllOpenOrders
} else {
path = fmt.Sprintf(getOpenOrders, marketName)
}
var resp []OrderData
var sequence int64
resultHeader := http.Header{}
err := b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, &resp, &resultHeader)
if err != nil {
return nil, 0, err
}
sequence, err = strconv.ParseInt(resultHeader.Get("sequence"), 10, 64)
if err != nil {
return nil, 0, err
}
return resp, sequence, err
}
// CancelExistingOrder is used to cancel a buy or sell order.
func (b *Bittrex) CancelExistingOrder(ctx context.Context, uuid string) (OrderData, error) {
var resp OrderData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, fmt.Sprintf(cancelOrder, uuid), nil, nil, &resp, nil)
}
// CancelOpenOrders is used to cancel all open orders for a specific market
// Or cancel all orders for all markets if the parameter `markets` is set to ""
func (b *Bittrex) CancelOpenOrders(ctx context.Context, market string) ([]BulkCancelResultData, error) {
var resp []BulkCancelResultData
params := url.Values{}
if market != "" {
params.Set("marketSymbol", market)
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, cancelOpenOrders, params, nil, &resp, nil)
}
// GetRecentCandles retrieves recent candles;
// Interval: MINUTE_1, MINUTE_5, HOUR_1, or DAY_1
// Type: TRADE or MIDPOINT
func (b *Bittrex) GetRecentCandles(ctx context.Context, marketName, candleInterval, candleType string) ([]CandleData, error) {
var resp []CandleData
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getRecentCandles, marketName, candleType, candleInterval), &resp, nil)
}
// GetHistoricalCandles retrieves recent candles
// Type: TRADE or MIDPOINT
func (b *Bittrex) GetHistoricalCandles(ctx context.Context, marketName, candleInterval, candleType string, year, month, day int) ([]CandleData, error) {
var resp []CandleData
var start string
switch candleInterval {
case "MINUTE_1", "MINUTE_5":
// Retrieve full day
start = fmt.Sprintf("%d/%d/%d", year, month, day)
case "HOUR_1":
// Retrieve full month
start = fmt.Sprintf("%d/%d", year, month)
case "DAY_1":
// Retrieve full year
start = fmt.Sprintf("%d", year)
default:
return resp, fmt.Errorf("%w %v", kline.ErrUnsupportedInterval, candleInterval)
}
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getHistoricalCandles, marketName, candleType, candleInterval, start), &resp, nil)
}
// GetBalances is used to retrieve all balances from your account
func (b *Bittrex) GetBalances(ctx context.Context) ([]BalanceData, error) {
var resp []BalanceData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getBalances, nil, nil, &resp, nil)
}
// GetAccountBalanceByCurrency is used to retrieve the balance from your account
// for a specific currency. ie. "btc" or "ltc"
func (b *Bittrex) GetAccountBalanceByCurrency(ctx context.Context, currency string) (BalanceData, error) {
var resp BalanceData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getBalance, currency), nil, nil, &resp, nil)
}
// GetCryptoDepositAddresses is used to retrieve all deposit addresses
func (b *Bittrex) GetCryptoDepositAddresses(ctx context.Context) ([]AddressData, error) {
var resp []AddressData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, depositAddresses, nil, nil, &resp, nil)
}
// GetCryptoDepositAddress is used to retrieve an address for a specific currency
func (b *Bittrex) GetCryptoDepositAddress(ctx context.Context, currency string) (AddressData, error) {
var resp AddressData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getDepositAddress, currency), nil, nil, &resp, nil)
}
// ProvisionNewDepositAddress provisions a new deposit address for a specific currency
func (b *Bittrex) ProvisionNewDepositAddress(ctx context.Context, currency string) (*ProvisionNewAddressData, error) {
req := make(map[string]interface{}, 1)
req["currencySymbol"] = currency
var resp ProvisionNewAddressData
return &resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, depositAddresses, nil, req, &resp, nil)
}
// Withdraw is used to withdraw funds from your account.
func (b *Bittrex) Withdraw(ctx context.Context, currency, paymentID, address string, quantity float64) (WithdrawalData, error) {
req := make(map[string]interface{})
req["currencySymbol"] = currency
req["quantity"] = strconv.FormatFloat(quantity, 'f', -1, 64)
req["cryptoAddress"] = address
if len(paymentID) > 0 {
req["cryptoAddressTag"] = paymentID
}
var resp WithdrawalData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, submitWithdrawal, nil, req, &resp, nil)
}
// GetOrder is used to retrieve a single order by UUID.
func (b *Bittrex) GetOrder(ctx context.Context, uuid string) (OrderData, error) {
var resp OrderData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getOrder, uuid), nil, nil, &resp, nil)
}
// GetOrderHistoryForCurrency is used to retrieve your order history. If marketName
// is omitted it will return the entire order History.
func (b *Bittrex) GetOrderHistoryForCurrency(ctx context.Context, currency string) ([]OrderData, error) {
var resp []OrderData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getClosedOrders, currency), nil, nil, &resp, nil)
}
// GetClosedWithdrawals is used to retrieve your withdrawal history.
func (b *Bittrex) GetClosedWithdrawals(ctx context.Context) ([]WithdrawalData, error) {
var resp []WithdrawalData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedWithdrawals, nil, nil, &resp, nil)
}
// GetClosedWithdrawalsForCurrency is used to retrieve your withdrawal history for the specified currency.
func (b *Bittrex) GetClosedWithdrawalsForCurrency(ctx context.Context, currency string) ([]WithdrawalData, error) {
var resp []WithdrawalData
params := url.Values{}
params.Set("currencySymbol", currency)
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedWithdrawals, params, nil, &resp, nil)
}
// GetOpenWithdrawals is used to retrieve your withdrawal history. If currency
// omitted it will return the entire history
func (b *Bittrex) GetOpenWithdrawals(ctx context.Context) ([]WithdrawalData, error) {
var resp []WithdrawalData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOpenWithdrawals, nil, nil, &resp, nil)
}
// GetClosedDeposits is used to retrieve your deposit history.
func (b *Bittrex) GetClosedDeposits(ctx context.Context) ([]DepositData, error) {
var resp []DepositData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedDeposits, nil, nil, &resp, nil)
}
// GetClosedDepositsForCurrency is used to retrieve your deposit history for the specified currency
func (b *Bittrex) GetClosedDepositsForCurrency(ctx context.Context, currency string) ([]DepositData, error) {
var resp []DepositData
params := url.Values{}
params.Set("currencySymbol", currency)
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedDeposits, params, nil, &resp, nil)
}
// GetClosedDepositsPaginated is used to retrieve your deposit history.
// The maximum page size is 200 and it defaults to 100.
// PreviousPageToken is the unique identifier of the item that the resulting
// query result should end before, in the sort order of the given endpoint. Used
// for traversing a paginated set in the reverse direction.
func (b *Bittrex) GetClosedDepositsPaginated(ctx context.Context, pageSize int, previousPageTokenOptional ...string) ([]DepositData, error) {
var resp []DepositData
params := url.Values{}
params.Set("pageSize", strconv.Itoa(pageSize))
if len(previousPageTokenOptional) > 0 {
params.Set("previousPageToken", previousPageTokenOptional[0])
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedDeposits, params, nil, &resp, nil)
}
// GetOpenDeposits is used to retrieve your open deposits.
func (b *Bittrex) GetOpenDeposits(ctx context.Context) ([]DepositData, error) {
var resp []DepositData
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOpenDeposits, nil, nil, &resp, nil)
}
// GetOpenDepositsForCurrency is used to retrieve your open deposits for the specified currency
func (b *Bittrex) GetOpenDepositsForCurrency(ctx context.Context, currency string) ([]DepositData, error) {
var resp []DepositData
params := url.Values{}
params.Set("currencySymbol", currency)
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOpenDeposits, params, nil, &resp, nil)
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (b *Bittrex) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}, resultHeader *http.Header) error {
endpoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
HeaderResponse: resultHeader,
}
return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) { return item, nil }, request.UnauthenticatedRequest)
}
// SendAuthHTTPRequest sends an authenticated request
func (b *Bittrex) SendAuthHTTPRequest(ctx context.Context, ep exchange.URL, method, action string, params url.Values, data, result interface{}, resultHeader *http.Header) error {
creds, err := b.GetCredentials(ctx)
if err != nil {
return err
}
endpoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
newRequest := func() (*request.Item, error) {
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
path := common.EncodeURLValues(action, params)
var body io.Reader
var hmac, payload []byte
var contentHash string
if data == nil {
payload = []byte("")
} else {
var err error
payload, err = json.Marshal(data)
if err != nil {
return nil, err
}
}
body = bytes.NewBuffer(payload)
hash, err := crypto.GetSHA512(payload)
if err != nil {
return nil, err
}
contentHash = crypto.HexEncodeToString(hash)
sigPayload := ts + endpoint + path + method + contentHash
hmac, err = crypto.GetHMAC(crypto.HashSHA512,
[]byte(sigPayload),
[]byte(creds.Secret))
if err != nil {
return nil, err
}
headers := make(map[string]string)
headers["Api-Key"] = creds.Key
headers["Api-Timestamp"] = ts
headers["Api-Content-Hash"] = contentHash
headers["Api-Signature"] = crypto.HexEncodeToString(hmac)
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
return &request.Item{
Method: method,
Path: endpoint + path,
Headers: headers,
Body: body,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
HeaderResponse: resultHeader,
}, nil
}
return b.SendPayload(ctx, request.Unset, newRequest, request.AuthenticatedRequest)
}
// GetFee returns an estimate of fee based on type of transaction
func (b *Bittrex) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
var err error
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
case exchange.CryptocurrencyWithdrawalFee:
fee, err = b.GetWithdrawalFee(ctx, feeBuilder.Pair.Base)
case exchange.OfflineTradeFee:
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, err
}
// GetWithdrawalFee returns the fee for withdrawing from the exchange
func (b *Bittrex) GetWithdrawalFee(ctx context.Context, c currency.Code) (float64, error) {
var fee float64
currencies, err := b.GetCurrencies(ctx)
if err != nil {
return 0, err
}
for i := range currencies {
if currencies[i].Symbol == c.String() {
fee = currencies[i].TxFee
}
}
return fee, nil
}
// calculateTradingFee returns the fee for trading any currency on Bittrex
func calculateTradingFee(price, amount float64) float64 {
return 0.0025 * price * amount
}

View File

@@ -1,747 +0,0 @@
package bittrex
import (
"context"
"errors"
"log"
"os"
"sync"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// Please supply you own test keys here to run better tests.
const (
apiKey = ""
apiSecret = ""
canManipulateRealOrders = false
currPair = "BTC-USDT"
curr = "BTC"
)
var b = &Bittrex{}
func TestMain(m *testing.M) {
b.SetDefaults()
cfg := config.GetConfig()
err := cfg.LoadConfig("../../testdata/configtest.json", true)
if err != nil {
log.Fatal(err)
}
bConfig, err := cfg.GetExchangeConfig("Bittrex")
if err != nil {
log.Fatal(err)
}
bConfig.API.Credentials.Key = apiKey
bConfig.API.Credentials.Secret = apiSecret
bConfig.API.AuthenticatedSupport = true
err = b.Setup(bConfig)
if err != nil {
log.Fatal(err)
}
if !b.IsEnabled() || !b.API.AuthenticatedSupport ||
b.Verbose || len(b.BaseCurrencies) < 1 {
log.Fatal("Bittrex Setup values not set correctly")
}
var wg sync.WaitGroup
err = b.Start(context.Background(), &wg)
if err != nil {
log.Fatal(err)
}
wg.Wait()
os.Exit(m.Run())
}
func TestGetMarkets(t *testing.T) {
t.Parallel()
_, err := b.GetMarkets(context.Background())
if err != nil {
t.Error(err)
}
}
func TestGetCurrencies(t *testing.T) {
t.Parallel()
_, err := b.GetCurrencies(context.Background())
if err != nil {
t.Error(err)
}
}
func TestGetTicker(t *testing.T) {
t.Parallel()
_, err := b.GetTicker(context.Background(), currPair)
if err != nil {
t.Error(err)
}
}
func TestGetMarketSummaries(t *testing.T) {
t.Parallel()
_, err := b.GetMarketSummaries(context.Background())
if err != nil {
t.Error(err)
}
}
func TestGetMarketSummary(t *testing.T) {
t.Parallel()
_, err := b.GetMarketSummary(context.Background(), currPair)
if err != nil {
t.Error(err)
}
}
func TestGetOrderbook(t *testing.T) {
t.Parallel()
_, _, err := b.GetOrderbook(context.Background(), currPair, 500)
if err != nil {
t.Error(err)
}
}
func TestGetMarketHistory(t *testing.T) {
t.Parallel()
_, err := b.GetMarketHistory(context.Background(), currPair)
if err != nil {
t.Error(err)
}
}
func TestGetRecentCandles(t *testing.T) {
t.Parallel()
_, err := b.GetRecentCandles(context.Background(),
currPair, "HOUR_1", "MIDPOINT")
if err != nil {
t.Error(err)
}
}
func TestGetHistoricalCandles(t *testing.T) {
t.Parallel()
_, err := b.GetHistoricalCandles(context.Background(),
currPair, "MINUTE_5", "MIDPOINT", 2020, 12, 31)
if err != nil {
t.Error(err)
}
_, err = b.GetHistoricalCandles(context.Background(),
currPair, "MINUTE_5", "MIDPOINT", 2020, 12, 32)
if err == nil {
t.Error("invalid date should give an error")
}
}
func TestOrder(t *testing.T) {
t.Parallel()
_, err := b.Order(context.Background(),
currPair, order.Buy.String(), order.Limit.String(), "", 1, 1, 0.0)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetOpenOrders(t *testing.T) {
t.Parallel()
_, _, err := b.GetOpenOrders(context.Background(), "")
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
_, _, err = b.GetOpenOrders(context.Background(), currPair)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestCancelExistingOrder(t *testing.T) {
t.Parallel()
_, err := b.CancelExistingOrder(context.Background(), "invalid-order")
if err == nil {
t.Error("Expected error")
}
}
func TestGetAccountBalances(t *testing.T) {
t.Parallel()
_, err := b.GetBalances(context.Background())
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetAccountBalanceByCurrency(t *testing.T) {
t.Parallel()
_, err := b.GetAccountBalanceByCurrency(context.Background(), curr)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetOrder(t *testing.T) {
t.Parallel()
_, err := b.GetOrder(context.Background(), "0cb4c4e4-bdc7-4e13-8c13-430e587d2cc1")
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
_, err = b.GetOrder(context.Background(), "")
if sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetOrderHistoryForCurrency(t *testing.T) {
t.Parallel()
_, err := b.GetOrderHistoryForCurrency(context.Background(), "")
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
_, err = b.GetOrderHistoryForCurrency(context.Background(), currPair)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetClosedWithdrawals(t *testing.T) {
t.Parallel()
_, err := b.GetClosedWithdrawals(context.Background())
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetClosedWithdrawalsForCurrency(t *testing.T) {
t.Parallel()
_, err := b.GetClosedWithdrawalsForCurrency(context.Background(), curr)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetOpenWithdrawals(t *testing.T) {
t.Parallel()
_, err := b.GetOpenWithdrawals(context.Background())
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetCryptoDepositAddresses(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetCryptoDepositAddresses(context.Background())
if err != nil {
t.Error(err)
}
}
func TestProvisionNewDepositAddress(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.ProvisionNewDepositAddress(context.Background(), currency.XRP.String())
if err != nil {
t.Error(err)
}
}
func TestGetClosedDeposits(t *testing.T) {
t.Parallel()
_, err := b.GetClosedDeposits(context.Background())
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetClosedDepositsForCurrency(t *testing.T) {
t.Parallel()
_, err := b.GetClosedDepositsForCurrency(context.Background(), curr)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetClosedDepositsPaginated(t *testing.T) {
t.Parallel()
_, err := b.GetClosedDepositsPaginated(context.Background(), 100)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetOpenDeposits(t *testing.T) {
t.Parallel()
_, err := b.GetOpenDeposits(context.Background())
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestGetOpenDepositsForCurrency(t *testing.T) {
t.Parallel()
_, err := b.GetOpenDepositsForCurrency(context.Background(), curr)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Error(err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expected error")
}
}
func TestWithdraw(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders)
_, err := b.Withdraw(context.Background(),
curr, "", core.BitcoinDonationAddress, 0.0009)
if err != nil {
t.Error(err)
}
}
func setFeeBuilder() *exchange.FeeBuilder {
return &exchange.FeeBuilder{
Amount: 1,
FeeType: exchange.CryptocurrencyTradeFee,
Pair: currency.NewPair(currency.BTC, currency.LTC),
PurchasePrice: 1,
}
}
// TestGetFeeByTypeOfflineTradeFee logic test
func TestGetFeeByTypeOfflineTradeFee(t *testing.T) {
var feeBuilder = setFeeBuilder()
_, err := b.GetFeeByType(context.Background(), feeBuilder)
if err != nil {
t.Fatal(err)
}
if !sharedtestvalues.AreAPICredentialsSet(b) {
if feeBuilder.FeeType != exchange.OfflineTradeFee {
t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType)
}
} else {
if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee {
t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType)
}
}
}
func TestGetFee(t *testing.T) {
var feeBuilder = setFeeBuilder()
// CryptocurrencyTradeFee Basic
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// CryptocurrencyTradeFee High quantity
feeBuilder = setFeeBuilder()
feeBuilder.Amount = 1000
feeBuilder.PurchasePrice = 1000
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// CryptocurrencyTradeFee IsMaker
feeBuilder = setFeeBuilder()
feeBuilder.IsMaker = true
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// CryptocurrencyTradeFee Negative purchase price
feeBuilder = setFeeBuilder()
feeBuilder.PurchasePrice = -1000
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// CryptocurrencyWithdrawalFee Basic
feeBuilder = setFeeBuilder()
feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// CryptocurrencyDepositFee Basic
feeBuilder = setFeeBuilder()
feeBuilder.FeeType = exchange.CryptocurrencyDepositFee
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// InternationalBankDepositFee Basic
feeBuilder = setFeeBuilder()
feeBuilder.FeeType = exchange.InternationalBankDepositFee
feeBuilder.FiatCurrency = currency.HKD
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
// InternationalBankWithdrawalFee Basic
feeBuilder = setFeeBuilder()
feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee
feeBuilder.FiatCurrency = currency.HKD
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
t.Error(err)
}
}
func TestFormatWithdrawPermissions(t *testing.T) {
expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.NoFiatWithdrawalsText
withdrawPermissions := b.FormatWithdrawPermissions()
if withdrawPermissions != expectedResult {
t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions)
}
}
func TestGetActiveOrders(t *testing.T) {
p, err := currency.NewPairFromString(currPair)
if err != nil {
t.Fatal(err)
}
var getOrdersRequest = order.MultiOrderRequest{
Type: order.AnyType,
Pairs: []currency.Pair{p},
AssetType: asset.Spot,
Side: order.AnySide,
}
getOrdersRequest.Pairs[0].Delimiter = currency.DashDelimiter
_, err = b.GetActiveOrders(context.Background(), &getOrdersRequest)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Errorf("Could not get open orders: %s", err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expecting an error when no keys are set")
}
}
func TestGetOrderHistory(t *testing.T) {
var getOrdersRequest = order.MultiOrderRequest{
Type: order.AnyType,
AssetType: asset.Spot,
Side: order.AnySide,
}
_, err := b.GetOrderHistory(context.Background(), &getOrdersRequest)
if err == nil {
t.Error("Expected: 'At least one currency is required to fetch order history'. received nil")
}
getOrdersRequest.Pairs = []currency.Pair{
currency.NewPair(currency.BTC, currency.USDT),
}
_, err = b.GetOrderHistory(context.Background(), &getOrdersRequest)
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Errorf("Could not get order history: %s", err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expecting an error when no keys are set")
}
}
// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them
// ----------------------------------------------------------------------------------------------------------------------------
func TestSubmitOrder(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
var orderSubmission = &order.Submit{
Exchange: b.GetName(),
Pair: currency.Pair{
Delimiter: currency.DashDelimiter,
Base: currency.BTC,
Quote: currency.LTC,
},
Side: order.Buy,
Type: order.Limit,
Price: 1,
Amount: 1,
ClientID: "meowOrder",
AssetType: asset.Spot,
}
response, err := b.SubmitOrder(context.Background(), orderSubmission)
if sharedtestvalues.AreAPICredentialsSet(b) && (err != nil || response.Status != order.New) {
t.Errorf("Order failed to be placed: %v", err)
} else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expecting an error when no keys are set")
}
}
func TestCancelExchangeOrder(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
currencyPair := currency.NewPair(currency.LTC, currency.BTC)
var orderCancellation = &order.Cancel{
OrderID: "1",
WalletAddress: core.BitcoinDonationAddress,
AccountID: "1",
Pair: currencyPair,
AssetType: asset.Spot,
}
err := b.CancelOrder(context.Background(), orderCancellation)
if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expecting an error when no keys are set")
}
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Errorf("Could not cancel orders: %v", err)
}
}
func TestCancelAllExchangeOrders(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
currencyPair := currency.NewPair(currency.LTC, currency.BTC)
var orderCancellation = &order.Cancel{
OrderID: "1",
WalletAddress: core.BitcoinDonationAddress,
AccountID: "1",
Pair: currencyPair,
AssetType: asset.Spot,
}
resp, err := b.CancelAllOrders(context.Background(), orderCancellation)
if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expecting an error when no keys are set")
}
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Errorf("Could not cancel orders: %v", err)
}
if len(resp.Status) > 0 {
t.Errorf("%v orders failed to cancel", len(resp.Status))
}
}
func TestModifyOrder(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
_, err := b.ModifyOrder(context.Background(),
&order.Modify{AssetType: asset.Spot})
if err == nil {
t.Error("Expected error")
}
}
func TestWithdrawCryptocurrencyFunds(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
withdrawCryptoRequest := withdraw.Request{
Exchange: b.Name,
Amount: -1,
Currency: currency.BTC,
Description: "WITHDRAW IT ALL",
Crypto: withdraw.CryptoRequest{
Address: core.BitcoinDonationAddress,
},
}
_, err := b.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest)
if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil {
t.Error("Expecting an error when no keys are set")
}
if sharedtestvalues.AreAPICredentialsSet(b) && err != nil {
t.Errorf("Withdraw failed to be placed: %v", err)
}
}
func TestWithdrawFiat(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
var withdrawFiatRequest = withdraw.Request{}
_, err := b.WithdrawFiatFunds(context.Background(), &withdrawFiatRequest)
if err != common.ErrFunctionNotSupported {
t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err)
}
}
func TestWithdrawInternationalBank(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders)
var withdrawFiatRequest = withdraw.Request{}
_, err := b.WithdrawFiatFundsToInternationalBank(context.Background(),
&withdrawFiatRequest)
if err != common.ErrFunctionNotSupported {
t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err)
}
}
func TestGetDepositAddress(t *testing.T) {
if sharedtestvalues.AreAPICredentialsSet(b) {
_, err := b.GetDepositAddress(context.Background(), currency.XRP, "", "")
if err != nil {
t.Error(err)
}
} else {
_, err := b.GetDepositAddress(context.Background(), currency.BTC, "", "")
if err == nil {
t.Error("error cannot be nil")
}
}
}
func TestGetRecentTrades(t *testing.T) {
t.Parallel()
currencyPair, err := currency.NewPairFromString(currPair)
if err != nil {
t.Fatal(err)
}
_, err = b.GetRecentTrades(context.Background(), currencyPair, asset.Spot)
if err != nil {
t.Error(err)
}
}
func TestGetHistoricTrades(t *testing.T) {
t.Parallel()
currencyPair, err := currency.NewPairFromString(currPair)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricTrades(context.Background(),
currencyPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now())
if err != nil && err != common.ErrFunctionNotSupported {
t.Fatal(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
t.Parallel()
pair, err := currency.NewPairFromString("btc-usdt")
if err != nil {
t.Fatal(err)
}
start := time.Unix(1546300800, 0)
end := start.AddDate(0, 12, 0)
_, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, start, end)
if err != nil {
t.Fatal(err)
}
end = time.Now()
start = end.AddDate(0, -12, 0)
_, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, start, end)
if err != nil {
t.Fatal(err)
}
start = end.AddDate(0, 0, -30)
_, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneHour, start, end)
if err != nil {
t.Fatal(err)
}
end = time.Now().Add(-kline.OneDay.Duration())
start = end.AddDate(0, 0, -1).Add(time.Minute * 5)
_, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.FiveMin, start, end)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
t.Parallel()
pair, err := currency.NewPairFromString("btc-usdt")
if err != nil {
t.Fatal(err)
}
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err = b.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneDay, start, end)
if !errors.Is(err, common.ErrFunctionNotSupported) {
t.Fatal(err)
}
}
func TestGetTickers(t *testing.T) {
t.Parallel()
_, err := b.GetTickers(context.Background())
if err != nil {
t.Error(err)
}
}
func TestUpdateTickers(t *testing.T) {
t.Parallel()
err := b.UpdateTickers(context.Background(), asset.Spot)
if err != nil {
t.Error(err)
}
err = b.UpdateTickers(context.Background(), asset.Futures)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatal(err)
}
}

View File

@@ -1,305 +0,0 @@
package bittrex
import (
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
)
// CancelOrderRequest holds request data for CancelOrder
type CancelOrderRequest struct {
OrderID int64 `json:"orderId,string"`
}
// TimeInForce defines timeInForce types
type TimeInForce string
// All order status types
const (
GoodTilCancelled TimeInForce = "GOOD_TIL_CANCELLED"
ImmediateOrCancel TimeInForce = "IMMEDIATE_OR_CANCEL"
FillOrKill TimeInForce = "FILL_OR_KILL"
PostOnlyGoodTilCancelled TimeInForce = "POST_ONLY_GOOD_TIL_CANCELLED"
BuyNow TimeInForce = "BUY_NOW"
)
// OrderData holds order data
type OrderData struct {
ID string `json:"id"`
MarketSymbol string `json:"marketSymbol"`
Direction string `json:"direction"`
Type string `json:"type"`
Quantity float64 `json:"quantity,string"`
Limit float64 `json:"limit,string"`
Ceiling float64 `json:"ceiling,string"`
TimeInForce string `json:"timeInForce"`
ClientOrderID string `json:"clientOrderId"`
FillQuantity float64 `json:"fillQuantity,string"`
Commission float64 `json:"commission,string"`
Proceeds float64 `json:"proceeds,string"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ClosedAt time.Time `json:"closedAt"`
OrderToCancel struct {
Type string `json:"type,string"`
ID string `json:"id,string"`
} `json:"orderToCancel"`
}
// BulkCancelResultData holds the result of a bulk cancel action
type BulkCancelResultData struct {
ID string `json:"id"`
StatusCode string `json:"statusCode"`
Result OrderData `json:"result"`
}
// MarketData stores market data
type MarketData struct {
Symbol string `json:"symbol"`
BaseCurrencySymbol string `json:"baseCurrencySymbol"`
QuoteCurrencySymbol string `json:"quoteCurrencySymbol"`
MinTradeSize float64 `json:"minTradeSize,string"`
Precision int32 `json:"precision"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
Notice string `json:"notice"`
ProhibitedIn []string `json:"prohibitedIn"`
}
// TickerData stores ticker data
type TickerData struct {
Symbol string `json:"symbol"`
LastTradeRate float64 `json:"lastTradeRate,string"`
BidRate float64 `json:"bidRate,string"`
AskRate float64 `json:"askRate,string"`
UpdatedAt time.Time `json:"updatedAt"`
}
// TradeData stores trades data
type TradeData struct {
ID string `json:"id"`
ExecutedAt time.Time `json:"executedAt"`
Quantity float64 `json:"quantity,string"`
Rate float64 `json:"rate,string"`
TakerSide string `json:"takerSide"`
}
// MarketSummaryData stores market summary data
type MarketSummaryData struct {
Symbol string `json:"symbol"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Volume float64 `json:"volume,string"`
QuoteVolume float64 `json:"quoteVolume,string"`
PercentChange float64 `json:"percentChange,string"`
UpdatedAt time.Time `json:"updatedAt"`
}
// OrderbookData holds the order book data
type OrderbookData struct {
Bid []OrderbookEntryData `json:"bid"`
Ask []OrderbookEntryData `json:"ask"`
}
// OrderbookEntryData holds an order book entry
type OrderbookEntryData struct {
Quantity float64 `json:"quantity,string"`
Rate float64 `json:"rate,string"`
}
// BalanceData holds balance data
type BalanceData struct {
CurrencySymbol string `json:"currencySymbol"`
Total float64 `json:"total,string"`
Available float64 `json:"available,string"`
UpdatedAt time.Time `json:"updatedAt"`
}
// AddressData holds address data
// Status is REQUESTED or PROVISIONED
type AddressData struct {
Status string `json:"status"`
CurrencySymbol string `json:"currencySymbol"`
CryptoAddress string `json:"cryptoAddress"`
CryptoAddressTag string `json:"cryptoAddressTag"`
}
// ProvisionNewAddressData holds the provision deposit data
// Status is REQUESTED
type ProvisionNewAddressData struct {
Status string `json:"status"`
CurrencySymbol string `json:"currencySymbol"`
}
// CurrencyData holds currency data
// Status is ONLINE or OFFLINE
type CurrencyData struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
CoinType string `json:"coinType"`
Status string `json:"status"`
MinConfirmations int32 `json:"minConfirmations"`
Notice string `json:"notice"`
TxFee float64 `json:"txFee,string"`
LogoURL string `json:"logoUrl"`
ProhibitedIn []string `json:"prohibitedIn"`
}
// WithdrawalData holds withdrawal data
type WithdrawalData struct {
ID string `json:"id"`
CurrencySymbol string `json:"currencySymbol"`
Quantity float64 `json:"quantity,string"`
CryptoAddress string `json:"cryptoAddress"`
CryptoAddressTag string `json:"cryptoAddressTag"`
TxCost float64 `json:"txCost,string"`
TxID string `json:"txId"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
CompletedAt time.Time `json:"completedAt"`
ClientWithdrawalID string `json:"clientWithdrawalId"`
}
// DepositData holds deposit data
type DepositData struct {
ID string `json:"id"`
CurrencySymbol string `json:"currencySymbol"`
Quantity float64 `json:"quantity,string"`
CryptoAddress string `json:"cryptoAddress"`
CryptoAddressTag string `json:"cryptoAddressTag"`
TxID string `json:"txId"`
Confirmations int32 `json:"confirmations"`
UpdatedAt time.Time `json:"updatedAt"`
CompletedAt time.Time `json:"completedAt"`
Status string `json:"status"`
Source string `json:"source"`
}
// CandleData holds candle data
type CandleData struct {
StartsAt time.Time `json:"startsAt"`
Open float64 `json:"open,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Close float64 `json:"close,string"`
Volume float64 `json:"volume,string"`
QuoteVolume float64 `json:"quoteVolume,string"`
}
// WsSignalRHandshakeData holds data for the SignalR websocket wrapper handshake
type WsSignalRHandshakeData struct {
URL string `json:"Url"` // Path to the SignalR endpoint
ConnectionToken string `json:"ConnectionToken"` // Connection token assigned by the server
ConnectionID string `json:"ConnectionId"` // The ID of the connection
KeepAliveTimeout float64 `json:"KeepAliveTimeout"` // Representing the amount of time to wait before sending a keep alive packet over an idle connection
DisconnectTimeout float64 `json:"DisconnectTimeout"` // Represents the amount of time to wait after a connection goes away before raising the disconnect event
ConnectionTimeout float64 `json:"ConnectionTimeout"` // Represents the amount of time to leave a connection open before timing out
TryWebSockets bool `json:"TryWebSockets"` // Whether the server supports websockets
ProtocolVersion string `json:"ProtocolVersion"` // The version of the protocol used for communication
TransportConnectTimeout float64 `json:"TransportConnectTimeout"` // The maximum amount of time the client should try to connect to the server using a given transport
LongPollDelay float64 `json:"LongPollDelay"` // The time to tell the browser to wait before reestablishing a long poll connection after data is sent from the server.
}
// WsEventRequest holds data on websocket requests
type WsEventRequest struct {
Hub string `json:"H"`
Method string `json:"M"`
Arguments interface{} `json:"A"`
InvocationID int64 `json:"I"`
}
// WsEventStatus holds data on the websocket event status
type WsEventStatus struct {
Success bool `json:"Success"`
ErrorCode string `json:"ErrorCode"`
}
// WsEventResponse holds data on the websocket response
type WsEventResponse struct {
C string `json:"C"`
S int `json:"S"`
G string `json:"G"`
Response interface{} `json:"R"`
InvocationID int64 `json:"I,string"`
Message []struct {
Hub string `json:"H"`
Method string `json:"M"`
Arguments []string `json:"A"`
} `json:"M"`
}
// WsSubscriptionResponse holds data on the websocket response
type WsSubscriptionResponse struct {
C string `json:"C"`
S int `json:"S"`
G string `json:"G"`
Response []WsEventStatus `json:"R"`
InvocationID int64 `json:"I,string"`
Message []struct {
Hub string `json:"H"`
Method string `json:"M"`
Arguments []string `json:"A"`
} `json:"M"`
}
// WsAuthResponse holds data on the websocket response
type WsAuthResponse struct {
C string `json:"C"`
S int `json:"S"`
G string `json:"G"`
Response WsEventStatus `json:"R"`
InvocationID int64 `json:"I,string"`
Message []struct {
Hub string `json:"H"`
Method string `json:"M"`
Arguments []string `json:"A"`
} `json:"M"`
}
// OrderbookUpdateMessage holds websocket orderbook update messages
type OrderbookUpdateMessage struct {
MarketSymbol string `json:"marketSymbol"`
Depth int `json:"depth"`
Sequence int64 `json:"sequence"`
BidDeltas []OrderbookEntryData `json:"bidDeltas"`
AskDeltas []OrderbookEntryData `json:"askDeltas"`
}
// OrderUpdateMessage holds websocket order update messages
type OrderUpdateMessage struct {
AccountID string `json:"accountId"`
Sequence int `json:"int,string"`
Delta OrderData `json:"delta"`
}
// WsPendingRequest holds pending requests
type WsPendingRequest struct {
WsEventRequest
ChannelsToSubscribe *[]stream.ChannelSubscription
}
// orderbookManager defines a way of managing and maintaining synchronisation
// across connections and assets.
type orderbookManager struct {
state map[currency.Code]map[currency.Code]map[asset.Item]*update
sync.Mutex
jobs chan job
}
type update struct {
buffer chan *OrderbookUpdateMessage
fetchingBook bool
initialSync bool
needsFetchingBook bool
}
// job defines a synchronisation job that tells a go routine to fetch an
// orderbook via the REST protocol
type job struct {
Pair currency.Pair
}

View File

@@ -1,624 +0,0 @@
package bittrex
import (
"bytes"
"compress/flate"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"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/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"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/log"
)
const (
bittrexAPIWSURL = "wss://socket-v3.bittrex.com/signalr"
bittrexAPIWSNegotiationsURL = "https://socket-v3.bittrex.com/signalr"
bittrexWebsocketTimer = 13 * time.Second
wsTicker = "ticker"
wsOrderbook = "orderbook"
wsMarketSummary = "market_summary"
wsOrders = "order"
wsHeartbeat = "heartbeat"
authenticate = "Authenticate"
subscribe = "subscribe"
unsubscribe = "unsubscribe"
wsRateLimit = 50
wsMessageRateLimit = 60
)
var defaultSpotSubscribedChannels = []string{
// wsHeartbeat,
wsOrderbook,
wsTicker,
wsMarketSummary,
}
var defaultSpotSubscribedChannelsAuth = []string{
wsOrders,
}
// TickerCache holds ticker and market summary data
// in order to combine them when processing data
type TickerCache struct {
MarketSummaries map[string]*MarketSummaryData
Tickers map[string]*TickerData
mu sync.RWMutex
}
// WsConnect connects to a websocket feed
func (b *Bittrex) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(stream.WebsocketNotEnabled)
}
var wsHandshakeData WsSignalRHandshakeData
err := b.WsSignalRHandshake(context.TODO(), &wsHandshakeData)
if err != nil {
return err
}
var dialer websocket.Dialer
endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
params := url.Values{}
params.Set("clientProtocol", "1.5")
params.Set("transport", "webSockets")
params.Set("connectionToken", wsHandshakeData.ConnectionToken)
params.Set("connectionData", "[{name:\"c3\"}]")
params.Set("tid", "10")
path := common.EncodeURLValues("/connect", params)
err = b.Websocket.SetWebsocketURL(endpoint+path, false, false)
if err != nil {
return err
}
err = b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
// Can set up custom ping handler per websocket connection.
b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
MessageType: websocket.PingMessage,
Delay: bittrexWebsocketTimer,
})
// This reader routine is called prior to initiating a subscription for
// efficient processing.
b.Websocket.Wg.Add(1)
go b.wsReadData()
b.setupOrderbookManager()
b.tickerCache = &TickerCache{
MarketSummaries: make(map[string]*MarketSummaryData),
Tickers: make(map[string]*TickerData),
}
if b.IsWebsocketAuthenticationSupported() {
err = b.WsAuth(context.TODO())
if err != nil {
b.Websocket.DataHandler <- err
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
return nil
}
// WsSignalRHandshake requests the SignalR connection token over https
func (b *Bittrex) WsSignalRHandshake(ctx context.Context, result interface{}) error {
endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpotSupplementary)
if err != nil {
return err
}
path := "/negotiate?connectionData=[{name:\"c3\"}]&clientProtocol=1.5"
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
}
return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
return item, nil
}, request.UnauthenticatedRequest)
}
// WsAuth sends an authentication message to receive auth data
// Authentications expire after 10 minutes
func (b *Bittrex) WsAuth(ctx context.Context) error {
creds, err := b.GetCredentials(ctx)
if err != nil {
return err
}
// [apiKey, timestamp in ms, random uuid, signed payload]
apiKey := creds.Key
randomContent, err := uuid.NewV4()
if err != nil {
return err
}
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
hmac, err := crypto.GetHMAC(
crypto.HashSHA512,
[]byte(timestamp+randomContent.String()),
[]byte(creds.Secret),
)
if err != nil {
return err
}
signature := crypto.HexEncodeToString(hmac)
req := WsEventRequest{
Hub: "c3",
Method: authenticate,
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
}
arguments := make([]string, 0)
arguments = append(arguments, apiKey, timestamp, randomContent.String(), signature)
req.Arguments = arguments
requestString, err := json.Marshal(req)
if err != nil {
return err
}
if b.Verbose {
log.Debugf(log.WebsocketMgr, "%s Sending JSON message - %s\n", b.Name, requestString)
}
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
if err != nil {
return err
}
var response WsAuthResponse
err = json.Unmarshal(respRaw, &response)
if err != nil {
log.Warnf(log.WebsocketMgr, "%s - Cannot unmarshal into WsAuthResponse (%s)\n", b.Name, string(respRaw))
return err
}
if !response.Response.Success {
log.Warnf(log.WebsocketMgr, "%s - Unable to authenticate (%s)", b.Name, response.Response.ErrorCode)
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
return nil
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
// handled by ManageSubscriptions()
func (b *Bittrex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
channels := defaultSpotSubscribedChannels
if b.IsWebsocketAuthenticationSupported() {
channels = append(channels, defaultSpotSubscribedChannelsAuth...)
}
for i := range pairs {
pair, err := b.FormatExchangeCurrency(pairs[i], asset.Spot)
if err != nil {
return nil, err
}
for y := range channels {
var channel string
switch channels[y] {
case wsOrderbook:
channel = channels[y] + "_" + pair.String() + "_" + strconv.FormatInt(orderbookDepth, 10)
case wsTicker:
channel = channels[y] + "_" + pair.String()
case wsMarketSummary:
channel = channels[y] + "_" + pair.String()
default:
channel = channels[y]
}
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channel,
Currency: pair,
Asset: asset.Spot,
})
}
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *Bittrex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var x int
var errs error
for x = 0; x+wsMessageRateLimit < len(channelsToSubscribe); x += wsMessageRateLimit {
err := b.subscribeSlice(channelsToSubscribe[x : x+wsMessageRateLimit])
if err != nil {
errs = common.AppendError(errs, err)
}
}
err := b.subscribeSlice(channelsToSubscribe[x:])
if err != nil {
errs = common.AppendError(errs, err)
}
return errs
}
func (b *Bittrex) subscribeSlice(channelsToSubscribe []stream.ChannelSubscription) error {
req := WsEventRequest{
Hub: "c3",
Method: subscribe,
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
}
channels := make([]string, len(channelsToSubscribe))
for i := range channelsToSubscribe {
channels[i] = channelsToSubscribe[i].Channel
}
arguments := make([][]string, 0)
arguments = append(arguments, channels)
req.Arguments = arguments
requestString, err := json.Marshal(req)
if err != nil {
return err
}
if b.Verbose {
log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
}
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
if err != nil {
return err
}
var response WsSubscriptionResponse
err = json.Unmarshal(respRaw, &response)
if err != nil {
return err
}
var errs error
for i := range response.Response {
if !response.Response[i].Success {
errs = common.AppendError(errs, errors.New("unable to subscribe to "+channels[i]+" - error code "+response.Response[i].ErrorCode))
continue
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
}
return errs
}
// Unsubscribe sends a websocket message to receive data from the channel
func (b *Bittrex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var x int
var errs error
for x = 0; x+wsMessageRateLimit < len(channelsToUnsubscribe); x += wsMessageRateLimit {
err := b.unsubscribeSlice(channelsToUnsubscribe[x : x+wsMessageRateLimit])
if err != nil {
errs = common.AppendError(errs, err)
}
}
err := b.unsubscribeSlice(channelsToUnsubscribe[x:])
if err != nil {
errs = common.AppendError(errs, err)
}
return errs
}
func (b *Bittrex) unsubscribeSlice(channelsToUnsubscribe []stream.ChannelSubscription) error {
req := WsEventRequest{
Hub: "c3",
Method: unsubscribe,
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
}
channels := make([]string, len(channelsToUnsubscribe))
for i := range channelsToUnsubscribe {
channels[i] = channelsToUnsubscribe[i].Channel
}
arguments := make([][]string, 0)
arguments = append(arguments, channels)
req.Arguments = arguments
requestString, err := json.Marshal(req)
if err != nil {
return err
}
if b.Verbose {
log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
}
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
if err != nil {
return err
}
var response WsSubscriptionResponse
err = json.Unmarshal(respRaw, &response)
if err != nil {
return err
}
var errs error
for i := range response.Response {
if !response.Response[i].Success {
errs = common.AppendError(errs, errors.New("unable to unsubscribe from "+channels[i]+" - error code "+response.Response[i].ErrorCode))
continue
}
b.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i])
}
return errs
}
// wsReadData gets and passes on websocket messages for processing
func (b *Bittrex) wsReadData() {
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
return
default:
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
log.Warnf(log.WebsocketMgr, "%s Received empty message\n", b.Name)
return
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
}
func (b *Bittrex) wsDecodeMessage(encodedMessage string, v interface{}) error {
raw, err := crypto.Base64Decode(encodedMessage)
if err != nil {
return err
}
reader := flate.NewReader(bytes.NewBuffer(raw))
message, err := io.ReadAll(reader)
if err != nil {
return err
}
if err = reader.Close(); err != nil {
log.Warnf(log.WebsocketMgr, "%s wsDecodeMessage: unable to close reader: %s",
b.Name,
err,
)
}
return json.Unmarshal(message, v)
}
func (b *Bittrex) wsHandleData(respRaw []byte) error {
var response WsEventResponse
err := json.Unmarshal(respRaw, &response)
if err != nil {
log.Warnf(log.WebsocketMgr, "%s Cannot unmarshal into eventResponse (%s)\n", b.Name, string(respRaw))
return err
}
if response.Response != nil && response.InvocationID > 0 {
if b.Websocket.Match.IncomingWithData(response.InvocationID, respRaw) {
return nil
}
return errors.New("received response to unknown request")
}
if response.Response == nil && len(response.Message) == 0 && response.C == "" {
if b.Verbose {
log.Warnf(log.WebsocketMgr, "%s Received keep-alive (%s)\n", b.Name, string(respRaw))
}
return nil
}
for i := range response.Message {
switch response.Message[i].Method {
case "orderBook":
for j := range response.Message[i].Arguments {
var orderbookUpdate OrderbookUpdateMessage
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderbookUpdate)
if err != nil {
return err
}
var init bool
init, err = b.UpdateLocalOBBuffer(&orderbookUpdate)
if err != nil {
if init {
return nil
}
return fmt.Errorf("%v - UpdateLocalCache error: %s",
b.Name,
err)
}
}
case "ticker":
for j := range response.Message[i].Arguments {
var tickerUpdate TickerData
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &tickerUpdate)
if err != nil {
return err
}
err = b.WsProcessUpdateTicker(tickerUpdate)
if err != nil {
return err
}
}
case "marketSummary":
for j := range response.Message[i].Arguments {
var marketSummaryUpdate MarketSummaryData
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &marketSummaryUpdate)
if err != nil {
return err
}
err = b.WsProcessUpdateMarketSummary(&marketSummaryUpdate)
if err != nil {
return err
}
}
case "heartbeat":
if b.Verbose {
log.Warnf(log.WebsocketMgr, "%s Received heartbeat\n", b.Name)
}
case "authenticationExpiring":
if b.Verbose {
log.Debugf(log.WebsocketMgr, "%s - Re-authenticating.\n", b.Name)
}
err = b.WsAuth(context.TODO())
if err != nil {
b.Websocket.DataHandler <- err
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
case "order":
for j := range response.Message[i].Arguments {
var orderUpdate OrderUpdateMessage
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderUpdate)
if err != nil {
return err
}
err = b.WsProcessUpdateOrder(&orderUpdate)
if err != nil {
return err
}
}
}
}
return nil
}
// WsProcessUpdateTicker processes an update on the ticker
func (b *Bittrex) WsProcessUpdateTicker(tickerData TickerData) error {
pair, err := currency.NewPairFromString(tickerData.Symbol)
if err != nil {
return err
}
tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
if err != nil {
b.tickerCache.mu.Lock()
defer b.tickerCache.mu.Unlock()
if b.tickerCache.MarketSummaries[tickerData.Symbol] != nil {
marketSummaryData := b.tickerCache.MarketSummaries[tickerData.Symbol]
tickerPrice = b.constructTicker(tickerData, marketSummaryData, pair, asset.Spot)
b.Websocket.DataHandler <- tickerPrice
return nil
}
b.tickerCache.Tickers[tickerData.Symbol] = &tickerData
return nil
}
tickerPrice.Last = tickerData.LastTradeRate
tickerPrice.Bid = tickerData.BidRate
tickerPrice.Ask = tickerData.AskRate
b.Websocket.DataHandler <- tickerPrice
return nil
}
// WsProcessUpdateMarketSummary processes an update on the ticker
func (b *Bittrex) WsProcessUpdateMarketSummary(marketSummaryData *MarketSummaryData) error {
pair, err := currency.NewPairFromString(marketSummaryData.Symbol)
if err != nil {
return err
}
tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
if err != nil {
b.tickerCache.mu.Lock()
defer b.tickerCache.mu.Unlock()
if b.tickerCache.Tickers[marketSummaryData.Symbol] != nil {
tickerData := b.tickerCache.Tickers[marketSummaryData.Symbol]
tickerPrice = b.constructTicker(*tickerData, marketSummaryData, pair, asset.Spot)
b.Websocket.DataHandler <- tickerPrice
return nil
}
b.tickerCache.MarketSummaries[marketSummaryData.Symbol] = marketSummaryData
return nil
}
tickerPrice.High = marketSummaryData.High
tickerPrice.Low = marketSummaryData.Low
tickerPrice.Volume = marketSummaryData.Volume
tickerPrice.QuoteVolume = marketSummaryData.QuoteVolume
tickerPrice.LastUpdated = marketSummaryData.UpdatedAt
b.Websocket.DataHandler <- tickerPrice
return nil
}
// WsProcessUpdateOrder processes an update on the open orders
func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error {
orderType, err := order.StringToOrderType(data.Delta.Type)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: data.Delta.ID,
Err: err,
}
}
orderSide, err := order.StringToOrderSide(data.Delta.Direction)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: data.Delta.ID,
Err: err,
}
}
orderStatus, err := order.StringToOrderStatus(data.Delta.Status)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: data.Delta.ID,
Err: err,
}
}
pair, err := currency.NewPairFromString(data.Delta.MarketSymbol)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: data.Delta.ID,
Err: err,
}
}
b.Websocket.DataHandler <- &order.Detail{
ImmediateOrCancel: data.Delta.TimeInForce == string(ImmediateOrCancel),
FillOrKill: data.Delta.TimeInForce == string(GoodTilCancelled),
PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled),
Price: data.Delta.Limit,
Amount: data.Delta.Quantity,
RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity,
ExecutedAmount: data.Delta.FillQuantity,
Exchange: b.Name,
OrderID: data.Delta.ID,
Type: orderType,
Side: orderSide,
Status: orderStatus,
AssetType: asset.Spot,
Date: data.Delta.CreatedAt,
Pair: pair,
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,437 +0,0 @@
package bittrex
import (
"context"
"fmt"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/log"
)
var (
// maxWSUpdateBuffer defines max websocket updates to apply when an
// orderbook is initially fetched
maxWSUpdateBuffer = 150
// maxWSOrderbookJobs defines max websocket orderbook jobs in queue to fetch
// an orderbook snapshot via REST
maxWSOrderbookJobs = 2000
// maxWSOrderbookWorkers defines a max amount of workers allowed to execute
// jobs from the job channel
maxWSOrderbookWorkers = 10
)
func (b *Bittrex) setupOrderbookManager() {
if b.obm == nil {
b.obm = &orderbookManager{
state: make(map[currency.Code]map[currency.Code]map[asset.Item]*update),
jobs: make(chan job, maxWSOrderbookJobs),
}
} else {
// Change state on reconnect for initial sync.
for _, m1 := range b.obm.state {
for _, m2 := range m1 {
for _, update := range m2 {
update.initialSync = true
update.needsFetchingBook = true
}
}
}
}
for i := 0; i < maxWSOrderbookWorkers; i++ {
// 10 workers for synchronising book
b.SynchroniseWebsocketOrderbook()
}
}
// ProcessUpdateOB processes the websocket orderbook update
func (b *Bittrex) ProcessUpdateOB(pair currency.Pair, message *OrderbookUpdateMessage) error {
updateBids := make([]orderbook.Item, len(message.BidDeltas))
for x := range message.BidDeltas {
updateBids[x] = orderbook.Item{
Price: message.BidDeltas[x].Rate,
Amount: message.BidDeltas[x].Quantity,
}
}
updateAsks := make([]orderbook.Item, len(message.AskDeltas))
for x := range message.AskDeltas {
updateAsks[x] = orderbook.Item{
Price: message.AskDeltas[x].Rate,
Amount: message.AskDeltas[x].Quantity,
}
}
return b.Websocket.Orderbook.Update(&orderbook.Update{
Asset: asset.Spot,
Pair: pair,
UpdateID: message.Sequence,
Bids: updateBids,
Asks: updateAsks,
})
}
// UpdateLocalOBBuffer updates and returns the most recent iteration of the orderbook
func (b *Bittrex) UpdateLocalOBBuffer(update *OrderbookUpdateMessage) (bool, error) {
enabledPairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return false, err
}
format, err := b.GetPairFormat(asset.Spot, true)
if err != nil {
return false, err
}
currencyPair, err := currency.NewPairFromFormattedPairs(update.MarketSymbol,
enabledPairs,
format)
if err != nil {
return false, err
}
err = b.obm.stageWsUpdate(update, currencyPair, asset.Spot)
if err != nil {
init, err2 := b.obm.checkIsInitialSync(currencyPair)
if err2 != nil {
return false, err2
}
return init, err
}
err = b.applyBufferUpdate(currencyPair)
if err != nil {
log.Errorf(log.WebsocketMgr, "%s websocket UpdateLocalOBBuffer: Could not apply buffer update\n", b.Name)
}
return false, err
}
// SeedLocalOBCache seeds depth data
func (b *Bittrex) SeedLocalOBCache(ctx context.Context, p currency.Pair) error {
ob, sequence, err := b.GetOrderbook(ctx, p.String(), orderbookDepth)
if err != nil {
return err
}
return b.SeedLocalCacheWithOrderBook(p, sequence, ob, orderbookDepth)
}
// SeedLocalCacheWithOrderBook seeds the local orderbook cache
func (b *Bittrex) SeedLocalCacheWithOrderBook(p currency.Pair, sequence int64, orderbookNew *OrderbookData, maxDepth int) error {
newOrderBook := orderbook.Base{
Pair: p,
Asset: asset.Spot,
Exchange: b.Name,
LastUpdateID: sequence,
VerifyOrderbook: b.CanVerifyOrderbook,
Bids: make(orderbook.Items, len(orderbookNew.Bid)),
Asks: make(orderbook.Items, len(orderbookNew.Ask)),
MaxDepth: maxDepth,
}
for i := range orderbookNew.Bid {
newOrderBook.Bids[i] = orderbook.Item{
Amount: orderbookNew.Bid[i].Quantity,
Price: orderbookNew.Bid[i].Rate,
}
}
for i := range orderbookNew.Ask {
newOrderBook.Asks[i] = orderbook.Item{
Amount: orderbookNew.Ask[i].Quantity,
Price: orderbookNew.Ask[i].Rate,
}
}
return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
}
// applyBufferUpdate applies the buffer to the orderbook or initiates a new
// orderbook sync by the REST protocol which is off handed to go routine.
func (b *Bittrex) applyBufferUpdate(pair currency.Pair) error {
fetching, needsFetching, err := b.obm.handleFetchingBook(pair)
if err != nil {
return err
}
if fetching {
return nil
}
if needsFetching {
if b.Verbose {
log.Debugf(log.WebsocketMgr, "%s Orderbook: Fetching via REST\n", b.Name)
}
return b.obm.fetchBookViaREST(pair)
}
recent, err := b.Websocket.Orderbook.GetOrderbook(pair, asset.Spot)
if err != nil {
log.Errorf(
log.WebsocketMgr,
"%s error fetching recent orderbook when applying updates: %s\n",
b.Name,
err)
}
if recent != nil {
err = b.obm.checkAndProcessUpdate(b.ProcessUpdateOB, pair, recent)
if err != nil {
log.Errorf(
log.WebsocketMgr,
"%s error processing update - initiating new orderbook sync via REST: %s\n",
b.Name,
err)
err = b.obm.setNeedsFetchingBook(pair)
if err != nil {
return err
}
}
}
return nil
}
// SynchroniseWebsocketOrderbook synchronises full orderbook for currency pair
// asset
func (b *Bittrex) SynchroniseWebsocketOrderbook() {
b.Websocket.Wg.Add(1)
go func() {
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
for {
select {
case <-b.obm.jobs:
default:
return
}
}
case j := <-b.obm.jobs:
err := b.processJob(j.Pair)
if err != nil {
log.Errorf(log.WebsocketMgr,
"%s processing websocket orderbook error %v",
b.Name, err)
}
}
}
}()
}
// processJob fetches and processes orderbook updates
func (b *Bittrex) processJob(p currency.Pair) error {
err := b.SeedLocalOBCache(context.TODO(), p)
if err != nil {
return fmt.Errorf("%s %s seeding local cache for orderbook error: %v",
p, asset.Spot, err)
}
err = b.obm.stopFetchingBook(p)
if err != nil {
return err
}
// Immediately apply the buffer updates so we don't wait for a
// new update to initiate this.
return b.applyBufferUpdate(p)
}
// stageWsUpdate stages websocket update to roll through updates that need to
// be applied to a fetched orderbook via REST.
func (o *orderbookManager) stageWsUpdate(u *OrderbookUpdateMessage, pair currency.Pair, a asset.Item) error {
o.Lock()
defer o.Unlock()
m1, ok := o.state[pair.Base]
if !ok {
m1 = make(map[currency.Code]map[asset.Item]*update)
o.state[pair.Base] = m1
}
m2, ok := m1[pair.Quote]
if !ok {
m2 = make(map[asset.Item]*update)
m1[pair.Quote] = m2
}
state, ok := m2[a]
if !ok {
state = &update{
// 100ms update assuming we might have up to a 10 second delay.
// There could be a potential 100 updates for the currency.
buffer: make(chan *OrderbookUpdateMessage, maxWSUpdateBuffer),
fetchingBook: false,
initialSync: true,
needsFetchingBook: true,
}
m2[a] = state
}
select {
// Put update in the channel buffer to be processed
case state.buffer <- u:
return nil
default:
<-state.buffer // pop one element
state.buffer <- u // to shift buffer on fail
return fmt.Errorf("channel blockage for %s, asset %s and connection",
pair, a)
}
}
// stopFetchingBook completes the book fetching.
func (o *orderbookManager) stopFetchingBook(pair currency.Pair) error {
o.Lock()
defer o.Unlock()
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
if !ok {
return fmt.Errorf("could not match pair %s and asset type %s in hash table",
pair,
asset.Spot)
}
if !state.fetchingBook {
return fmt.Errorf("fetching book already set to false for %s %s",
pair,
asset.Spot)
}
state.fetchingBook = false
return nil
}
// setNeedsFetchingBook completes the book fetching initiation.
func (o *orderbookManager) setNeedsFetchingBook(pair currency.Pair) error {
o.Lock()
defer o.Unlock()
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
if !ok {
return fmt.Errorf("could not match pair %s and asset type %s in hash table",
pair,
asset.Spot)
}
state.needsFetchingBook = true
return nil
}
// handleFetchingBook checks if a full book is being fetched or needs to be
// fetched
func (o *orderbookManager) handleFetchingBook(pair currency.Pair) (fetching, needsFetching bool, err error) {
o.Lock()
defer o.Unlock()
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
if !ok {
return false, false,
fmt.Errorf("check is fetching book cannot match currency pair %s asset type %s",
pair,
asset.Spot)
}
if state.fetchingBook {
return true, false, nil
}
if state.needsFetchingBook {
state.needsFetchingBook = false
state.fetchingBook = true
return false, true, nil
}
return false, false, nil
}
// checkIsInitialSync checks status if the book is Initial Sync being via the REST
// protocol.
func (o *orderbookManager) checkIsInitialSync(pair currency.Pair) (bool, error) {
o.Lock()
defer o.Unlock()
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
if !ok {
return false,
fmt.Errorf("checkIsInitialSync of orderbook cannot match currency pair %s asset type %s",
pair,
asset.Spot)
}
return state.initialSync, nil
}
// fetchBookViaREST pushes a job of fetching the orderbook via the REST protocol
// to get an initial full book that we can apply our buffered updates too.
func (o *orderbookManager) fetchBookViaREST(pair currency.Pair) error {
o.Lock()
defer o.Unlock()
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
if !ok {
return fmt.Errorf("fetch book via rest cannot match currency pair %s asset type %s",
pair,
asset.Spot)
}
state.initialSync = true
state.fetchingBook = true
select {
case o.jobs <- job{pair}:
return nil
default:
return fmt.Errorf("%s %s book synchronisation channel blocked up",
pair,
asset.Spot)
}
}
func (o *orderbookManager) checkAndProcessUpdate(processor func(currency.Pair, *OrderbookUpdateMessage) error, pair currency.Pair, recent *orderbook.Base) error {
o.Lock()
defer o.Unlock()
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
if !ok {
return fmt.Errorf("could not match pair [%s] asset type [%s] in hash table to process websocket orderbook update",
pair, asset.Spot)
}
// This will continuously remove updates from the buffered channel and
// apply them to the current orderbook.
buffer:
for {
select {
case d := <-state.buffer:
process, err := state.validate(d, recent)
if err != nil {
return err
}
if process {
err := processor(pair, d)
if err != nil {
return fmt.Errorf("%s %s processing update error: %w",
pair, asset.Spot, err)
}
recent.LastUpdateID = d.Sequence
}
default:
break buffer
}
}
return nil
}
// validate checks for correct update alignment
func (u *update) validate(updt *OrderbookUpdateMessage, recent *orderbook.Base) (bool, error) {
if updt.Sequence <= recent.LastUpdateID {
// Drop any event where u is <= lastUpdateId in the snapshot.
return false, nil
}
id := recent.LastUpdateID + 1
if u.initialSync {
// The first processed event should have U <= lastUpdateId+1 AND
// u >= lastUpdateId+1.
if updt.Sequence > id {
return false, fmt.Errorf("initial websocket orderbook sync failure for pair %s and asset %s",
recent.Pair,
asset.Spot)
}
u.initialSync = false
} else if updt.Sequence != id {
// While listening to the stream, each new event's U should be
// equal to the previous event's u+1.
return false, fmt.Errorf("websocket orderbook synchronisation failure for pair %s and asset %s",
recent.Pair,
asset.Spot)
}
return true, nil
}

View File

@@ -73,6 +73,7 @@ func TestMain(m *testing.M) {
if err != nil {
log.Fatal(err)
}
request.MaxRequestJobs = 100
ku.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
ku.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
@@ -2480,11 +2481,11 @@ func TestProcessMarketSnapshot(t *testing.T) {
assert.Equal(t, 0.00000039450000000000, v.High, "high")
assert.Equal(t, 0.0000003897, v.Last, "lastTradedPrice")
assert.Equal(t, 0.00000034200000000000, v.Low, "low")
assert.Equal(t, currency.NewPairWithDelimiter("MTV", "BTC", "-"), v.Pair, "symbol")
assert.Equal(t, currency.NewPairWithDelimiter("ETH", "BTC", "-"), v.Pair, "symbol")
assert.Equal(t, 316078.69700000000000000000, v.Volume, "volume")
assert.Equal(t, 0.11768519138877000000, v.QuoteVolume, "volValue")
// both margin and spot
case 3:
case 3, 4:
assert.Equal(t, time.UnixMilli(1698740324437), v.LastUpdated, "datetime")
assert.Equal(t, 0.00008486000000000000, v.High, "high")
assert.Equal(t, 0.00008318, v.Last, "lastTradedPrice")
@@ -2505,7 +2506,6 @@ func TestProcessMarketSnapshot(t *testing.T) {
func TestSubscribeMarketSnapshot(t *testing.T) {
t.Parallel()
setupWS()
s := []stream.ChannelSubscription{
{Channel: marketTickerSnapshotForCurrencyChannel,
Currency: currency.Pair{Base: currency.BTC}},

View File

@@ -1,3 +1,3 @@
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324504,"data":{"averagePrice":0.00001164,"baseCurrency":"XMR","board":0,"buy":0.00001252,"changePrice":0.00000104800000000000,"changeRate":0.0914,"close":0.000012508,"datetime":1698740324415,"high":0.00001402100000000000,"lastTradedPrice":0.000012508,"low":0.00001129200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000104800000000000,"changeRate":0.0914,"high":0.00001402100000000000,"low":0.00001129200000000000,"open":0.00001146000000000000,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000},"marketChange4h":{"changePrice":0.00000009600000000000,"changeRate":0.0077,"high":0.00001308400000000000,"low":0.00001241200000000000,"open":0.00001241200000000000,"vol":7090.00000000000000000000,"volValue":0.08885800028840000000},"markets":["BTC"],"open":0.00001146000000000000,"quoteCurrency":"BTC","sell":0.000013191,"sort":100,"symbol":"XMR-BTC","symbolCode":"XMR-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324488,"data":{"averagePrice":0.00000037,"baseCurrency":"MTV","board":0,"buy":0.0000003641,"changePrice":0.00000004770000000000,"changeRate":0.1394,"close":0.0000003897,"datetime":1698740324483,"high":0.00000039450000000000,"lastTradedPrice":0.0000003897,"low":0.00000034200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000004770000000000,"changeRate":0.1394,"high":0.00000039450000000000,"low":0.00000034200000000000,"open":0.00000034200000000000,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000},"marketChange4h":{"changePrice":0.00000003290000000000,"changeRate":0.0922,"high":0.00000038970000000000,"low":0.00000035680000000000,"open":0.00000035680000000000,"vol":2309.46880000000000000000,"volValue":0.00089999999136000000},"markets":["BTC"],"open":0.00000034200000000000,"quoteCurrency":"BTC","sell":0.0000004022,"sort":100,"symbol":"MTV-BTC","symbolCode":"MTV-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324488,"data":{"averagePrice":0.00000037,"baseCurrency":"ETH","board":0,"buy":0.0000003641,"changePrice":0.00000004770000000000,"changeRate":0.1394,"close":0.0000003897,"datetime":1698740324483,"high":0.00000039450000000000,"lastTradedPrice":0.0000003897,"low":0.00000034200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000004770000000000,"changeRate":0.1394,"high":0.00000039450000000000,"low":0.00000034200000000000,"open":0.00000034200000000000,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000},"marketChange4h":{"changePrice":0.00000003290000000000,"changeRate":0.0922,"high":0.00000038970000000000,"low":0.00000035680000000000,"open":0.00000035680000000000,"vol":2309.46880000000000000000,"volValue":0.00089999999136000000},"markets":["BTC"],"open":0.00000034200000000000,"quoteCurrency":"BTC","sell":0.0000004022,"sort":100,"symbol":"ETH-BTC","symbolCode":"ETH-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324508,"data":{"averagePrice":0.00007307,"baseCurrency":"BTC","board":0,"buy":0.00008388,"changePrice":0.00001166000000000000,"changeRate":0.1630,"close":0.00008318,"datetime":1698740324437,"high":0.00008486000000000000,"lastTradedPrice":0.00008318,"low":0.00007152000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"USDT","marketChange1h":{"changePrice":-0.00000116000000000000,"changeRate":-0.0137,"high":0.00008434000000000000,"low":0.00008318000000000000,"open":0.00008434000000000000,"vol":189.33430000000000000000,"volValue":0.01578748292300000000},"marketChange24h":{"changePrice":0.00001166000000000000,"changeRate":0.1630,"high":0.00008486000000000000,"low":0.00007152000000000000,"open":0.00007152000000000000,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000},"marketChange4h":{"changePrice":0.00000143000000000000,"changeRate":0.0174,"high":0.00008486000000000000,"low":0.00008175000000000000,"open":0.00008175000000000000,"vol":1752.55690000000000000000,"volValue":0.14543003812900000000},"markets":["BTC"],"open":0.00007152000000000000,"quoteCurrency":"USDT","sell":0.00008421,"sort":100,"symbol":"BTC-USDT","symbolCode":"BTC-USDT","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000}}}

View File

@@ -106,8 +106,8 @@ type Base struct {
// Checks if the orderbook needs ID alignment as well as price alignment
IDAlignment bool
// Determines if there is a max depth of orderbooks and after an append we
// should remove any items that are outside of this scope. Bittrex and
// Kraken utilise this field.
// should remove any items that are outside of this scope. Kraken utilises
// this field.
MaxDepth int
// ChecksumStringRequired defines if the checksum is built from the raw
// string representations of the price and amount. This helps alleviate any

View File

@@ -21,7 +21,6 @@ var Exchanges = []string{
"bitflyer",
"bitmex",
"bitstamp",
"bittrex",
"btc markets",
"btse",
"bybit",

View File

@@ -67,7 +67,6 @@ _b in this context is an `IBotExchange` implemented struct_
| Bithumb | Yes | Yes | No |
| BitMEX | Yes | Yes | Yes |
| Bitstamp | Yes | Yes | No |
| Bittrex | Yes | Yes | No |
| BTCMarkets | Yes | Yes | No |
| BTSE | Yes | Yes | No |
| Bybit | Yes | Yes | Yes |

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,6 @@ bitflyer,
bithumb,
bitmex,
bitstamp,
bittrex,
btc markets,
btse,
coinbasepro,
1 binanceus
5 bithumb
6 bitmex
7 bitstamp
bittrex
8 btc markets
9 btse
10 coinbasepro