Currency System Update (#448)

* initial update of currency system

* WIP progress

* Finish initial currency string error returns

* fix whoopsie testing for non https insecureinos

* Current WIP for getEnabledPairs check and error return

* WIP continued

* When getting enabled pairs throw error when item is not contained in available pairs list

* More updates -WIP

* Wip continued including potential interface

* Current WIP

* pairs manager pass

* drop asset string and just use the map key, plus return some errors and create more work for myself.

* clean and fixed a bug in currency.json, will not populate correctly without coinmarketcap api keys set.

* purge logger references after merge

* go mod tidy after merge

* Pointer change WIP

* fix some issues and added error returns to a few items (WIP)

* WIP

* Clean

* Fix some linter issues

* Fix more linter issues

* even more linters

* xtda nits

* revert pointer change and rm field

* Addr madcozbadd nits

* fix linter issues: shadow declarations

* Fix linter issues: gocritic huge things

* linter issue fix

* Addr nits

* flush go mod files

* after merge woops

* fix shadow dec

* Addr thrasher nits

* addr nits

* fix some issues

* more fixes

* RM println

* Addr glorious nits

* Add helper method for setting assets

* add missing format directive

* Addr nits

* Actually process new futures contracts -_- derp

* WIP for GRPC upgrade for pair management

* update config pairs

* finished disabling and enabling asset

* linked update of tradable pairs to cli

* fix oopsies

* defer writing to file on program termination for currency storage system

* update template

* don't add disabled asset items to initial sync

* Fix enable disabling a list of pairs and added in a slice error type so we can add whats allowable without throwing an error and return a report, also addressed some other nits

* WIP on getting a channel to unsub

* Wip track down unsubscribe bug and start creating streaming interface

* purge websocket orderbook object and centralised updating routine for orderbook

* general clean before interface implementation

* stage one connection interface WIP

* WIP

* repackage wshandler WIP

* find difference of subs and change signature of subscriber functions so we can batch subscriptions and unsubscription in exchanges that support it

* design change on mange subscription routine WIP

* integrated ZB with the new webosocket updates

* WIP - okex conversion

* integrate websocket upgrades for lakebtc, kraken, huobi, hitbtc, gateio, and WIP for coinbene

* integrate another range of exchanges for websocket update

* Added subscriber and unsubscriber methods to websocket functionality

* fixed tests WIP

* amalgamate cache setup with main websocket setup

* reinstate exported fields traffic alert and shutdownC to accommodate gemini and lakebtc implementations

* added in colon

* Updated websocket auth handling as they werent getting passed through. Added a setter method for websocket URL due to the Binance generated auth key/listenKey. Fixed bug which stopped reconnection.

* Fix subscribe candle bug
Fix time conversion in candle
Fix inititial candle history to datahandler
Include funding to orderbook handling
Include funding to trades
Reduce code duplication in sub and unsub functions
Added the ability to include funding currency websocket subscriptions
validated all channels and added more items todo list (Auth items)

* RM line

* bitstamp pass

* btcmarkets pass - still needs to implement unsubscriber functionality and pairs change test.

* Batch outgoing subscriptions and fix unsubscribe bug

* BTSE - bumped time to minute to reduce pinger calling by 75 calls per day. Fix authentication bug and add authentication pass into to-do. Batch outgoing subscription calls

* fix type field and batch outgoing subs and unsubs for coinbasepro

* Batch outgoing subs and unsubs

* Fixes bug when matching return from authentication

* Fix bug where params where being sent out of order due to map ,where depth items werent being subscribed too, where trying to subscribe to too many kline items caused error, where trying to get a nano secocond ID conflicted due to speed of generation.

* Add websocket capability for currency pair change by utilizing full channel subscription list in subscribe function.

* Add error handling

* Fix public: time conversions, subscription list, stopped pushing heartbeat to data handler, aggregated list of connections.

* hitBTC pass

* returned nil instead of error due to period null bids and asks updates coming through.

* Fix auth ping capture and reply. Added in interval handling for kline data. Added correct full trade data handling. Fix subscribe and unsubscribe.

* Fix when websocket auth conn and token generation fail we don't try and auth sub. Fix bug between auth and normal connection id generation and matching. Batch outgoing payloads to increase efficiency. Updated matching functions to utilise channels instead of waitgroups and go routines.

* RM debug output

* rm func to get shutdown channel

* Add unsubscriber functionality, added wsTicker type, removed return as this will impede data flow and cause reconnection when handling and processing data

* okgroup WIP

* *Added missing fields for websocket trades
*Fix bug processing kline interval
*Added fields for websocket ticker struct
*Fix auth bug
	-Updated request and response matching param to interface so we can custom signature match. Stops auth subscribing before a reply is issued.
	-Updated channel inclusion of pair fo auth subs as this was missing.
*Assortment of perfomance improvements

* poloniex pass

* send all trades to data handler, validated enabled and disable pairs

* initial clean

* centralised request matching mechanism

* websocket main improvements WIP

* WIP

* Websocket management via gctcli WIP

* GRPC expansion

* Updated GCTCLI with websocket url and proxy setting functionality which flushes connection

* Fix continuous spawning of routines bug on error with reconnection

* Addr linter issues

* fix subscription bug that I caused when I changed to a switch case

* fix linter issue

* fix woopsie

* End of day WIP

* Fix order submission REST, time conversion, order type conversion, orderID bugs

* fix gateio test and unsubscribe bug

* revert comment out code

* websocketAPI changed to to true in configtest.json

* fix race in gateio test

* End of day WIP for websocket tests.

* BugFix for binance when book isn't seeded. Updated websocket tests. Deprecated subscription manager. RM wrapper funcs.

* Added string title to exchange name as they are saved as lower case in type, reinstated verbose check in websocket.go

* Added verbosity check for setting websocket URL

* fix bug where the asset had a mind of its own

* purge dodgy coding

* Fix tests, drop blocking chan in websocket Dial function

* few more changes

* race condition fix for websocket tests.

* fix intermittent test failure due to underlying hash table storage

* Address madcuzbad nit

* RM superfluous printlines

* Add quick top example with paramater fields

* First pass Glorious nits

* As per madcozbad suggestion return error when enabled pair not found in full return map. Add test.

* addr madcozbadd nits

* as per glorious suggestion rm'd loadedJSON field

* adjusted ticker, added test and RM'd code that can never be executed

* Addr nits and add in crypto rand genration for ID's

* remove global channel declaration and rescoped as this was causing a lock

* as per glorious suggestion restructured return error for websocket

* addr glorious suggestions

* fix linter issues

* purge non-existent pair from testdata

* add side field to struct and parse

* addr glorious nits

* Add verbosity to error returns and logs and fix string parsing in GCTRPC

* fix speeling mistwake

* Adds websocket functionality check before flushing websocket connection

* Addr kraken panic and setting/flush websocket url stage one.

* added websocket url check before setting with tests

* Added in edge case test if by the time we call contains on available pairs it has been changed

* remove error return for func

* Continuation of tests

* continuation of tests

* Stop potential panic within pair creation function

* Implement changes to upstream

* rm sup comment

* fix bug when subscribing and unsubscribing. Also add in boolean to determine there are currencies that need to be flushed via set pairs via gctcli

* fix test

* Fix linter issues

* Fix tests

* turn websocket off in config example

* Fix issue where you cannot enable websocket when config is set to false, also added config websocket enable state saving

* Introduced err var for same error returns

* Add err var exchange base not found

* restructure function

* drop gctscript from generic response name

* drop managesub delay const as its not being used

* correctly implement websocket rate limiting for coinut

* remove quotations

* drop pair management check

* fix spelling

* return error in function to not update currency with unset role

* amalagamted enable/disable into set function and added in pairstore fetch function

* update error description

* rm function

* moved test function to sharedtestvals and move type to types.go

* append delimiter onto currency delimiter strings

* add test coverage

* rm functions as they are set as methods in base

* remove superfluous methods

* Fix issue that would occur when a subscription errored and not appending successful subs

* fix after rebase woopsie

* fix linter issues

* fix bug streamline code

* fix linter issues

* fix linter issues

* fix case where it should not change ID if set but append new

* fix whoopsie

* fix websocket tests

* fix readme, fix wrapper issues reporting template, go mod tidy

* add test coverage

* add test coverage and verified futures pariing

* add in futures bypass as its not currently supported on BTSE until API update and implementation

* removed downside/upside profit contract type as its no longer supported. Added in check in set config pairs to warn user of potential conflict and to manually remove or update.

* If asset enabled add pair and increase code coverage

* remove strings.title, set and fetch with strings.Lower but keep struct field exchangename unchanged. Streamline ticker and orderbook code.

* Add code coverage

* log error if setting default currency fails, add code coverage

* address glorious nits

* Addr xtda nits

* fix linter issues

* addr glorious nits

* xtda nits

* Addr glorious nits

* add subscription protection and removed a superfluous wait call

* fix test

* fix whoopsie

* addr xtda nits

* addr glorious nits

* Added asset types to subscriptions structs, also added in error handling for resubscription errors

* consolidated rpc returned type and added in sucessful strings

* dropped stream timing down to 100ms

* DOC changes

* proxy and url usage string additions

* WIP

* go mod tidy rides again

* Addr nits

* Addr nits, fix tests

* fix wording

* add in test case for currency matching

* Add byte length check on outbound websocket payload subscriptions

* addr thrasher nits

* Addr madcozbadd nits

* addr linter issues

* Addr glorious nits by amalgamating function into one mega amazing function.

* fix futures account subscription bug

* addr glorious nits and reinstated wg.Wait() checks

* changed string to currency delimiter string and setconnected by function
This commit is contained in:
Ryan O'Hara-Reid
2020-07-24 13:18:09 +10:00
committed by GitHub
parent 627a1e38ea
commit 14c72c9c6b
213 changed files with 23199 additions and 19517 deletions

View File

@@ -141,14 +141,14 @@ Binaries will be published once the codebase reaches a stable condition.
|User|Contribution Amount|
|--|--|
| [thrasher-](https://github.com/thrasher-) | 640 |
| [thrasher-](https://github.com/thrasher-) | 641 |
| [shazbert](https://github.com/shazbert) | 191 |
| [gloriousCode](https://github.com/gloriousCode) | 169 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 48 |
| [xtda](https://github.com/xtda) | 43 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 51 |
| [xtda](https://github.com/xtda) | 45 |
| [ermalguni](https://github.com/ermalguni) | 14 |
| [vadimzhukck](https://github.com/vadimzhukck) | 10 |
| [MadCozBadd](https://github.com/MadCozBadd) | 8 |
| [MadCozBadd](https://github.com/MadCozBadd) | 9 |
| [140am](https://github.com/140am) | 8 |
| [marcofranssen](https://github.com/marcofranssen) | 8 |
| [dackroyd](https://github.com/dackroyd) | 5 |

View File

@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"log"
@@ -49,9 +50,9 @@ func main() {
if !config.ConfirmECS(fileData) && !encrypt {
var result interface{}
errf := config.ConfirmConfigJSON(fileData, result)
errf := json.Unmarshal(fileData, &result)
if errf != nil {
log.Fatal("File isn't in JSON format")
log.Fatal(errf)
}
log.Println("File is already decrypted. Encrypting..")
encrypt = true

View File

@@ -15,7 +15,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
const (
@@ -147,9 +146,6 @@ func makeExchange(exch *exchange) error {
newExchConfig.API.Credentials.Key = "Key"
newExchConfig.API.Credentials.Secret = "Secret"
newExchConfig.CurrencyPairs = &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,

View File

@@ -17,7 +17,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -30,13 +30,10 @@ func ({{.Variable}} *{{.CapitalName}}) GetDefaultConfig() (*config.ExchangeConfi
exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout
exchCfg.BaseCurrencies = {{.Variable}}.BaseCurrencies
err := {{.Variable}}.SetupDefaults(exchCfg)
if err != nil {
return nil, err
}
{{.Variable}}.SetupDefaults(exchCfg)
if {{.Variable}}.Features.Supports.RESTCapabilities.AutoPairUpdates {
err = {{.Variable}}.UpdateTradablePairs(true)
err := {{.Variable}}.UpdateTradablePairs(true)
if err != nil {
return nil, err
}
@@ -51,20 +48,46 @@ func ({{.Variable}} *{{.CapitalName}}) SetDefaults() {
{{.Variable}}.Verbose = true
{{.Variable}}.API.CredentialsValidator.RequiresKey = true
{{.Variable}}.API.CredentialsValidator.RequiresSecret = true
{{.Variable}}.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
},
// If using only one pair format for request and configuration, across all
// supported asset types either SPOT and FUTURES etc. You can use the
// example below:
// Request format denotes what the pair as a string will be, when you send
// a request to an exchange.
requestFmt := &currency.PairFormat{/*Set pair request formatting details here for e.g.*/ Uppercase: true, Delimiter: ":"}
// Config format denotes what the pair as a string will be, when saved to
// the config.json file.
configFmt := &currency.PairFormat{/*Set pair request formatting details here*/}
err := {{.Variable}}.SetGlobalPairsManager(requestFmt, configFmt, /*multiple assets can be set here using the asset package ie asset.Spot*/)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// If assets require multiple differences in formating for request and
// configuration, another exchange method can be be used e.g. futures
// contracts require a dash as a delimiter rather than an underscore. You
// can use this example below:
fmt1 := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
ConfigFormat: &currency.PairFormat{Uppercase: true},
}
fmt2 := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
ConfigFormat: &currency.PairFormat{Uppercase: true, Delimiter: ":"},
}
err = {{.Variable}}.StoreAssetPairFormat(asset.Spot, fmt1)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = {{.Variable}}.StoreAssetPairFormat(asset.Margin, fmt2)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// Fill out the capabilities/features that the exchange supports
{{.Variable}}.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
@@ -91,7 +114,7 @@ func ({{.Variable}} *{{.CapitalName}}) SetDefaults() {
{{.Variable}}.API.Endpoints.URLDefault = {{.Name}}APIURL
{{.Variable}}.API.Endpoints.URL = {{.Variable}}.API.Endpoints.URLDefault
{{.Variable}}.Websocket = wshandler.New()
{{.Variable}}.Websocket = stream.New()
{{.Variable}}.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
{{.Variable}}.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
{{.Variable}}.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
@@ -104,15 +127,12 @@ func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.ExchangeConfig) error
return nil
}
err := {{.Variable}}.SetupDefaults(exch)
if err != nil {
return err
}
{{.Variable}}.SetupDefaults(exch)
// If websocket is supported, please fill out the following
/*
err = {{.Variable}}.Websocket.Setup(
&wshandler.WebsocketSetup{
&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
@@ -129,7 +149,7 @@ func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.ExchangeConfig) error
return err
}
{{.Variable}}.WebsocketConn = &wshandler.WebsocketConnection{
{{.Variable}}.WebsocketConn = &stream.WebsocketConnection{
ExchangeName: {{.Variable}}.Name,
URL: {{.Variable}}.Websocket.GetWebsocketURL(),
ProxyURL: {{.Variable}}.Websocket.GetProxyAddress(),
@@ -195,8 +215,13 @@ func ({{.Variable}} *{{.CapitalName}}) UpdateTradablePairs(forceUpdate bool) err
if err != nil {
return err
}
return {{.Variable}}.UpdatePairs(currency.NewPairsFromStrings(pairs),
asset.Spot, false, forceUpdate)
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
return {{.Variable}}.UpdatePairs(p, asset.Spot, false, forceUpdate)
}
@@ -356,11 +381,6 @@ func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFundsToInternationalBank(with
return nil, common.ErrNotYetImplemented
}
// GetWebsocket returns a pointer to the exchange websocket
func ({{.Variable}} *{{.CapitalName}}) GetWebsocket() (*wshandler.Websocket, error) {
return nil, common.ErrNotYetImplemented
}
// GetActiveOrders retrieves any orders that are active/open
func ({{.Variable}} *{{.CapitalName}}) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) {
return nil, common.ErrNotYetImplemented
@@ -377,30 +397,6 @@ func ({{.Variable}} *{{.CapitalName}}) GetFeeByType(feeBuilder *exchange.FeeBuil
return 0, common.ErrNotYetImplemented
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func ({{.Variable}} *{{.CapitalName}}) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
{{.Variable}}.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
{{.Variable}}.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrNotYetImplemented
}
// AuthenticateWebsocket sends an authentication message to the websocket
func ({{.Variable}} *{{.CapitalName}}) AuthenticateWebsocket() error {
return common.ErrNotYetImplemented
}
// ValidateCredentials validates current credentials used for wrapper
func ({{.Variable}} *{{.CapitalName}}) ValidateCredentials() error {
_, err := {{.Variable}}.UpdateAccountInfo()

View File

@@ -296,10 +296,16 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
switch {
case currencyPairOverride != "":
p = currency.NewPairFromString(currencyPairOverride)
var err error
p, err = currency.NewPairFromString(currencyPairOverride)
if err != nil {
log.Printf("%v Encountered error: '%v'", base.GetName(), err)
continue
}
case len(base.Config.CurrencyPairs.Pairs[assetTypes[i]].Enabled) == 0:
if len(base.Config.CurrencyPairs.Pairs[assetTypes[i]].Available) == 0 {
log.Printf("%v has no enabled or available currencies. Skipping", base.GetName())
log.Printf("%v has no enabled or available currencies. Skipping",
base.GetName())
continue
}
p = base.Config.CurrencyPairs.Pairs[assetTypes[i]].Available.GetRandomPair()
@@ -308,8 +314,8 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
}
responseContainer := ExchangeAssetPairResponses{
AssetType: assetTypes[i],
CurrencyPair: p,
AssetType: assetTypes[i],
Pair: p,
}
log.Printf("Setup config for %v %v %v", base.GetName(), assetTypes[i], p)
@@ -858,7 +864,7 @@ func outputToConsole(exchangeResponses []ExchangeResponses) {
log.Printf("%v Result: %v", exchangeResponses[i].ExchangeName, k)
log.Printf("Function:\t%v", exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].Function)
log.Printf("AssetType:\t%v", exchangeResponses[i].AssetPairResponses[j].AssetType)
log.Printf("Currency:\t%v\n", exchangeResponses[i].AssetPairResponses[j].CurrencyPair)
log.Printf("Currency:\t%v\n", exchangeResponses[i].AssetPairResponses[j].Pair)
log.Printf("Wrapper Params:\t%s\n", exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].SentParams)
if exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].Error != "" {
totalErrors++

View File

@@ -69,10 +69,10 @@
{{ if eq $length 1 }}
{{ else }}
<div class="col-11 container-fluid">
<h4> {{ $assetPairResponses.AssetType }} {{ $assetPairResponses.CurrencyPair }} Results </h4>
<h4> {{ $assetPairResponses.AssetType }} {{ $assetPairResponses.Pair}} Results </h4>
<div class="alert col-6 container-fluid {{ if $assetPairResponses.ErrorCount }} alert-danger {{ else }} alert-success {{ end }}"
role="alert">
{{ $assetPairResponses.AssetType }} {{ $assetPairResponses.CurrencyPair }} Total Errors:
{{ $assetPairResponses.AssetType }} {{ $assetPairResponses.Pair}} Total Errors:
{{ $assetPairResponses.ErrorCount }}
</div>
</div>
@@ -108,18 +108,18 @@
{{ $assetPairResponses.AssetType }}
</td>
<td>
{{ $assetPairResponses.CurrencyPair }}
{{ $assetPairResponses.Pair}}
</td>
<td>
{{ $endpointResponse.Function }}
</td>
<td>
<button class="btn btn-info" type="button" data-toggle="collapse"
data-target="#{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.CurrencyPair }}{{ $endpointResponse.Function }}sent"
data-target="#{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.Pair}}{{ $endpointResponse.Function }}sent"
aria-expanded="false" aria-controls="collapseExample">
Wrapper Params
</button>
<div id="{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.CurrencyPair }}{{ $endpointResponse.Function }}sent"
<div id="{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.Pair}}{{ $endpointResponse.Function }}sent"
class="collapse">
<div class="card card-body" style="word-wrap:anywhere">
{{ $endpointResponse.SentParams | printf "%s" }}
@@ -131,11 +131,11 @@
</td>
<td>
<button class="btn btn-info" type="button" data-toggle="collapse"
data-target="#{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.CurrencyPair }}{{ $endpointResponse.Function }}"
data-target="#{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.Pair}}{{ $endpointResponse.Function }}"
aria-expanded="false" aria-controls="collapseExample">
Wrapper Response
</button>
<div id="{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.CurrencyPair }}{{ $endpointResponse.Function }}"
<div id="{{ $assetPairResponses.AssetType }}{{ $assetPairResponses.Pair}}{{ $endpointResponse.Function }}"
class="collapse">
<div class="card card-body" style="word-wrap:anywhere">
{{ $endpointResponse.Response | printf "%s" }}

View File

@@ -58,7 +58,7 @@ type ExchangeResponses struct {
type ExchangeAssetPairResponses struct {
ErrorCount int64 `json:"errorCount"`
AssetType asset.Item `json:"asset"`
CurrencyPair currency.Pair `json:"currency"`
Pair currency.Pair `json:"currency"`
EndpointResponses []EndpointResponse `json:"responses"`
}

View File

@@ -7,9 +7,7 @@ import (
"io/ioutil"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
@@ -566,7 +564,11 @@ func getTicker(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetTicker(context.Background(),
&gctrpc.GetTickerRequest{
@@ -679,7 +681,11 @@ func getOrderbook(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetOrderbook(context.Background(),
&gctrpc.GetOrderbookRequest{
@@ -1199,7 +1205,11 @@ func getOrders(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetOrders(context.Background(), &gctrpc.GetOrdersRequest{
Exchange: exchangeName,
@@ -1407,7 +1417,11 @@ func submitOrder(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.SubmitOrder(context.Background(), &gctrpc.SubmitOrderRequest{
Exchange: exchangeName,
@@ -1516,7 +1530,11 @@ func simulateOrder(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.SimulateOrder(context.Background(), &gctrpc.SimulateOrderRequest{
Exchange: exchangeName,
@@ -1618,7 +1636,11 @@ func whaleBomb(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.WhaleBomb(context.Background(), &gctrpc.WhaleBombRequest{
Exchange: exchangeName,
@@ -1750,7 +1772,11 @@ func cancelOrder(c *cli.Context) error {
if !validPair(currencyPair) {
return errInvalidPair
}
p = currency.NewPairDelimiter(currencyPair, pairDelimiter)
var err error
p, err = currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
}
conn, err := setupClient()
@@ -1985,7 +2011,11 @@ func addEvent(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.AddEvent(context.Background(), &gctrpc.AddEventRequest{
Exchange: exchangeName,
@@ -2776,249 +2806,6 @@ func setLoggerDetails(c *cli.Context) error {
return nil
}
var getExchangePairsCommand = cli.Command{
Name: "getexchangepairs",
Usage: "gets an exchanges supported currency pairs (available and enabled) plus asset types",
ArgsUsage: "<exchange> <asset>",
Action: getExchangePairs,
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to list of the currency pairs of",
},
cli.StringFlag{
Name: "asset",
Usage: "the asset type to filter by",
},
},
}
func getExchangePairs(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
cli.ShowCommandHelp(c, "getexchangepairs")
return nil
}
var exchange string
var asset string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
if c.IsSet("asset") {
asset = c.String("asset")
} else {
asset = c.Args().Get(1)
}
asset = strings.ToLower(asset)
if !validAsset(asset) {
return errInvalidAsset
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetExchangePairs(context.Background(),
&gctrpc.GetExchangePairsRequest{
Exchange: exchange,
Asset: asset,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var enableExchangePairCommand = cli.Command{
Name: "enableexchangepair",
Usage: "enables an exchange currency pair",
ArgsUsage: "<exchange> <pair> <asset>",
Action: enableExchangePair,
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to enable the currency pair for",
},
cli.StringFlag{
Name: "pair",
Usage: "the currency pair to enable",
},
cli.StringFlag{
Name: "asset",
Usage: "the asset type to enable the currency pair for",
},
},
}
func enableExchangePair(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
cli.ShowCommandHelp(c, "enableexchangepair")
return nil
}
var exchange string
var pair string
var asset string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
if c.IsSet("pair") {
pair = c.String("pair")
} else {
pair = c.Args().Get(1)
}
if !validPair(pair) {
return errInvalidPair
}
if c.IsSet("asset") {
asset = c.String("asset")
} else {
asset = c.Args().Get(2)
}
asset = strings.ToLower(asset)
if !validAsset(asset) {
return errInvalidAsset
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
p := currency.NewPairDelimiter(pair, pairDelimiter)
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.EnableExchangePair(context.Background(),
&gctrpc.ExchangePairRequest{
Exchange: exchange,
Pair: &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
},
AssetType: asset,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var disableExchangePairCommand = cli.Command{
Name: "disableexchangepair",
Usage: "disables a previously enabled exchange currency pair",
ArgsUsage: "<exchange> <pair> <asset>",
Action: disableExchangePair,
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to disable the currency pair for",
},
cli.StringFlag{
Name: "pair",
Usage: "the currency pair to disable",
},
cli.StringFlag{
Name: "asset",
Usage: "the asset type to disable the currency pair for",
},
},
}
func disableExchangePair(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
cli.ShowCommandHelp(c, "disableexchangepair")
return nil
}
var exchange string
var pair string
var asset string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
if c.IsSet("pair") {
pair = c.String("pair")
} else {
pair = c.Args().Get(1)
}
if !validPair(pair) {
return errInvalidPair
}
if c.IsSet("asset") {
asset = c.String("asset")
} else {
asset = c.Args().Get(2)
}
asset = strings.ToLower(asset)
if !validAsset(asset) {
return errInvalidAsset
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
p := currency.NewPairDelimiter(pair, pairDelimiter)
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.DisableExchangePair(context.Background(),
&gctrpc.ExchangePairRequest{
Exchange: exchange,
Pair: &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
},
AssetType: asset,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var getOrderbookStreamCommand = cli.Command{
Name: "getorderbookstream",
Usage: "gets the orderbook stream for a specific currency pair and exchange",
@@ -3088,7 +2875,10 @@ func getOrderbookStream(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(pair, pairDelimiter)
p, err := currency.NewPairDelimiter(pair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetOrderbookStream(context.Background(),
@@ -3296,7 +3086,10 @@ func getTickerStream(c *cli.Context) error {
}
defer conn.Close()
p := currency.NewPairDelimiter(pair, pairDelimiter)
p, err := currency.NewPairDelimiter(pair, pairDelimiter)
if err != nil {
return err
}
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetTickerStream(context.Background(),
@@ -3410,19 +3203,6 @@ func getExchangeTickerStream(c *cli.Context) error {
}
}
func clearScreen() error {
switch runtime.GOOS {
case "windows":
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
return cmd.Run()
default:
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
return cmd.Run()
}
}
var getAuditEventCommand = cli.Command{
Name: "getauditevent",
Usage: "gets audit events matching query parameters",
@@ -3529,8 +3309,8 @@ func getAuditEvent(c *cli.Context) error {
var uuid, filename, path string
var gctScriptCommand = cli.Command{
Name: "gctscript",
Usage: "execute gctscript command",
Name: "script",
Usage: "execute scripting management command",
ArgsUsage: "<command> <args>",
Subcommands: []cli.Command{
{
@@ -4022,7 +3802,10 @@ func getHistoricCandles(c *cli.Context) error {
if !validPair(currencyPair) {
return errInvalidPair
}
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
var assetType string
if c.IsSet("asset") {
@@ -4038,7 +3821,6 @@ func getHistoricCandles(c *cli.Context) error {
if c.IsSet("rangesize") {
candleRangeSize = c.Int64("rangesize")
} else if c.Args().Get(3) != "" {
var err error
candleRangeSize, err = strconv.ParseInt(c.Args().Get(3), 10, 64)
if err != nil {
return err
@@ -4048,7 +3830,6 @@ func getHistoricCandles(c *cli.Context) error {
if c.IsSet("granularity") {
candleGranularity = c.Int64("granularity")
} else if c.Args().Get(4) != "" {
var err error
candleGranularity, err = strconv.ParseInt(c.Args().Get(4), 10, 64)
if err != nil {
return err
@@ -4151,7 +3932,11 @@ func getHistoricCandlesExtended(c *cli.Context) error {
if !validPair(currencyPair) {
return errInvalidPair
}
p := currency.NewPairDelimiter(currencyPair, pairDelimiter)
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
var assetType string
if c.IsSet("asset") {
@@ -4167,7 +3952,6 @@ func getHistoricCandlesExtended(c *cli.Context) error {
if c.IsSet("interval") {
candleGranularity = c.Int64("interval")
} else if c.Args().Get(3) != "" {
var err error
candleGranularity, err = strconv.ParseInt(c.Args().Get(3), 10, 64)
if err != nil {
return err

20
cmd/gctcli/helpers.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"os"
"os/exec"
"runtime"
)
func clearScreen() error {
switch runtime.GOOS {
case "windows":
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
return cmd.Run()
default:
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
return cmd.Run()
}
}

View File

@@ -127,9 +127,7 @@ func main() {
withdrawalRequestCommand,
getLoggerDetailsCommand,
setLoggerDetailsCommand,
getExchangePairsCommand,
enableExchangePairCommand,
disableExchangePairCommand,
exchangePairManagerCommand,
getOrderbookStreamCommand,
getExchangeOrderbookStreamCommand,
getTickerStreamCommand,
@@ -138,6 +136,7 @@ func main() {
getHistoricCandlesCommand,
getHistoricCandlesExtendedCommand,
gctScriptCommand,
websocketManagerCommand,
}
err := app.Run(os.Args)

View File

@@ -0,0 +1,456 @@
package main
import (
"context"
"strings"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
"github.com/urfave/cli"
)
var exchangePairManagerCommand = cli.Command{
Name: "pair",
Usage: "execute exchange pair management command",
ArgsUsage: "<command> <args>",
Subcommands: []cli.Command{
{
Name: "get",
Usage: "returns all enabled and available pairs by asset type",
ArgsUsage: "<asset>",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "asset",
Usage: "asset",
},
},
Action: getExchangePairs,
},
{
Name: "disableasset",
Usage: "disables asset type",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "asset",
Usage: "asset",
},
},
Action: enableDisableExchangeAsset,
},
{
Name: "enableasset",
Usage: "enables asset type",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "asset",
Usage: "asset",
},
cli.BoolTFlag{
Name: "enable",
Hidden: true,
},
},
Action: enableDisableExchangeAsset,
},
{
Name: "disable",
Usage: "disable pairs by asset type",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "pairs",
Usage: "either a single currency pair string or comma delimiter string of pairs e.g. \"BTC-USD,XRP-USD\"",
},
cli.StringFlag{
Name: "asset",
Usage: "asset",
},
},
Action: enableDisableExchangePair,
},
{
Name: "enable",
Usage: "enable pairs by asset type",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "pairs",
Usage: "either a single currency pair string or comma delimiter string of pairs e.g. \"BTC-USD,XRP-USD\"",
},
cli.StringFlag{
Name: "asset",
Usage: "asset",
},
cli.BoolTFlag{
Name: "enable",
Hidden: true,
},
},
Action: enableDisableExchangePair,
},
{
Name: "enableall",
Usage: "enable all pairs",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.BoolTFlag{
Name: "enable",
Hidden: true,
},
},
Action: enableDisableAllExchangePairs,
},
{
Name: "disableall",
Usage: "dissable all pairs",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
},
Action: enableDisableAllExchangePairs,
},
{
Name: "update",
Usage: "fetches supported pairs from the exchange and updates available pairs and removes unsupported enable pairs",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
},
Action: updateExchangeSupportedPairs,
},
{
Name: "getassets",
Usage: "fetches supported assets",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
},
Action: getExchangeAssets,
},
},
}
func enableDisableExchangePair(c *cli.Context) error {
enable := c.BoolT("enable")
if c.NArg() == 0 && c.NumFlags() == 0 {
if enable {
return cli.ShowCommandHelp(c, "enable")
}
return cli.ShowCommandHelp(c, "disable")
}
var exchange string
var pairs string
var asset string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
if c.IsSet("pairs") {
pairs = c.String("pairs")
} else {
pairs = c.Args().Get(1)
}
if c.IsSet("asset") {
asset = c.String("asset")
} else {
asset = c.Args().Get(2)
}
asset = strings.ToLower(asset)
if !validAsset(asset) {
return errInvalidAsset
}
pairList := strings.Split(pairs, ",")
var validPairs []*gctrpc.CurrencyPair
for i := range pairList {
if !validPair(pairList[i]) {
return errInvalidPair
}
p, err := currency.NewPairFromString(pairList[i])
if err != nil {
return err
}
validPairs = append(validPairs, &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
})
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.SetExchangePair(context.Background(),
&gctrpc.SetExchangePairRequest{
Exchange: exchange,
Pairs: validPairs,
AssetType: asset,
Enable: enable,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func getExchangePairs(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
var asset string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
if c.IsSet("asset") {
asset = c.String("asset")
} else {
asset = c.Args().Get(1)
}
asset = strings.ToLower(asset)
if !validAsset(asset) {
return errInvalidAsset
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetExchangePairs(context.Background(),
&gctrpc.GetExchangePairsRequest{
Exchange: exchange,
Asset: asset,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func enableDisableExchangeAsset(c *cli.Context) error {
enable := c.BoolT("enable")
if c.NArg() == 0 && c.NumFlags() == 0 {
if enable {
return cli.ShowCommandHelp(c, "enableasset")
}
return cli.ShowCommandHelp(c, "disableasset")
}
var exchange string
var asset string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
if c.IsSet("asset") {
asset = c.String("asset")
} else {
asset = c.Args().Get(1)
}
asset = strings.ToLower(asset)
if !validAsset(asset) {
return errInvalidAsset
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.SetExchangeAsset(context.Background(),
&gctrpc.SetExchangeAssetRequest{
Exchange: exchange,
Asset: asset,
Enable: enable,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func enableDisableAllExchangePairs(c *cli.Context) error {
enable := c.BoolT("enable")
if c.NArg() == 0 && c.NumFlags() == 0 {
if enable {
return cli.ShowCommandHelp(c, "enableall")
}
return cli.ShowCommandHelp(c, "disableall")
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.SetAllExchangePairs(context.Background(),
&gctrpc.SetExchangeAllPairsRequest{
Exchange: exchange,
Enable: enable,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func updateExchangeSupportedPairs(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.UpdateExchangeSupportedPairs(context.Background(),
&gctrpc.UpdateExchangeSupportedPairsRequest{
Exchange: exchange,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func getExchangeAssets(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return errInvalidExchange
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.GetExchangeAssets(context.Background(),
&gctrpc.GetExchangeAssetsRequest{
Exchange: exchange,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}

View File

@@ -0,0 +1,270 @@
package main
import (
"context"
"fmt"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
"github.com/urfave/cli"
)
var websocketManagerCommand = cli.Command{
Name: "websocket",
Usage: "execute websocket management command",
ArgsUsage: "<command> <args>",
Subcommands: []cli.Command{
{
Name: "getinfo",
Usage: "returns all exchange websocket information",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
},
Action: getwebsocketInfo,
},
{
Name: "disable",
Usage: "disables websocket connection for an exchange",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
},
Action: enableDisableWebsocket,
},
{
Name: "enable",
Usage: "enables websocket connection for an exchange",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.BoolTFlag{
Name: "enable",
Hidden: true,
},
},
Action: enableDisableWebsocket,
},
{
Name: "getsubs",
Usage: "returns current subscriptions for an exchange",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
},
Action: getSubscriptions,
},
{
Name: "setproxy",
Usage: "sets exchange websocket proxy, flushes and reroutes connection",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "proxy",
Usage: "proxy address to change to, if proxy string is not set, this will stop the utilization of the prior set proxy.",
},
},
Action: setProxy,
},
{
Name: "seturl",
Usage: "sets exchange websocket endpoint URL and resets the websocket connection",
Flags: []cli.Flag{
cli.StringFlag{
Name: "exchange",
Usage: "the exchange to act on",
},
cli.StringFlag{
Name: "url",
Usage: "url string to change to, an empty string will set it back to the packaged defined default",
},
},
Action: setURL,
},
},
}
func getwebsocketInfo(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return fmt.Errorf("[%s] is not a valid exchange", exchange)
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.WebsocketGetInfo(context.Background(),
&gctrpc.WebsocketGetInfoRequest{Exchange: exchange})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func enableDisableWebsocket(c *cli.Context) error {
enable := c.BoolT("enable")
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return fmt.Errorf("[%s] is not a valid exchange", exchange)
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.WebsocketSetEnabled(context.Background(),
&gctrpc.WebsocketSetEnabledRequest{Exchange: exchange, Enable: enable})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func getSubscriptions(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return fmt.Errorf("[%s] is not a valid exchange", exchange)
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.WebsocketGetSubscriptions(context.Background(),
&gctrpc.WebsocketGetSubscriptionsRequest{Exchange: exchange})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func setProxy(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return fmt.Errorf("[%s] is not a valid exchange", exchange)
}
var proxy string
if c.IsSet("proxy") {
proxy = c.String("proxy")
} else {
proxy = c.Args().Get(1)
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.WebsocketSetProxy(context.Background(),
&gctrpc.WebsocketSetProxyRequest{Exchange: exchange, Proxy: proxy})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
func setURL(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var exchange string
if c.IsSet("exchange") {
exchange = c.String("exchange")
} else {
exchange = c.Args().First()
}
if !validExchange(exchange) {
return fmt.Errorf("[%s] is not a valid exchange", exchange)
}
var url string
if c.IsSet("url") {
url = c.String("url")
} else {
url = c.Args().Get(1)
}
conn, err := setupClient()
if err != nil {
return err
}
defer conn.Close()
client := gctrpc.NewGoCryptoTraderClient(conn)
result, err := client.WebsocketSetURL(context.Background(),
&gctrpc.WebsocketSetURLRequest{Exchange: exchange, Url: url})
if err != nil {
return err
}
jsonOutput(result)
return nil
}

View File

@@ -361,3 +361,18 @@ func InArray(val, array interface{}) (exists bool, index int) {
}
return
}
// Errors defines multiple errors
type Errors []error
// Error implements error interface
func (e Errors) Error() string {
if len(e) == 0 {
return ""
}
var r string
for i := range e {
r += e[i].Error() + ", "
}
return r[:len(r)-2]
}

View File

@@ -1,6 +1,7 @@
package common
import (
"errors"
"net/url"
"os"
"os/user"
@@ -547,3 +548,18 @@ func TestInArray(t *testing.T) {
t.Errorf("found a non existent value in the slice")
}
}
func TestErrors(t *testing.T) {
var test Errors
if test.Error() != "" {
t.Fatal("string should be nil")
}
test = append(test, errors.New("test1"))
if test.Error() != "test1" {
t.Fatal("does not match error")
}
test = append(test, errors.New("test2"))
if test.Error() != "test1, test2" {
t.Fatal("does not match error")
}
}

View File

@@ -345,71 +345,46 @@ func (c *Config) GetExchangeAssetTypes(exchName string) (asset.Items, error) {
return nil, fmt.Errorf("exchange %s currency pairs is nil", exchName)
}
return exchCfg.CurrencyPairs.AssetTypes, nil
return exchCfg.CurrencyPairs.GetAssetTypes(), nil
}
// SupportsExchangeAssetType returns whether or not the exchange supports the supplied asset type
func (c *Config) SupportsExchangeAssetType(exchName string, assetType asset.Item) (bool, error) {
func (c *Config) SupportsExchangeAssetType(exchName string, assetType asset.Item) error {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return false, err
return err
}
if exchCfg.CurrencyPairs == nil {
return false, fmt.Errorf("exchange %s currency pairs is nil", exchName)
return fmt.Errorf("exchange %s currency pairs is nil", exchName)
}
if !asset.IsValid(assetType) {
return false, fmt.Errorf("exchange %s invalid asset types", exchName)
return fmt.Errorf("exchange %s invalid asset type %s",
exchName,
assetType)
}
return exchCfg.CurrencyPairs.AssetTypes.Contains(assetType), nil
}
// CheckExchangeAssetsConsistency checks the exchanges supported assets compared to the stored
// entries and removes any non supported
func (c *Config) CheckExchangeAssetsConsistency(exchName string) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return
}
exchangeAssetTypes, err := c.GetExchangeAssetTypes(exchName)
if err != nil {
return
}
storedAssetTypes := exchCfg.CurrencyPairs.GetAssetTypes()
for x := range storedAssetTypes {
if !exchangeAssetTypes.Contains(storedAssetTypes[x]) {
log.Warnf(log.ConfigMgr,
"%s has non-needed stored asset type %v. Removing..\n",
exchName, storedAssetTypes[x])
exchCfg.CurrencyPairs.Delete(storedAssetTypes[x])
}
if !exchCfg.CurrencyPairs.GetAssetTypes().Contains(assetType) {
return fmt.Errorf("exchange %s unsupported asset type %s",
exchName,
assetType)
}
return nil
}
// SetPairs sets the exchanges currency pairs
func (c *Config) SetPairs(exchName string, assetType asset.Item, enabled bool, pairs currency.Pairs) error {
if len(pairs) == 0 {
return fmt.Errorf("pairs is nil")
}
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return err
}
supports, err := c.SupportsExchangeAssetType(exchName, assetType)
err = c.SupportsExchangeAssetType(exchName, assetType)
if err != nil {
return err
}
if !supports {
return fmt.Errorf("exchange %s does not support asset type %v", exchName, assetType)
}
exchCfg.CurrencyPairs.StorePairs(assetType, pairs, enabled)
return nil
}
@@ -421,16 +396,12 @@ func (c *Config) GetCurrencyPairConfig(exchName string, assetType asset.Item) (*
return nil, err
}
supports, err := c.SupportsExchangeAssetType(exchName, assetType)
err = c.SupportsExchangeAssetType(exchName, assetType)
if err != nil {
return nil, err
}
if !supports {
return nil, fmt.Errorf("exchange %s does not support asset type %v", exchName, assetType)
}
return exchCfg.CurrencyPairs.Get(assetType), nil
return exchCfg.CurrencyPairs.Get(assetType)
}
// CheckPairConfigFormats checks to see if the pair config format is valid
@@ -508,60 +479,128 @@ func (c *Config) CheckPairConsistency(exchName string) error {
return err
}
var atLeastOneEnabled bool
for x := range assetTypes {
enabledPairs, err := c.GetEnabledPairs(exchName, assetTypes[x])
if err == nil {
if len(enabledPairs) != 0 {
atLeastOneEnabled = true
continue
}
var enabled bool
enabled, err = c.AssetTypeEnabled(assetTypes[x], exchName)
if err != nil {
return err
}
if !enabled {
continue
}
var availPairs currency.Pairs
availPairs, err = c.GetAvailablePairs(exchName, assetTypes[x])
if err != nil {
return err
}
err = c.SetPairs(exchName,
assetTypes[x],
true,
currency.Pairs{availPairs.GetRandomPair()})
if err != nil {
return err
}
atLeastOneEnabled = true
continue
}
// On error an enabled pair is not found in the available pairs list
// so remove and report
availPairs, err := c.GetAvailablePairs(exchName, assetTypes[x])
if err != nil {
return err
}
availPairs, _ := c.GetAvailablePairs(exchName, assetTypes[x])
if len(availPairs) == 0 {
continue
}
var pairs, pairsRemoved currency.Pairs
update := false
if len(enabledPairs) > 0 {
for x := range enabledPairs {
if !availPairs.Contains(enabledPairs[x], true) {
update = true
pairsRemoved = append(pairsRemoved, enabledPairs[x])
continue
}
pairs = append(pairs, enabledPairs[x])
for x := range enabledPairs {
if !availPairs.Contains(enabledPairs[x], true) {
pairsRemoved = append(pairsRemoved, enabledPairs[x])
continue
}
} else {
update = true
pairs = append(pairs, enabledPairs[x])
}
if !update {
if len(pairsRemoved) == 0 {
return fmt.Errorf("check pair consistency fault for asset %s, conflict found but no pairs removed",
assetTypes[x])
}
// Flush corrupted/misspelled enabled pairs in config
err = c.SetPairs(exchName, assetTypes[x], true, pairs)
if err != nil {
return err
}
log.Warnf(log.ConfigMgr,
"Exchange %s: [%v] Removing enabled pair(s) %v from enabled pairs list, as it isn't located in the available pairs list.\n",
exchName,
assetTypes[x],
pairsRemoved.Strings())
if len(pairs) != 0 {
atLeastOneEnabled = true
continue
}
if len(pairs) == 0 || len(enabledPairs) == 0 {
newPair := availPairs.GetRandomPair()
c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{newPair})
log.Warnf(log.ExchangeSys, "Exchange %s: [%v] No enabled pairs found in available pairs, randomly added %v pair.\n",
exchName, assetTypes[x], newPair)
continue
} else {
c.SetPairs(exchName, assetTypes[x], true, pairs)
enabled, err := c.AssetTypeEnabled(assetTypes[x], exchName)
if err != nil {
return err
}
log.Warnf(log.ExchangeSys, "Exchange %s: [%v] Removing enabled pair(s) %v from enabled pairs as it isn't an available pair.\n",
exchName, assetTypes[x], pairsRemoved.Strings())
if !enabled {
continue
}
err = c.SetPairs(exchName,
assetTypes[x],
true,
currency.Pairs{availPairs.GetRandomPair()})
if err != nil {
return err
}
atLeastOneEnabled = true
}
// If no pair is enabled across the entire range of assets, then atleast
// enable one and turn on the asset type
if !atLeastOneEnabled {
avail, err := c.GetAvailablePairs(exchName, assetTypes[0])
if err != nil {
return err
}
newPair := avail.GetRandomPair()
err = c.SetPairs(exchName, assetTypes[0], true, currency.Pairs{newPair})
if err != nil {
return err
}
log.Warnf(log.ConfigMgr,
"Exchange %s: [%v] No enabled pairs found in available pairs list, randomly added %v pair.\n",
exchName,
assetTypes[0],
newPair)
}
return nil
}
// SupportsPair returns true or not whether the exchange supports the supplied
// pair
func (c *Config) SupportsPair(exchName string, p currency.Pair, assetType asset.Item) (bool, error) {
func (c *Config) SupportsPair(exchName string, p currency.Pair, assetType asset.Item) bool {
pairs, err := c.GetAvailablePairs(exchName, assetType)
if err != nil {
return false, err
return false
}
return pairs.Contains(p, false), nil
return pairs.Contains(p, false)
}
// GetPairFormat returns the exchanges pair config storage format
@@ -571,25 +610,31 @@ func (c *Config) GetPairFormat(exchName string, assetType asset.Item) (currency.
return currency.PairFormat{}, err
}
supports, err := c.SupportsExchangeAssetType(exchName, assetType)
err = c.SupportsExchangeAssetType(exchName, assetType)
if err != nil {
return currency.PairFormat{}, err
}
if !supports {
return currency.PairFormat{},
fmt.Errorf("exchange %s does not support asset type %s", exchName,
assetType)
}
if exchCfg.CurrencyPairs.UseGlobalFormat {
return *exchCfg.CurrencyPairs.ConfigFormat, nil
}
p := exchCfg.CurrencyPairs.Get(assetType)
p, err := exchCfg.CurrencyPairs.Get(assetType)
if err != nil {
return currency.PairFormat{}, err
}
if p == nil {
return currency.PairFormat{},
fmt.Errorf("exchange %s pair store for asset type %s is nil", exchName,
fmt.Errorf("exchange %s pair store for asset type %s is nil",
exchName,
assetType)
}
if p.ConfigFormat == nil {
return currency.PairFormat{},
fmt.Errorf("exchange %s pair config format for asset type %s is nil",
exchName,
assetType)
}
@@ -608,7 +653,11 @@ func (c *Config) GetAvailablePairs(exchName string, assetType asset.Item) (curre
return nil, err
}
pairs := exchCfg.CurrencyPairs.GetPairs(assetType, false)
pairs, err := exchCfg.CurrencyPairs.GetPairs(assetType, false)
if err != nil {
return nil, err
}
if pairs == nil {
return nil, nil
}
@@ -618,7 +667,7 @@ func (c *Config) GetAvailablePairs(exchName string, assetType asset.Item) (curre
}
// GetEnabledPairs returns a list of currency pairs for a specifc exchange
func (c *Config) GetEnabledPairs(exchName string, assetType asset.Item) ([]currency.Pair, error) {
func (c *Config) GetEnabledPairs(exchName string, assetType asset.Item) (currency.Pairs, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
@@ -629,13 +678,19 @@ func (c *Config) GetEnabledPairs(exchName string, assetType asset.Item) ([]curre
return nil, err
}
pairs := exchCfg.CurrencyPairs.GetPairs(assetType, true)
pairs, err := exchCfg.CurrencyPairs.GetPairs(assetType, true)
if err != nil {
return pairs, err
}
if pairs == nil {
return nil, nil
}
return pairs.Format(pairFormat.Delimiter, pairFormat.Index,
pairFormat.Uppercase), nil
return pairs.Format(pairFormat.Delimiter,
pairFormat.Index,
pairFormat.Uppercase),
nil
}
// GetEnabledExchanges returns a list of enabled exchanges
@@ -843,16 +898,6 @@ func (c *Config) CheckExchangeConfigValues() error {
c.Exchanges[i].CurrencyPairs.ConfigFormat = c.Exchanges[i].ConfigCurrencyPairFormat
c.Exchanges[i].CurrencyPairs.RequestFormat = c.Exchanges[i].RequestCurrencyPairFormat
if c.Exchanges[i].AssetTypes == nil {
c.Exchanges[i].CurrencyPairs.AssetTypes = asset.Items{
asset.Spot,
}
} else {
c.Exchanges[i].CurrencyPairs.AssetTypes = asset.New(
strings.ToLower(*c.Exchanges[i].AssetTypes),
)
}
var availPairs, enabledPairs currency.Pairs
if c.Exchanges[i].AvailablePairs != nil {
availPairs = *c.Exchanges[i].AvailablePairs
@@ -865,8 +910,9 @@ func (c *Config) CheckExchangeConfigValues() error {
c.Exchanges[i].CurrencyPairs.UseGlobalFormat = true
c.Exchanges[i].CurrencyPairs.Store(asset.Spot,
currency.PairStore{
Available: availPairs,
Enabled: enabledPairs,
AssetEnabled: convert.BoolPtr(true),
Available: availPairs,
Enabled: enabledPairs,
},
)
@@ -877,6 +923,50 @@ func (c *Config) CheckExchangeConfigValues() error {
c.Exchanges[i].AssetTypes = nil
c.Exchanges[i].AvailablePairs = nil
c.Exchanges[i].EnabledPairs = nil
} else {
assets := c.Exchanges[i].CurrencyPairs.GetAssetTypes()
var atLeastOne bool
for index := range assets {
err := c.Exchanges[i].CurrencyPairs.IsAssetEnabled(assets[index])
if err != nil {
// Checks if we have an old config without the ability to
// enable disable the entire asset
if err.Error() == "cannot ascertain if asset is enabled, variable is nil" {
log.Warnf(log.ConfigMgr,
"Exchange %s: upgrading config for asset type %s and setting enabled.\n",
c.Exchanges[i].Name,
assets[index])
err = c.Exchanges[i].CurrencyPairs.SetAssetEnabled(assets[index], true)
if err != nil {
return err
}
atLeastOne = true
}
continue
}
atLeastOne = true
}
if !atLeastOne {
if len(assets) == 0 {
c.Exchanges[i].Enabled = false
log.Warnf(log.ConfigMgr,
"%s no assets found, disabling...",
c.Exchanges[i].Name)
continue
}
// turn on an asset if all disabled
log.Warnf(log.ConfigMgr,
"%s assets disabled, turning on asset %s",
c.Exchanges[i].Name,
assets[0])
err := c.Exchanges[i].CurrencyPairs.SetAssetEnabled(assets[0], true)
if err != nil {
return err
}
}
}
if c.Exchanges[i].Enabled {
@@ -885,71 +975,101 @@ func (c *Config) CheckExchangeConfigValues() error {
c.Exchanges[i].Enabled = false
continue
}
if (c.Exchanges[i].API.AuthenticatedSupport || c.Exchanges[i].API.AuthenticatedWebsocketSupport) && c.Exchanges[i].API.CredentialsValidator != nil {
if (c.Exchanges[i].API.AuthenticatedSupport || c.Exchanges[i].API.AuthenticatedWebsocketSupport) &&
c.Exchanges[i].API.CredentialsValidator != nil {
var failed bool
if c.Exchanges[i].API.CredentialsValidator.RequiresKey && (c.Exchanges[i].API.Credentials.Key == "" || c.Exchanges[i].API.Credentials.Key == DefaultAPIKey) {
if c.Exchanges[i].API.CredentialsValidator.RequiresKey &&
(c.Exchanges[i].API.Credentials.Key == "" || c.Exchanges[i].API.Credentials.Key == DefaultAPIKey) {
failed = true
}
if c.Exchanges[i].API.CredentialsValidator.RequiresSecret && (c.Exchanges[i].API.Credentials.Secret == "" || c.Exchanges[i].API.Credentials.Secret == DefaultAPISecret) {
if c.Exchanges[i].API.CredentialsValidator.RequiresSecret &&
(c.Exchanges[i].API.Credentials.Secret == "" || c.Exchanges[i].API.Credentials.Secret == DefaultAPISecret) {
failed = true
}
if c.Exchanges[i].API.CredentialsValidator.RequiresClientID && (c.Exchanges[i].API.Credentials.ClientID == DefaultAPIClientID || c.Exchanges[i].API.Credentials.ClientID == "") {
if c.Exchanges[i].API.CredentialsValidator.RequiresClientID &&
(c.Exchanges[i].API.Credentials.ClientID == DefaultAPIClientID || c.Exchanges[i].API.Credentials.ClientID == "") {
failed = true
}
if failed {
c.Exchanges[i].API.AuthenticatedSupport = false
c.Exchanges[i].API.AuthenticatedWebsocketSupport = false
log.Warnf(log.ExchangeSys, WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name)
log.Warnf(log.ConfigMgr, WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name)
}
}
if !c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates && !c.Exchanges[i].Features.Supports.WebsocketCapabilities.AutoPairUpdates {
if !c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates &&
!c.Exchanges[i].Features.Supports.WebsocketCapabilities.AutoPairUpdates {
lastUpdated := convert.UnixTimestampToTime(c.Exchanges[i].CurrencyPairs.LastUpdated)
lastUpdated = lastUpdated.AddDate(0, 0, pairsLastUpdatedWarningThreshold)
if lastUpdated.Unix() <= time.Now().Unix() {
log.Warnf(log.ExchangeSys, WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, pairsLastUpdatedWarningThreshold)
log.Warnf(log.ConfigMgr,
WarningPairsLastUpdatedThresholdExceeded,
c.Exchanges[i].Name,
pairsLastUpdatedWarningThreshold)
}
}
if c.Exchanges[i].HTTPTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Timeout value not set, defaulting to %v.\n", c.Exchanges[i].Name, defaultHTTPTimeout)
log.Warnf(log.ConfigMgr,
"Exchange %s HTTP Timeout value not set, defaulting to %v.\n",
c.Exchanges[i].Name,
defaultHTTPTimeout)
c.Exchanges[i].HTTPTimeout = defaultHTTPTimeout
}
if c.Exchanges[i].WebsocketResponseCheckTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response check timeout value not set, defaulting to %v.",
c.Exchanges[i].Name, defaultWebsocketResponseCheckTimeout)
log.Warnf(log.ConfigMgr,
"Exchange %s Websocket response check timeout value not set, defaulting to %v.",
c.Exchanges[i].Name,
defaultWebsocketResponseCheckTimeout)
c.Exchanges[i].WebsocketResponseCheckTimeout = defaultWebsocketResponseCheckTimeout
}
if c.Exchanges[i].WebsocketResponseMaxLimit <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response max limit value not set, defaulting to %v.",
c.Exchanges[i].Name, defaultWebsocketResponseMaxLimit)
log.Warnf(log.ConfigMgr,
"Exchange %s Websocket response max limit value not set, defaulting to %v.",
c.Exchanges[i].Name,
defaultWebsocketResponseMaxLimit)
c.Exchanges[i].WebsocketResponseMaxLimit = defaultWebsocketResponseMaxLimit
}
if c.Exchanges[i].WebsocketTrafficTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response traffic timeout value not set, defaulting to %v.",
c.Exchanges[i].Name, defaultWebsocketTrafficTimeout)
log.Warnf(log.ConfigMgr,
"Exchange %s Websocket response traffic timeout value not set, defaulting to %v.",
c.Exchanges[i].Name,
defaultWebsocketTrafficTimeout)
c.Exchanges[i].WebsocketTrafficTimeout = defaultWebsocketTrafficTimeout
}
if c.Exchanges[i].WebsocketOrderbookBufferLimit <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.",
c.Exchanges[i].Name, defaultWebsocketOrderbookBufferLimit)
log.Warnf(log.ConfigMgr,
"Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.",
c.Exchanges[i].Name,
defaultWebsocketOrderbookBufferLimit)
c.Exchanges[i].WebsocketOrderbookBufferLimit = defaultWebsocketOrderbookBufferLimit
}
err := c.CheckPairConsistency(c.Exchanges[i].Name)
if err != nil {
log.Errorf(log.ExchangeSys, "Exchange %s: CheckPairConsistency error: %s\n", c.Exchanges[i].Name, err)
log.Errorf(log.ConfigMgr,
"Exchange %s: CheckPairConsistency error: %s\n",
c.Exchanges[i].Name,
err)
c.Exchanges[i].Enabled = false
continue
}
c.CheckExchangeAssetsConsistency(c.Exchanges[i].Name)
for x := range c.Exchanges[i].BankAccounts {
if !c.Exchanges[i].BankAccounts[x].Enabled {
continue
}
err := c.Exchanges[i].BankAccounts[x].Validate()
if err != nil {
c.Exchanges[i].BankAccounts[x].Enabled = false
log.Warnln(log.ConfigMgr, err.Error())
}
}
exchanges++
}
}
if exchanges == 0 {
return errors.New(ErrNoEnabledExchanges)
}
@@ -1104,8 +1224,8 @@ func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool, assetType asset.I
continue
}
supports, _ := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType)
if !supports {
err := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType)
if err != nil {
continue
}
@@ -1118,13 +1238,12 @@ func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool, assetType asset.I
}
for x := range c.Exchanges {
supports, _ := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType)
if !supports {
err := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType)
if err != nil {
continue
}
var pairs []currency.Pair
var err error
if !c.Exchanges[x].Enabled && enabledOnly {
pairs, err = c.GetEnabledPairs(c.Exchanges[x].Name, assetType)
} else {
@@ -1475,7 +1594,7 @@ func (c *Config) ReadConfig(configPath string, dryrun bool) error {
}
if !ConfirmECS(fileData) {
err = ConfirmConfigJSON(fileData, &c)
err = json.Unmarshal(fileData, c)
if err != nil {
return err
}
@@ -1493,38 +1612,39 @@ func (c *Config) ReadConfig(configPath string, dryrun bool) error {
return c.SaveConfig(defaultPath, dryrun)
}
}
} else {
errCounter := 0
for {
if errCounter >= maxAuthFailures {
return errors.New("failed to decrypt config after 3 attempts")
}
key, err := PromptForConfigKey(IsInitialSetup)
if err != nil {
log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err)
errCounter++
continue
}
return nil
}
var f []byte
f = append(f, fileData...)
data, err := DecryptConfigFile(f, key)
if err != nil {
log.Errorf(log.ConfigMgr, "DecryptConfigFile err: %s", err)
errCounter++
continue
}
err = ConfirmConfigJSON(data, &c)
if err != nil {
if errCounter < maxAuthFailures {
log.Error(log.ConfigMgr, "Invalid password.")
}
errCounter++
continue
}
break
errCounter := 0
for {
if errCounter >= maxAuthFailures {
return errors.New("failed to decrypt config after 3 attempts")
}
key, err := PromptForConfigKey(IsInitialSetup)
if err != nil {
log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err)
errCounter++
continue
}
var f []byte
f = append(f, fileData...)
data, err := DecryptConfigFile(f, key)
if err != nil {
log.Errorf(log.ConfigMgr, "DecryptConfigFile err: %s", err)
errCounter++
continue
}
err = json.Unmarshal(data, c)
if err != nil {
if errCounter < maxAuthFailures {
log.Error(log.ConfigMgr, "Invalid password.")
}
errCounter++
continue
}
break
}
return nil
}
@@ -1611,12 +1731,16 @@ func (c *Config) CheckRemoteControlConfig() {
func (c *Config) CheckConfig() error {
err := c.CheckLoggerConfig()
if err != nil {
log.Errorf(log.ConfigMgr, "Failed to configure logger, some logging features unavailable: %s\n", err)
log.Errorf(log.ConfigMgr,
"Failed to configure logger, some logging features unavailable: %s\n",
err)
}
err = c.checkDatabaseConfig()
if err != nil {
log.Errorf(log.DatabaseMgr, "Failed to configure database: %v", err)
log.Errorf(log.DatabaseMgr,
"Failed to configure database: %v",
err)
}
err = c.CheckExchangeConfigValues()
@@ -1626,7 +1750,9 @@ func (c *Config) CheckConfig() error {
err = c.checkGCTScriptConfig()
if err != nil {
log.Errorf(log.Global, "Failed to configure gctscript, feature has been disabled: %s\n", err)
log.Errorf(log.Global,
"Failed to configure gctscript, feature has been disabled: %s\n",
err)
}
c.CheckConnectionMonitorConfig()
@@ -1641,7 +1767,9 @@ func (c *Config) CheckConfig() error {
}
if c.GlobalHTTPTimeout <= 0 {
log.Warnf(log.ConfigMgr, "Global HTTP Timeout value not set, defaulting to %v.\n", defaultHTTPTimeout)
log.Warnf(log.ConfigMgr,
"Global HTTP Timeout value not set, defaulting to %v.\n",
defaultHTTPTimeout)
c.GlobalHTTPTimeout = defaultHTTPTimeout
}
@@ -1703,3 +1831,17 @@ func (c *Config) RemoveExchange(exchName string) bool {
}
return false
}
// AssetTypeEnabled checks to see if the asset type is enabled in configuration
func (c *Config) AssetTypeEnabled(a asset.Item, exch string) (bool, error) {
cfg, err := c.GetExchangeConfig(exch)
if err != nil {
return false, err
}
err = cfg.CurrencyPairs.IsAssetEnabled(a)
if err != nil {
return false, nil
}
return true, nil
}

View File

@@ -5,7 +5,6 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
@@ -167,11 +166,6 @@ func DecryptConfigFile(configData, key []byte) ([]byte, error) {
return result, nil
}
// ConfirmConfigJSON confirms JSON in file
func ConfirmConfigJSON(file []byte, result interface{}) error {
return json.Unmarshal(file, &result)
}
// ConfirmSalt checks whether the encrypted data contains a salt
func ConfirmSalt(file []byte) bool {
return bytes.Contains(file, []byte(SaltPrefix))

View File

@@ -1,7 +1,6 @@
package config
import (
"io/ioutil"
"testing"
)
@@ -84,19 +83,6 @@ func TestDecryptConfigFile(t *testing.T) {
}
}
func TestConfirmConfigJSON(t *testing.T) {
var result interface{}
testConfirmJSON, err := ioutil.ReadFile(TestFile)
if err != nil {
t.Errorf("testConfirmJSON: %s", err)
}
err = ConfirmConfigJSON(testConfirmJSON, &result)
if err != nil || result == nil {
t.Errorf("testConfirmJSON: %s", err)
}
}
func TestConfirmECS(t *testing.T) {
t.Parallel()

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/connchecker"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
@@ -21,6 +22,7 @@ const (
defaultEnabledExchanges = 28
testFakeExchangeName = "Stampbit"
testPair = "BTC-USD"
testString = "test"
)
func TestGetCurrencyConfig(t *testing.T) {
@@ -32,6 +34,22 @@ func TestGetCurrencyConfig(t *testing.T) {
_ = cfg.GetCurrencyConfig()
}
func TestGetClientBankAccounts(t *testing.T) {
cfg := GetConfig()
err := cfg.LoadConfig(TestFile, true)
if err != nil {
t.Fatal("GetExchangeBankAccounts LoadConfig error", err)
}
_, err = cfg.GetClientBankAccounts("Kraken", "USD")
if err != nil {
t.Error("GetExchangeBankAccounts error", err)
}
_, err = cfg.GetClientBankAccounts("noob exchange", "USD")
if err == nil {
t.Fatal("error cannot be nil")
}
}
func TestGetExchangeBankAccounts(t *testing.T) {
cfg := GetConfig()
err := cfg.LoadConfig(TestFile, true)
@@ -48,6 +66,17 @@ func TestGetExchangeBankAccounts(t *testing.T) {
}
}
func TestCheckBankAccountConfig(t *testing.T) {
cfg := GetConfig()
err := cfg.LoadConfig(TestFile, true)
if err != nil {
t.Error("GetExchangeBankAccounts LoadConfig error", err)
}
cfg.BankAccounts[0].Enabled = true
cfg.CheckBankAccountConfig()
}
func TestUpdateExchangeBankAccounts(t *testing.T) {
cfg := GetConfig()
err := cfg.LoadConfig(TestFile, true)
@@ -84,7 +113,7 @@ func TestUpdateClientBankAccounts(t *testing.T) {
if err != nil {
t.Error("UpdateClientBankAccounts LoadConfig error", err)
}
b := banking.Account{Enabled: false, BankName: "test", AccountNumber: "0234"}
b := banking.Account{Enabled: false, BankName: testString, AccountNumber: "0234"}
err = cfg.UpdateClientBankAccounts(&b)
if err != nil {
t.Error("UpdateClientBankAccounts error", err)
@@ -185,7 +214,7 @@ func TestPurgeExchangeCredentials(t *testing.T) {
var c Config
c.Exchanges = []ExchangeConfig{
{
Name: "test",
Name: testString,
API: APIConfig{
AuthenticatedSupport: true,
AuthenticatedWebsocketSupport: true,
@@ -219,7 +248,7 @@ func TestPurgeExchangeCredentials(t *testing.T) {
c.PurgeExchangeAPICredentials()
exchCfg, err := c.GetExchangeConfig("test")
exchCfg, err := c.GetExchangeConfig(testString)
if err != nil {
t.Error(err)
}
@@ -257,8 +286,8 @@ func TestUpdateCommunicationsConfig(t *testing.T) {
if err != nil {
t.Error("UpdateCommunicationsConfig LoadConfig error", err)
}
cfg.UpdateCommunicationsConfig(&CommunicationsConfig{SlackConfig: SlackConfig{Name: "TEST"}})
if cfg.Communications.SlackConfig.Name != "TEST" {
cfg.UpdateCommunicationsConfig(&CommunicationsConfig{SlackConfig: SlackConfig{Name: testString}})
if cfg.Communications.SlackConfig.Name != testString {
t.Error("UpdateCommunicationsConfig LoadConfig error")
}
}
@@ -308,7 +337,7 @@ func TestCheckCommunicationsConfig(t *testing.T) {
cfg.SMS = &SMSGlobalConfig{}
cfg.Communications.SMSGlobalConfig.Name = ""
cfg.CheckCommunicationsConfig()
if cfg.Communications.SMSGlobalConfig.Password != "test" {
if cfg.Communications.SMSGlobalConfig.Password != testString {
t.Error("CheckCommunicationsConfig error:", err)
}
@@ -389,9 +418,9 @@ func TestGetExchangeAssetTypes(t *testing.T) {
ExchangeConfig{
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: new(currency.PairStore),
asset.Futures: new(currency.PairStore),
},
},
},
@@ -403,7 +432,7 @@ func TestGetExchangeAssetTypes(t *testing.T) {
t.Error(err)
}
if assets.JoinToString(",") != "spot,futures" {
if !assets.Contains(asset.Spot) || !assets.Contains(asset.Futures) {
t.Error("unexpected results")
}
@@ -417,7 +446,7 @@ func TestGetExchangeAssetTypes(t *testing.T) {
func TestSupportsExchangeAssetType(t *testing.T) {
t.Parallel()
var c Config
_, err := c.SupportsExchangeAssetType("void", asset.Spot)
err := c.SupportsExchangeAssetType("void", asset.Spot)
if err == nil {
t.Error("Expected error for non-existent exchange")
}
@@ -426,73 +455,30 @@ func TestSupportsExchangeAssetType(t *testing.T) {
ExchangeConfig{
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: new(currency.PairStore),
},
},
},
)
supports, err := c.SupportsExchangeAssetType(testFakeExchangeName, asset.Spot)
err = c.SupportsExchangeAssetType(testFakeExchangeName, asset.Spot)
if err != nil {
t.Error(err)
}
if !supports {
t.Error("exchange should support spot asset item")
}
_, err = c.SupportsExchangeAssetType(testFakeExchangeName, "asdf")
err = c.SupportsExchangeAssetType(testFakeExchangeName, "asdf")
if err == nil {
t.Error("Expected error from invalid asset item")
}
c.Exchanges[0].CurrencyPairs = nil
_, err = c.SupportsExchangeAssetType(testFakeExchangeName, asset.Spot)
err = c.SupportsExchangeAssetType(testFakeExchangeName, asset.Spot)
if err == nil {
t.Error("Expected error from nil pair manager")
}
}
func TestCheckExchangeAssetsConsistency(t *testing.T) {
t.Parallel()
var c Config
// Test for non-existent exchange
c.CheckExchangeAssetsConsistency("void")
c.Exchanges = append(c.Exchanges,
ExchangeConfig{
Name: testFakeExchangeName,
},
)
// Tests for nil currency pairs store but valid exchange name
c.CheckExchangeAssetsConsistency(testFakeExchangeName)
// Simulate testing a diff between stored asset types (config loading)
// and pair store
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
asset.Index,
},
}
c.Exchanges[0].CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
c.Exchanges[0].CurrencyPairs.Pairs[asset.PerpetualContract] = &currency.PairStore{}
c.CheckExchangeAssetsConsistency(testFakeExchangeName)
supports, err := c.SupportsExchangeAssetType(testFakeExchangeName, asset.PerpetualContract)
if err != nil {
t.Error(err)
}
if supports {
t.Error("perpetual contract should have been removed from the pair manager")
}
}
func TestSetPairs(t *testing.T) {
t.Parallel()
@@ -524,9 +510,8 @@ func TestSetPairs(t *testing.T) {
}
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: new(currency.PairStore),
},
}
@@ -562,10 +547,6 @@ func TestGetCurrencyPairConfig(t *testing.T) {
}
pm := &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
},
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: {
RequestFormat: &currency.PairFormat{
@@ -610,11 +591,6 @@ func TestCheckPairConfigFormats(t *testing.T) {
c.Exchanges = append(c.Exchanges,
ExchangeConfig{
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{
AssetTypes: asset.Items{
asset.Item("wrong"),
},
},
},
)
@@ -622,23 +598,40 @@ func TestCheckPairConfigFormats(t *testing.T) {
t.Error("nil pair store should return an error")
}
c.Exchanges[0].CurrencyPairs.AssetTypes = asset.Items{asset.Spot}
c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{
asset.Spot: {
RequestFormat: &currency.PairFormat{},
ConfigFormat: &currency.PairFormat{},
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: {},
asset.Futures: {},
},
asset.Futures: {
RequestFormat: &currency.PairFormat{},
ConfigFormat: &currency.PairFormat{},
}
if err := c.CheckPairConfigFormats(testFakeExchangeName); err == nil {
t.Error("error cannot be nil")
}
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: {
RequestFormat: &currency.PairFormat{},
ConfigFormat: &currency.PairFormat{},
},
asset.Futures: {
RequestFormat: &currency.PairFormat{},
ConfigFormat: &currency.PairFormat{},
},
},
}
if err := c.CheckPairConfigFormats(testFakeExchangeName); err != nil {
t.Error("nil pairs should be okay to continue")
}
avail, err := currency.NewPairDelimiter(testPair, "-")
if err != nil {
t.Fatal(err)
}
enabled, err := currency.NewPairDelimiter("BTC~USD", "~")
if err != nil {
t.Fatal(err)
}
// Test having a pair index and delimiter set at the same time throws an error
c.Exchanges[0].CurrencyPairs.AssetTypes = asset.Items{asset.Spot}
c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{
asset.Spot: {
RequestFormat: &currency.PairFormat{
@@ -651,10 +644,10 @@ func TestCheckPairConfigFormats(t *testing.T) {
Index: "USD",
},
Available: currency.Pairs{
currency.NewPairDelimiter(testPair, "-"),
avail,
},
Enabled: currency.Pairs{
currency.NewPairDelimiter("BTC~USD", "~"),
enabled,
},
},
}
@@ -698,11 +691,6 @@ func TestCheckPairConsistency(t *testing.T) {
c.Exchanges = append(c.Exchanges,
ExchangeConfig{
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
},
},
)
@@ -711,33 +699,71 @@ func TestCheckPairConsistency(t *testing.T) {
t.Error("nil pair store should return an error")
}
c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{
asset.Spot: {
RequestFormat: &currency.PairFormat{
Uppercase: false,
Delimiter: "_",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
},
Enabled: currency.Pairs{
currency.NewPairDelimiter("BTC_USD", "_"),
enabled, err := currency.NewPairDelimiter("BTC_USD", "_")
if err != nil {
t.Fatal(err)
}
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: {
RequestFormat: &currency.PairFormat{
Uppercase: false,
Delimiter: "_",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
},
Enabled: currency.Pairs{
enabled,
},
},
},
}
// Test for nil avail pairs
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("nil available pairs should continue")
err = c.CheckPairConsistency(testFakeExchangeName)
if err != nil {
t.Error(err)
}
p1, err := currency.NewPairDelimiter("LTC_USD", "_")
if err != nil {
t.Fatal(err)
}
// Test that enabled pair is not found in the available pairs
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{
currency.NewPairDelimiter("LTC_USD", "_"),
p1,
}
// LTC_USD is only found in the available pairs list and should therefor
// be added to the enabled pairs list due to the atLestOneEnabled code
err = c.CheckPairConsistency(testFakeExchangeName)
if err != nil {
t.Fatal(err)
}
for _, item := range c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled {
if !item.Equal(p1) {
t.Fatal("LTC_USD should be contained in the enabled pairs list")
}
}
p2, err := currency.NewPairDelimiter("BTC_USD", "_")
if err != nil {
t.Fatal(err)
}
// Add the BTC_USD pair and see result
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{
p1,
p2,
}
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
t.Fatal(err)
}
// Test that an empty enabled pair is populated with an available pair
@@ -746,10 +772,14 @@ func TestCheckPairConsistency(t *testing.T) {
t.Error("unexpected result")
}
if len(c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled) != 1 {
t.Fatal("should be populated with atleast one currency pair")
}
// Test that an invalid enabled pair is removed from the list
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{
currency.NewPairDelimiter("LTC_USD", "_"),
currency.NewPairDelimiter("BTC_USD", "_"),
p1,
p2,
}
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
@@ -760,6 +790,48 @@ func TestCheckPairConsistency(t *testing.T) {
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
}
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].AssetEnabled = convert.BoolPtr(true)
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{}
// Test no conflict and atleast one on enabled asset type
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
}
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].AssetEnabled = convert.BoolPtr(true)
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{currency.NewPair(currency.DASH, currency.USD)}
// Test with conflict and atleast one on enabled asset type
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
}
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].AssetEnabled = convert.BoolPtr(false)
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{}
// Test no conflict and atleast one on disabled asset type
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
}
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{
currency.NewPair(currency.DASH, currency.USD),
p1,
p2,
}
// Test with conflict and atleast one on disabled asset type
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
}
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].AssetEnabled = nil
// assetType enabled failure check
if err := c.CheckPairConsistency(testFakeExchangeName); err != nil {
t.Error("unexpected result")
}
}
func TestSupportsPair(t *testing.T) {
@@ -772,17 +844,15 @@ func TestSupportsPair(t *testing.T) {
}
assetType := asset.Spot
_, err = cfg.SupportsPair("asdf",
currency.NewPair(currency.BTC, currency.USD), assetType)
if err == nil {
if cfg.SupportsPair("asdf",
currency.NewPair(currency.BTC, currency.USD), assetType) {
t.Error(
"TestSupportsPair. Expected error from Non-existent exchange",
)
}
_, err = cfg.SupportsPair("Bitfinex",
currency.NewPair(currency.BTC, currency.USD), assetType)
if err != nil {
if !cfg.SupportsPair("Bitfinex",
currency.NewPair(currency.BTC, currency.USD), assetType) {
t.Errorf(
"TestSupportsPair. Incorrect values. Err: %s", err,
)
@@ -809,7 +879,26 @@ func TestGetPairFormat(t *testing.T) {
}
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
AssetTypes: asset.Items{asset.Spot},
UseGlobalFormat: false,
RequestFormat: &currency.PairFormat{
Uppercase: false,
Delimiter: "_",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
},
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: nil,
},
}
_, err = c.GetPairFormat(testFakeExchangeName, asset.Spot)
if err == nil {
t.Error("Expected error from nil pair manager")
}
c.Exchanges[0].CurrencyPairs = &currency.PairsManager{
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: false,
@@ -819,6 +908,9 @@ func TestGetPairFormat(t *testing.T) {
Uppercase: true,
Delimiter: "_",
},
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: new(currency.PairStore),
},
}
_, err = c.GetPairFormat(testFakeExchangeName, asset.Item("invalid"))
if err == nil {
@@ -876,12 +968,8 @@ func TestGetAvailablePairs(t *testing.T) {
c.Exchanges = append(c.Exchanges,
ExchangeConfig{
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
},
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{},
},
)
@@ -923,12 +1011,8 @@ func TestGetEnabledPairs(t *testing.T) {
c.Exchanges = append(c.Exchanges,
ExchangeConfig{
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
},
Name: testFakeExchangeName,
CurrencyPairs: &currency.PairsManager{},
},
)
@@ -953,6 +1037,11 @@ func TestGetEnabledPairs(t *testing.T) {
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{
currency.NewPair(currency.BTC, currency.USD),
}
c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{
currency.NewPair(currency.BTC, currency.USD),
}
_, err = c.GetEnabledPairs(testFakeExchangeName, asset.Spot)
if err != nil {
t.Error(err)
@@ -1183,16 +1272,15 @@ func TestCheckExchangeConfigValues(t *testing.T) {
// Test API settings migration
sptr := func(s string) *string { return &s }
bptr := func(b bool) *bool { return &b }
int64ptr := func(i int64) *int64 { return &i }
cfg.Exchanges[0].APIKey = sptr("awesomeKey")
cfg.Exchanges[0].APISecret = sptr("meowSecret")
cfg.Exchanges[0].ClientID = sptr("clientIDerino")
cfg.Exchanges[0].APIAuthPEMKey = sptr("-----BEGIN EC PRIVATE KEY-----\nASDF\n-----END EC PRIVATE KEY-----\n")
cfg.Exchanges[0].APIAuthPEMKeySupport = bptr(true)
cfg.Exchanges[0].AuthenticatedAPISupport = bptr(true)
cfg.Exchanges[0].AuthenticatedWebsocketAPISupport = bptr(true)
cfg.Exchanges[0].APIAuthPEMKeySupport = convert.BoolPtr(true)
cfg.Exchanges[0].AuthenticatedAPISupport = convert.BoolPtr(true)
cfg.Exchanges[0].AuthenticatedWebsocketAPISupport = convert.BoolPtr(true)
cfg.Exchanges[0].WebsocketURL = sptr("wss://1337")
cfg.Exchanges[0].APIURL = sptr(APIURLNonDefaultMessage)
cfg.Exchanges[0].APIURLSecondary = sptr(APIURLNonDefaultMessage)
@@ -1230,8 +1318,8 @@ func TestCheckExchangeConfigValues(t *testing.T) {
// Test feature and endpoint migrations migrations
cfg.Exchanges[0].Features = nil
cfg.Exchanges[0].SupportsAutoPairUpdates = bptr(true)
cfg.Exchanges[0].Websocket = bptr(true)
cfg.Exchanges[0].SupportsAutoPairUpdates = convert.BoolPtr(true)
cfg.Exchanges[0].Websocket = convert.BoolPtr(true)
cfg.Exchanges[0].API.Endpoints.URL = ""
cfg.Exchanges[0].API.Endpoints.URLSecondary = ""
cfg.Exchanges[0].API.Endpoints.WebsocketURL = ""
@@ -1253,11 +1341,16 @@ func TestCheckExchangeConfigValues(t *testing.T) {
t.Error("unexpected values")
}
p1, err := currency.NewPairDelimiter(testPair, "-")
if err != nil {
t.Fatal(err)
}
// Test currency pair migration
setupPairs := func(emptyAssets bool) {
cfg.Exchanges[0].CurrencyPairs = nil
p := currency.Pairs{
currency.NewPairDelimiter(testPair, "-"),
p1,
}
cfg.Exchanges[0].PairsLastUpdated = int64ptr(1234567)
@@ -1305,17 +1398,25 @@ func TestCheckExchangeConfigValues(t *testing.T) {
t.Error("unexpected request format values")
}
if cfg.Exchanges[0].CurrencyPairs.AssetTypes.JoinToString(",") != "spot" ||
if !cfg.Exchanges[0].CurrencyPairs.GetAssetTypes().Contains(asset.Spot) ||
!cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat {
t.Error("unexpected results")
}
pairs := cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, true)
pairs, err := cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, true)
if err != nil {
t.Fatal(err)
}
if len(pairs) == 0 || pairs.Join() != testPair {
t.Error("pairs not set properly")
}
pairs = cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, false)
pairs, err = cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, false)
if err != nil {
t.Fatal(err)
}
if len(pairs) == 0 || pairs.Join() != testPair {
t.Error("pairs not set properly")
}
@@ -1334,24 +1435,10 @@ func TestCheckExchangeConfigValues(t *testing.T) {
cfg.Exchanges[0].Features.Supports.RESTCapabilities.AutoPairUpdates = false
cfg.Exchanges[0].Features.Supports.WebsocketCapabilities.AutoPairUpdates = false
cfg.Exchanges[0].CurrencyPairs.LastUpdated = 0
cfg.CheckExchangeConfigValues()
// Test exchange pair consistency error
cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat = false
backup := cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot]
cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot] = nil
err = cfg.CheckExchangeConfigValues()
if err != nil {
t.Error(err)
}
if cfg.Exchanges[0].Enabled {
t.Error("exchange should have been disabled")
}
// Restore to previous state
cfg.Exchanges[0].Enabled = true
cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat = true
cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot] = backup
// Test websocket and HTTP timeout values
cfg.Exchanges[0].WebsocketResponseMaxLimit = 0
@@ -1419,6 +1506,10 @@ func TestCheckExchangeConfigValues(t *testing.T) {
!cfg.Exchanges[0].API.AuthenticatedWebsocketSupport {
t.Error("Expected AuthenticatedAPISupport and AuthenticatedWebsocketAPISupport to be false from invalid API keys")
}
// Make a sneaky copy for bank account testing
cpy := append(cfg.Exchanges[:0:0], cfg.Exchanges...)
// Test empty exchange name for an enabled exchange
cfg.Exchanges[0].Enabled = true
cfg.Exchanges[0].Name = ""
@@ -1436,6 +1527,82 @@ func TestCheckExchangeConfigValues(t *testing.T) {
if err == nil {
t.Error("Expected error from no enabled exchanges")
}
cfg.Exchanges = cpy
// Check bank account validation for exchange
cfg.Exchanges[0].BankAccounts = []banking.Account{
{
Enabled: true,
},
}
err = cfg.CheckExchangeConfigValues()
if err != nil {
t.Error(err)
}
if cfg.Exchanges[0].BankAccounts[0].Enabled {
t.Fatal("bank aaccount details not provided this should disable")
}
// Test international bank
cfg.Exchanges[0].BankAccounts[0].Enabled = true
cfg.Exchanges[0].BankAccounts[0].BankName = testString
cfg.Exchanges[0].BankAccounts[0].BankAddress = testString
cfg.Exchanges[0].BankAccounts[0].BankPostalCode = testString
cfg.Exchanges[0].BankAccounts[0].BankPostalCity = testString
cfg.Exchanges[0].BankAccounts[0].BankCountry = testString
cfg.Exchanges[0].BankAccounts[0].AccountName = testString
cfg.Exchanges[0].BankAccounts[0].SupportedCurrencies = "monopoly moneys"
cfg.Exchanges[0].BankAccounts[0].IBAN = "some iban"
cfg.Exchanges[0].BankAccounts[0].SWIFTCode = "some swifty"
err = cfg.CheckExchangeConfigValues()
if err != nil {
t.Error(err)
}
if !cfg.Exchanges[0].BankAccounts[0].Enabled {
t.Fatal("bank aaccount details provided this should not disable")
}
// Test aussie bank
cfg.Exchanges[0].BankAccounts[0].Enabled = true
cfg.Exchanges[0].BankAccounts[0].BankName = testString
cfg.Exchanges[0].BankAccounts[0].BankAddress = testString
cfg.Exchanges[0].BankAccounts[0].BankPostalCode = testString
cfg.Exchanges[0].BankAccounts[0].BankPostalCity = testString
cfg.Exchanges[0].BankAccounts[0].BankCountry = testString
cfg.Exchanges[0].BankAccounts[0].AccountName = testString
cfg.Exchanges[0].BankAccounts[0].SupportedCurrencies = "AUD"
cfg.Exchanges[0].BankAccounts[0].BSBNumber = "some BSB nonsense"
cfg.Exchanges[0].BankAccounts[0].IBAN = ""
cfg.Exchanges[0].BankAccounts[0].SWIFTCode = ""
err = cfg.CheckExchangeConfigValues()
if err != nil {
t.Error(err)
}
if !cfg.Exchanges[0].BankAccounts[0].Enabled {
t.Fatal("bank account details provided this should not disable")
}
cfg.Exchanges = nil
cfg.Exchanges = append(cfg.Exchanges, cpy[0])
cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = nil
cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].AssetEnabled = convert.BoolPtr(false)
err = cfg.CheckExchangeConfigValues()
if err != nil {
t.Error(err)
}
cfg.Exchanges[0].CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
err = cfg.CheckExchangeConfigValues()
if err == nil {
t.Error("err cannot be nil")
}
}
func TestRetrieveConfigCurrencyPairs(t *testing.T) {

View File

@@ -449,7 +449,7 @@
"websocketCapabilities": {}
},
"enabled": {
"autoPairUpdates": false,
"autoPairUpdates": true,
"websocketAPI": false
}
},

View File

@@ -6,8 +6,6 @@ import (
"fmt"
"strings"
"unicode"
"github.com/thrasher-corp/gocryptotrader/common"
)
func (r Role) String() string {
@@ -68,24 +66,24 @@ func (b *BaseCodes) HasData() bool {
// GetFullCurrencyData returns a type that is read to dump to file
func (b *BaseCodes) GetFullCurrencyData() (File, error) {
var file File
for _, i := range b.Items {
switch i.Role {
for i := range b.Items {
switch b.Items[i].Role {
case Unset:
file.UnsetCurrency = append(file.UnsetCurrency, *i)
file.UnsetCurrency = append(file.UnsetCurrency, *b.Items[i])
case Fiat:
file.FiatCurrency = append(file.FiatCurrency, *i)
file.FiatCurrency = append(file.FiatCurrency, *b.Items[i])
case Cryptocurrency:
file.Cryptocurrency = append(file.Cryptocurrency, *i)
file.Cryptocurrency = append(file.Cryptocurrency, *b.Items[i])
case Token:
file.Token = append(file.Token, *i)
file.Token = append(file.Token, *b.Items[i])
case Contract:
file.Contracts = append(file.Contracts, *i)
file.Contracts = append(file.Contracts, *b.Items[i])
default:
return file, errors.New("role undefined")
}
}
file.LastMainUpdate = b.LastMainUpdate
file.LastMainUpdate = b.LastMainUpdate.Unix()
return file, nil
}
@@ -103,50 +101,11 @@ func (b *BaseCodes) GetCurrencies() Currencies {
return currencies
}
// UpdateCryptocurrency updates or registers a cryptocurrency
func (b *BaseCodes) UpdateCryptocurrency(fullName, symbol string, id int) error {
b.mtx.Lock()
defer b.mtx.Unlock()
for i := range b.Items {
if b.Items[i].Symbol != symbol {
continue
}
if b.Items[i].Role != Unset {
if b.Items[i].Role != Cryptocurrency {
if b.Items[i].FullName != "" {
if b.Items[i].FullName != fullName {
// multiple symbols found, break this and add the
// full context - this most likely won't occur for
// fiat but could occur for contracts.
break
}
}
return fmt.Errorf("role already defined in cryptocurrency %s as [%s]",
b.Items[i].Symbol,
b.Items[i].Role)
}
b.Items[i].FullName = fullName
b.Items[i].ID = id
return nil
}
b.Items[i].Role = Cryptocurrency
b.Items[i].FullName = fullName
b.Items[i].ID = id
return nil
// UpdateCurrency updates or registers a currency/contract
func (b *BaseCodes) UpdateCurrency(fullName, symbol, blockchain string, id int, r Role) error {
if r == Unset {
return fmt.Errorf("role cannot be unset in update currency for %s", symbol)
}
b.Items = append(b.Items, &Item{
FullName: fullName,
Symbol: symbol,
ID: id,
Role: Cryptocurrency,
})
return nil
}
// UpdateFiatCurrency updates or registers a fiat currency
func (b *BaseCodes) UpdateFiatCurrency(fullName, symbol string, id int) error {
b.mtx.Lock()
defer b.mtx.Unlock()
for i := range b.Items {
@@ -154,115 +113,31 @@ func (b *BaseCodes) UpdateFiatCurrency(fullName, symbol string, id int) error {
continue
}
if b.Items[i].Role != Unset {
if b.Items[i].Role != Fiat {
return fmt.Errorf("role already defined in fiat currency %s as [%s]",
b.Items[i].Symbol,
b.Items[i].Role)
}
if b.Items[i].Role == Unset {
b.Items[i].FullName = fullName
b.Items[i].Role = r
b.Items[i].AssocChain = blockchain
b.Items[i].ID = id
return nil
}
b.Items[i].Role = Fiat
if b.Items[i].Role != r {
// Captures same name currencies and duplicates to different roles
break
}
b.Items[i].FullName = fullName
b.Items[i].AssocChain = blockchain
b.Items[i].ID = id
return nil
}
b.Items = append(b.Items, &Item{
FullName: fullName,
Symbol: symbol,
ID: id,
Role: Fiat,
})
return nil
}
// UpdateToken updates or registers a token
func (b *BaseCodes) UpdateToken(fullName, symbol, assocBlockchain string, id int) error {
b.mtx.Lock()
defer b.mtx.Unlock()
for i := range b.Items {
if b.Items[i].Symbol != symbol {
continue
}
if b.Items[i].Role != Unset {
if b.Items[i].Role != Token {
if b.Items[i].FullName != "" {
if b.Items[i].FullName != fullName {
// multiple symbols found, break this and add the
// full context - this most likely won't occur for
// fiat but could occur for contracts.
break
}
}
return fmt.Errorf("role already defined in token %s as [%s]",
b.Items[i].Symbol,
b.Items[i].Role)
}
b.Items[i].FullName = fullName
b.Items[i].ID = id
b.Items[i].AssocChain = assocBlockchain
return nil
}
b.Items[i].Role = Token
b.Items[i].FullName = fullName
b.Items[i].ID = id
b.Items[i].AssocChain = assocBlockchain
return nil
}
b.Items = append(b.Items, &Item{
FullName: fullName,
Symbol: symbol,
FullName: fullName,
Role: r,
AssocChain: blockchain,
ID: id,
Role: Token,
AssocChain: assocBlockchain,
})
return nil
}
// UpdateContract updates or registers a contract
func (b *BaseCodes) UpdateContract(fullName, symbol, assocExchange string) error {
b.mtx.Lock()
defer b.mtx.Unlock()
for i := range b.Items {
if b.Items[i].Symbol != symbol {
continue
}
if b.Items[i].Role != Unset {
if b.Items[i].Role != Contract {
return fmt.Errorf("role already defined in contract %s as [%s]",
b.Items[i].Symbol,
b.Items[i].Role)
}
b.Items[i].FullName = fullName
if !common.StringDataContains(b.Items[i].AssocExchange, assocExchange) {
b.Items[i].AssocExchange = append(b.Items[i].AssocExchange,
assocExchange)
}
return nil
}
b.Items[i].Role = Contract
b.Items[i].FullName = fullName
if !common.StringDataContains(b.Items[i].AssocExchange, assocExchange) {
b.Items[i].AssocExchange = append(b.Items[i].AssocExchange,
assocExchange)
}
return nil
}
b.Items = append(b.Items, &Item{
FullName: fullName,
Symbol: symbol,
Role: Contract,
AssocExchange: []string{assocExchange},
})
return nil
}
@@ -293,42 +168,38 @@ func (b *BaseCodes) Register(c string) Code {
}
}
newItem := Item{Symbol: NewUpperCode}
newCode := Code{
Item: &newItem,
newItem := &Item{Symbol: NewUpperCode}
b.Items = append(b.Items, newItem)
return Code{
Item: newItem,
UpperCase: format,
}
b.Items = append(b.Items, newCode.Item)
return newCode
}
// RegisterFiat registers a fiat currency from a string and returns a currency
// code
func (b *BaseCodes) RegisterFiat(c string) (Code, error) {
func (b *BaseCodes) RegisterFiat(c string) Code {
c = strings.ToUpper(c)
b.mtx.Lock()
defer b.mtx.Unlock()
for i := range b.Items {
if b.Items[i].Symbol == c {
if b.Items[i].Role != Unset {
if b.Items[i].Role != Fiat {
return Code{}, fmt.Errorf("register fiat error role already defined in fiat %s as [%s]",
b.Items[i].Symbol,
b.Items[i].Role)
}
return Code{Item: b.Items[i], UpperCase: true}, nil
if b.Items[i].Role == Unset {
b.Items[i].Role = Fiat
}
b.Items[i].Role = Fiat
return Code{Item: b.Items[i], UpperCase: true}, nil
if b.Items[i].Role != Fiat {
continue
}
return Code{Item: b.Items[i], UpperCase: true}
}
}
item := &Item{Symbol: c, Role: Fiat}
b.Items = append(b.Items, item)
return Code{Item: item, UpperCase: true}, nil
return Code{Item: item, UpperCase: true}
}
// LoadItem sets item data
@@ -336,41 +207,18 @@ func (b *BaseCodes) LoadItem(item *Item) error {
b.mtx.Lock()
defer b.mtx.Unlock()
for i := range b.Items {
if b.Items[i].Symbol == item.Symbol {
if b.Items[i].Role == Unset {
b.Items[i].AssocChain = item.AssocChain
b.Items[i].AssocExchange = item.AssocExchange
b.Items[i].ID = item.ID
b.Items[i].Role = item.Role
b.Items[i].FullName = item.FullName
return nil
}
if b.Items[i].FullName != "" {
if b.Items[i].FullName == item.FullName {
b.Items[i].AssocChain = item.AssocChain
b.Items[i].AssocExchange = item.AssocExchange
b.Items[i].ID = item.ID
b.Items[i].Role = item.Role
return nil
}
break
}
if b.Items[i].ID == item.ID {
b.Items[i].AssocChain = item.AssocChain
b.Items[i].AssocExchange = item.AssocExchange
b.Items[i].FullName = item.FullName
b.Items[i].ID = item.ID
b.Items[i].Role = item.Role
return nil
}
return fmt.Errorf("currency %s not found in currencycode list",
item.Symbol)
if b.Items[i].Symbol != item.Symbol ||
(b.Items[i].Role != Unset &&
item.Role != Unset &&
b.Items[i].Role != item.Role) {
continue
}
b.Items[i].AssocChain = item.AssocChain
b.Items[i].ID = item.ID
b.Items[i].Role = item.Role
b.Items[i].FullName = item.FullName
return nil
}
b.Items = append(b.Items, item)
return nil
}

View File

@@ -176,41 +176,51 @@ func TestBaseCode(t *testing.T) {
false)
}
err := main.UpdateContract("Bitcoin Perpetual", "XBTUSD", "Bitmex")
main.Register("XBTUSD")
err := main.UpdateCurrency("Bitcoin Perpetual",
"XBTUSD",
"",
0,
Contract)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
err = main.UpdateCryptocurrency("Bitcoin", "BTC", 1337)
main.Register("BTC")
err = main.UpdateCurrency("Bitcoin", "BTC", "", 1337, Cryptocurrency)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
err = main.UpdateFiatCurrency("Unreal Dollar", "AUD", 1111)
main.Register("AUD")
err = main.UpdateCurrency("Unreal Dollar", "AUD", "", 1111, Fiat)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
if main.Items[5].FullName != "Unreal Dollar" {
t.Error("Expected fullname to update for AUD")
}
err = main.UpdateFiatCurrency("Australian Dollar", "AUD", 1336)
err = main.UpdateCurrency("Australian Dollar", "AUD", "", 1336, Fiat)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
main.Items[5].Role = Unset
err = main.UpdateFiatCurrency("Australian Dollar", "AUD", 1336)
err = main.UpdateCurrency("Australian Dollar", "AUD", "", 1336, Fiat)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
if main.Items[5].Role != Fiat {
t.Error("Expected role to change to Fiat")
}
err = main.UpdateToken("Populous", "PPT", "ETH", 1335)
main.Register("PPT")
err = main.UpdateCurrency("Populous", "PPT", "ETH", 1335, Token)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
contract := main.Register("XBTUSD")
@@ -235,15 +245,12 @@ func TestBaseCode(t *testing.T) {
true)
}
err = main.LoadItem(&Item{
main.LoadItem(&Item{
ID: 0,
FullName: "Cardano",
Role: Cryptocurrency,
Symbol: "ADA",
})
if err != nil {
t.Error("BaseCode LoadItem() error", err)
}
full, err := main.GetFullCurrencyData()
if err != nil {
@@ -275,8 +282,8 @@ func TestBaseCode(t *testing.T) {
len(full.UnsetCurrency))
}
if !full.LastMainUpdate.IsZero() {
t.Errorf("BaseCode GetFullCurrencyData() error expected 0 but received %s",
if full.LastMainUpdate.(int64) != -62135596800 {
t.Errorf("BaseCode GetFullCurrencyData() error expected -62135596800 but received %d",
full.LastMainUpdate)
}
@@ -295,41 +302,35 @@ func TestBaseCode(t *testing.T) {
}
main.Items[0].FullName = "Hello"
err = main.UpdateCryptocurrency("MEWOW", "CATS", 1338)
err = main.UpdateCurrency("MEWOW", "CATS", "", 1338, Cryptocurrency)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
if main.Items[0].FullName != "MEWOW" {
t.Error("Fullname not updated")
}
err = main.UpdateCryptocurrency("MEWOW", "CATS", 1338)
err = main.UpdateCurrency("MEWOW", "CATS", "", 1338, Cryptocurrency)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
err = main.UpdateCurrency("WOWCATS", "CATS", "", 3, Token)
if err != nil {
t.Fatal(err)
}
main.Items[0].Role = Cryptocurrency
err = main.UpdateCryptocurrency("MEWOW", "CATS", 3)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
}
if main.Items[0].ID != 3 {
// Creates a new item under a different currency role
if main.Items[9].ID != 3 {
t.Error("ID not updated")
}
main.Items[0].Role = Unset
err = main.UpdateCryptocurrency("MEWOW", "CATS", 1338)
err = main.UpdateCurrency("MEWOW", "CATS", "", 1338, Cryptocurrency)
if err != nil {
t.Error("BaseCode UpdateContract error", err)
t.Fatal(err)
}
if main.Items[0].ID != 1338 {
t.Error("ID not updated")
}
main.Items[0].Role = Token
err = main.UpdateCryptocurrency("MEWOW", "CATS", 3)
if err == nil {
t.Error("Expecting cryptocurrency to already exist")
}
}
func TestCodeString(t *testing.T) {

View File

@@ -41,12 +41,11 @@ type Code struct {
// Item defines a sub type containing the main attributes of a designated
// currency code pointer
type Item struct {
ID int `json:"id"`
FullName string `json:"fullName"`
Symbol string `json:"symbol"`
Role Role `json:"role"`
AssocChain string `json:"associatedBlockchain"`
AssocExchange []string `json:"associatedExchanges"`
ID int `json:"id,omitempty"`
FullName string `json:"fullName,omitempty"`
Symbol string `json:"symbol"`
Role Role `json:"-"`
AssocChain string `json:"associatedBlockchain,omitempty"`
}
// Const declarations for individual currencies/tokens/fiat

View File

@@ -8,7 +8,6 @@ package coinmarketcap
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
@@ -17,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/log"
)
// SetDefaults sets default values for the exchange
@@ -62,8 +62,8 @@ func (c *Coinmarketcap) GetCryptocurrencyInfo(currencyID ...int64) (CryptoCurren
}
var currStr []string
for _, d := range currencyID {
currStr = append(currStr, strconv.FormatInt(d, 10))
for i := range currencyID {
currStr = append(currStr, strconv.FormatInt(currencyID[i], 10))
}
val := url.Values{}
@@ -667,7 +667,6 @@ func (c *Coinmarketcap) GetPriceConversion(amount float64, currencyID int64, atH
func (c *Coinmarketcap) SendHTTPRequest(method, endpoint string, v url.Values, result interface{}) error {
headers := make(map[string]string)
headers["Accept"] = "application/json"
headers["Accept-Encoding"] = "deflate, gzip"
headers["X-CMC_PRO_API_KEY"] = c.APIkey
path := c.APIUrl + c.APIVersion + endpoint
@@ -710,7 +709,8 @@ func (c *Coinmarketcap) SetAccountPlan(s string) error {
case "enterprise":
c.Plan = Enterprise
default:
return fmt.Errorf("account plan %s not found", s)
log.Warnf(log.Global, "account plan %s not found, defaulting to basic", s)
c.Plan = Basic
}
return nil
}

View File

@@ -44,11 +44,6 @@ func TestSetup(t *testing.T) {
if err := c.Setup(cfg); err != nil {
t.Error(err)
}
cfg.AccountPlan = "meow"
if err := c.Setup(cfg); err == nil {
t.Error("expected err when invalid account plan is specified")
}
}
func TestCheckAccountPlan(t *testing.T) {
@@ -405,8 +400,4 @@ func TestSetAccountPlan(t *testing.T) {
}
}
}
if err := c.SetAccountPlan("bra"); err == nil {
t.Error("SetAccountPlan() error cannot be nil")
}
}

View File

@@ -110,22 +110,14 @@ func (c *ConversionRates) Update(m map[string]float64) error {
var list []Code // Verification list, cross check all currencies coming in
var mainBaseCurrency Code
for key, val := range m {
code1, err := storage.ValidateFiatCode(key[:3])
if err != nil {
return err
}
code1 := storage.ValidateFiatCode(key[:3])
if mainBaseCurrency == (Code{}) {
mainBaseCurrency = code1
}
code2, err := storage.ValidateFiatCode(key[3:])
if err != nil {
return err
}
code2 := storage.ValidateFiatCode(key[3:])
if code1 == code2 { // Get rid of same conversions
continue
}

View File

@@ -23,8 +23,8 @@ type Currencies []Code
// Strings returns an array of currency strings
func (c Currencies) Strings() []string {
var list []string
for _, d := range c {
list = append(list, d.String())
for i := range c {
list = append(list, c[i].String())
}
return list
}
@@ -53,8 +53,9 @@ func (c *Currencies) UnmarshalJSON(d []byte) error {
}
var allTheCurrencies Currencies
for _, data := range strings.Split(configCurrencies, ",") {
allTheCurrencies = append(allTheCurrencies, NewCode(data))
curr := strings.Split(configCurrencies, ",")
for i := range curr {
allTheCurrencies = append(allTheCurrencies, NewCode(curr[i]))
}
*c = allTheCurrencies
@@ -72,17 +73,14 @@ func (c Currencies) Match(other Currencies) bool {
return false
}
for _, d := range c {
var found bool
for i := range other {
if d == other[i] {
found = true
break
match:
for x := range c {
for y := range other {
if c[x] == other[y] {
continue match
}
}
if !found {
return false
}
return false
}
return true
}

View File

@@ -47,3 +47,18 @@ func TestCurrenciesMarshalJSON(t *testing.T) {
expected, string(encoded))
}
}
func TestMatch(t *testing.T) {
matchString := []string{"btc", "usd", "ltc", "bro", "things"}
c := NewCurrenciesFromStringArray(matchString)
if !c.Match(NewCurrenciesFromStringArray(matchString)) {
t.Fatal("should match")
}
if c.Match(NewCurrenciesFromStringArray([]string{"btc", "usd", "ltc", "bro"})) {
t.Fatal("should not match")
}
if c.Match(NewCurrenciesFromStringArray([]string{"btc", "usd", "ltc", "bro", "garbo"})) {
t.Fatal("should not match")
}
}

View File

@@ -71,9 +71,14 @@ func GetTotalMarketCryptocurrencies() ([]Code, error) {
return storage.GetTotalMarketCryptocurrencies()
}
// RunStorageUpdater runs a new foreign exchange updater instance
func RunStorageUpdater(o BotOverrides, m *MainConfiguration, filepath string, v bool) error {
return storage.RunUpdater(o, m, filepath, v)
// RunStorageUpdater runs a new foreign exchange updater instance
func RunStorageUpdater(o BotOverrides, m *MainConfiguration, filepath string) error {
return storage.RunUpdater(o, m, filepath)
}
// ShutdownStorageUpdater cleanly shuts down and saves to currency.json
func ShutdownStorageUpdater() error {
return storage.Shutdown()
}
// CopyPairFormat copies the pair format from a list of pairs once matched
@@ -100,17 +105,23 @@ func FormatPairs(pairs []string, delimiter, index string) (Pairs, error) {
continue
}
var p Pair
var err error
if delimiter != "" {
p = NewPairDelimiter(pairs[x], delimiter)
p, err = NewPairDelimiter(pairs[x], delimiter)
if err != nil {
return nil, err
}
} else {
if index != "" {
var err error
p, err = NewPairFromIndex(pairs[x], index)
if err != nil {
return Pairs{}, err
}
} else {
p = NewPairFromStrings(pairs[x][0:3], pairs[x][3:])
p, err = NewPairFromStrings(pairs[x][0:3], pairs[x][3:])
if err != nil {
return Pairs{}, err
}
}
}
result = append(result, p)

View File

@@ -53,10 +53,24 @@ type FXSettings struct {
// File defines a full currency file generated by the currency storage
// analysis system
type File struct {
LastMainUpdate time.Time `json:"lastMainUpdate"`
Cryptocurrency []Item `json:"cryptocurrencies"`
FiatCurrency []Item `json:"fiatCurrencies"`
UnsetCurrency []Item `json:"unsetCurrencies"`
Contracts []Item `json:"contracts"`
Token []Item `json:"tokens"`
LastMainUpdate interface{} `json:"lastMainUpdate"`
Cryptocurrency []Item `json:"cryptocurrencies"`
FiatCurrency []Item `json:"fiatCurrencies"`
UnsetCurrency []Item `json:"unsetCurrencies"`
Contracts []Item `json:"contracts"`
Token []Item `json:"tokens"`
}
// Const here are packaged defined delimiters
const (
UnderscoreDelimiter = "_"
DashDelimiter = "-"
ForwardSlashDelimiter = "/"
ColonDelimiter = ":"
)
// delimiters is a delimiter list
var delimiters = []string{UnderscoreDelimiter,
DashDelimiter,
ForwardSlashDelimiter,
ColonDelimiter}

View File

@@ -2,14 +2,16 @@ package currency
import (
"errors"
"fmt"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// GetAssetTypes returns a list of stored asset types
func (p *PairsManager) GetAssetTypes() asset.Items {
p.m.Lock()
defer p.m.Unlock()
p.m.RLock()
defer p.m.RUnlock()
var assetTypes asset.Items
for k := range p.Pairs {
assetTypes = append(assetTypes, k)
@@ -18,28 +20,23 @@ func (p *PairsManager) GetAssetTypes() asset.Items {
}
// Get gets the currency pair config based on the asset type
func (p *PairsManager) Get(a asset.Item) *PairStore {
p.m.Lock()
defer p.m.Unlock()
func (p *PairsManager) Get(a asset.Item) (*PairStore, error) {
p.m.RLock()
defer p.m.RUnlock()
c, ok := p.Pairs[a]
if !ok {
return nil
return nil,
fmt.Errorf("cannot get pair store, asset type %s not supported", a)
}
return c
return c, nil
}
// Store stores a new currency pair config based on its asset type
func (p *PairsManager) Store(a asset.Item, ps PairStore) {
p.m.Lock()
if p.Pairs == nil {
p.Pairs = make(map[asset.Item]*PairStore)
}
if !p.AssetTypes.Contains(a) {
p.AssetTypes = append(p.AssetTypes, a)
}
p.Pairs[a] = &ps
p.m.Unlock()
}
@@ -51,37 +48,35 @@ func (p *PairsManager) Delete(a asset.Item) {
if p.Pairs == nil {
return
}
_, ok := p.Pairs[a]
if !ok {
return
}
delete(p.Pairs, a)
}
// GetPairs gets a list of stored pairs based on the asset type and whether
// they're enabled or not
func (p *PairsManager) GetPairs(a asset.Item, enabled bool) Pairs {
p.m.Lock()
defer p.m.Unlock()
func (p *PairsManager) GetPairs(a asset.Item, enabled bool) (Pairs, error) {
p.m.RLock()
defer p.m.RUnlock()
if p.Pairs == nil {
return nil
return nil, nil
}
c, ok := p.Pairs[a]
if !ok {
return nil
return nil, nil
}
var pairs Pairs
if enabled {
pairs = c.Enabled
} else {
pairs = c.Available
for i := range c.Enabled {
if !c.Available.Contains(c.Enabled[i], true) {
return c.Enabled,
fmt.Errorf("enabled pair %s of asset type %s not contained in available list",
c.Enabled[i],
a)
}
}
return c.Enabled, nil
}
return pairs
return c.Available, nil
}
// StorePairs stores a list of pairs based on the asset type and whether
@@ -96,7 +91,8 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) {
c, ok := p.Pairs[a]
if !ok {
c = new(PairStore)
p.Pairs[a] = new(PairStore)
c = p.Pairs[a]
}
if enabled {
@@ -104,8 +100,6 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) {
} else {
c.Available = pairs
}
p.Pairs[a] = c
}
// DisablePair removes the pair from the enabled pairs list if found
@@ -113,17 +107,9 @@ func (p *PairsManager) DisablePair(a asset.Item, pair Pair) error {
p.m.Lock()
defer p.m.Unlock()
if p.Pairs == nil {
return errors.New("pair manager not initialised")
}
c, ok := p.Pairs[a]
if !ok {
return errors.New("asset type not found")
}
if c == nil {
return errors.New("currency store is nil")
c, err := p.getPairStore(a)
if err != nil {
return err
}
if !c.Enabled.Contains(pair, true) {
@@ -140,27 +126,82 @@ func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error {
p.m.Lock()
defer p.m.Unlock()
if p.Pairs == nil {
return errors.New("pair manager not initialised")
}
c, ok := p.Pairs[a]
if !ok {
return errors.New("asset type not found")
}
if c == nil {
return errors.New("currency store is nil")
c, err := p.getPairStore(a)
if err != nil {
return err
}
if !c.Available.Contains(pair, true) {
return errors.New("specified pair was not found in the list of available pairs")
return fmt.Errorf("%s pair was not found in the list of available pairs",
pair)
}
if c.Enabled.Contains(pair, true) {
return errors.New("specified pair is already enabled")
return fmt.Errorf("%s pair is already enabled", pair)
}
c.Enabled = c.Enabled.Add(pair)
return nil
}
// IsAssetEnabled checks to see if an asset is enabled
func (p *PairsManager) IsAssetEnabled(a asset.Item) error {
p.m.RLock()
defer p.m.RUnlock()
c, err := p.getPairStore(a)
if err != nil {
return err
}
if c.AssetEnabled == nil {
return errors.New("cannot ascertain if asset is enabled, variable is nil")
}
if !*c.AssetEnabled {
return errors.New("asset not enabled")
}
return nil
}
// SetAssetEnabled sets if an asset is enabled or disabled for first run
func (p *PairsManager) SetAssetEnabled(a asset.Item, enabled bool) error {
p.m.Lock()
defer p.m.Unlock()
c, err := p.getPairStore(a)
if err != nil {
return err
}
if c.AssetEnabled == nil {
c.AssetEnabled = convert.BoolPtr(enabled)
return nil
}
if !*c.AssetEnabled && !enabled {
return errors.New("asset already disabled")
} else if *c.AssetEnabled && enabled {
return errors.New("asset already enabled")
}
*c.AssetEnabled = enabled
return nil
}
func (p *PairsManager) getPairStore(a asset.Item) (*PairStore, error) {
if p.Pairs == nil {
return nil, errors.New("pair manager not initialised")
}
c, ok := p.Pairs[a]
if !ok {
return nil, errors.New("asset type not found")
}
if c == nil {
return nil, errors.New("currency store is nil")
}
return c, nil
}

View File

@@ -8,11 +8,21 @@ import (
var p PairsManager
func initTest() {
func initTest(t *testing.T) {
spotAvailable, err := NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"})
if err != nil {
t.Fatal(err)
}
spotEnabled, err := NewPairsFromStrings([]string{"BTC-USD"})
if err != nil {
t.Fatal(err)
}
p.Store(asset.Spot,
PairStore{
Available: NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}),
Enabled: NewPairsFromStrings([]string{"BTC-USD"}),
Available: spotAvailable,
Enabled: spotEnabled,
RequestFormat: &PairFormat{
Uppercase: true,
},
@@ -25,7 +35,7 @@ func initTest() {
}
func TestGetAssetTypes(t *testing.T) {
initTest()
initTest(t)
a := p.GetAssetTypes()
if len(a) == 0 {
@@ -38,22 +48,34 @@ func TestGetAssetTypes(t *testing.T) {
}
func TestGet(t *testing.T) {
initTest()
initTest(t)
if p.Get(asset.Spot) == nil {
t.Error("Spot assets shouldn't be nil")
_, err := p.Get(asset.Spot)
if err != nil {
t.Error(err)
}
if p.Get(asset.Futures) != nil {
_, err = p.Get(asset.Futures)
if err == nil {
t.Error("Futures should be nil")
}
}
func TestStore(t *testing.T) {
availPairs, err := NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"})
if err != nil {
t.Fatal(err)
}
enabledPairs, err := NewPairsFromStrings([]string{"BTC-USD"})
if err != nil {
t.Fatal(err)
}
p.Store(asset.Futures,
PairStore{
Available: NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}),
Enabled: NewPairsFromStrings([]string{"BTC-USD"}),
Available: availPairs,
Enabled: enabledPairs,
RequestFormat: &PairFormat{
Uppercase: true,
},
@@ -64,7 +86,12 @@ func TestStore(t *testing.T) {
},
)
if p.Get(asset.Futures) == nil {
f, err := p.Get(asset.Futures)
if err != nil {
t.Fatal(err)
}
if f == nil {
t.Error("Futures assets shouldn't be nil")
}
}
@@ -73,67 +100,131 @@ func TestDelete(t *testing.T) {
p.Pairs = nil
p.Delete(asset.Spot)
p.Store(asset.Spot,
PairStore{
Available: NewPairsFromStrings([]string{"BTC-USD"}),
},
)
btcusdPairs, err := NewPairsFromStrings([]string{"BTC-USD"})
if err != nil {
t.Fatal(err)
}
p.Store(asset.Spot, PairStore{
Available: btcusdPairs,
})
p.Delete(asset.UpsideProfitContract)
if p.Get(asset.Spot) == nil {
spotPS, err := p.Get(asset.Spot)
if err != nil {
t.Fatal(err)
}
if spotPS == nil {
t.Error("AssetTypeSpot should exist")
}
p.Delete(asset.Spot)
if p.Get(asset.Spot) != nil {
if _, err := p.Get(asset.Spot); err == nil {
t.Error("Delete should have deleted AssetTypeSpot")
}
}
func TestGetPairs(t *testing.T) {
p.Pairs = nil
pairs := p.GetPairs(asset.Spot, true)
pairs, err := p.GetPairs(asset.Spot, true)
if err != nil {
t.Fatal(err)
}
if pairs != nil {
t.Fatal("pairs shouldn't be populated")
}
initTest()
pairs = p.GetPairs(asset.Spot, true)
initTest(t)
pairs, err = p.GetPairs(asset.Spot, true)
if err != nil {
t.Fatal(err)
}
if pairs == nil {
t.Fatal("pairs should be populated")
}
pairs = p.GetPairs("blah", true)
pairs, err = p.GetPairs("blah", true)
if err != nil {
t.Fatal(err)
}
if pairs != nil {
t.Fatal("pairs shouldn't be populated")
}
superfluous := NewPair(DASH, USDT)
newPairs := p.Pairs[asset.Spot].Enabled.Add(superfluous)
p.Pairs[asset.Spot].Enabled = newPairs
_, err = p.GetPairs(asset.Spot, true)
if err == nil {
t.Fatal("error cannot be nil")
}
}
func TestStorePairs(t *testing.T) {
p.Pairs = nil
p.StorePairs(asset.Spot, NewPairsFromStrings([]string{"ETH-USD"}), false)
pairs := p.GetPairs(asset.Spot, false)
if !pairs.Contains(NewPairFromString("ETH-USD"), true) {
ethusdPairs, err := NewPairsFromStrings([]string{"ETH-USD"})
if err != nil {
t.Fatal(err)
}
p.StorePairs(asset.Spot, ethusdPairs, false)
pairs, err := p.GetPairs(asset.Spot, false)
if err != nil {
t.Fatal(err)
}
ethusd, err := NewPairFromString("ETH-USD")
if err != nil {
t.Fatal(err)
}
if !pairs.Contains(ethusd, true) {
t.Errorf("TestStorePairs failed, unexpected result")
}
initTest()
p.StorePairs(asset.Spot, NewPairsFromStrings([]string{"ETH-USD"}), false)
pairs = p.GetPairs(asset.Spot, false)
initTest(t)
p.StorePairs(asset.Spot, ethusdPairs, false)
pairs, err = p.GetPairs(asset.Spot, false)
if err != nil {
t.Fatal(err)
}
if pairs == nil {
t.Errorf("pairs should be populated")
}
if !pairs.Contains(NewPairFromString("ETH-USD"), true) {
if !pairs.Contains(ethusd, true) {
t.Errorf("TestStorePairs failed, unexpected result")
}
p.StorePairs(asset.Futures, NewPairsFromStrings([]string{"ETH-KRW"}), true)
pairs = p.GetPairs(asset.Futures, true)
ethkrwPairs, err := NewPairsFromStrings([]string{"ETH-KRW"})
if err != nil {
t.Error(err)
}
p.StorePairs(asset.Futures, ethkrwPairs, true)
p.StorePairs(asset.Futures, ethkrwPairs, false)
pairs, err = p.GetPairs(asset.Futures, true)
if err != nil {
t.Fatal(err)
}
if pairs == nil {
t.Errorf("pairs futures should be populated")
}
if !pairs.Contains(NewPairFromString("ETH-KRW"), true) {
ethkrw, err := NewPairFromString("ETH-KRW")
if err != nil {
t.Error(err)
}
if !pairs.Contains(ethkrw, true) {
t.Errorf("TestStorePairs failed, unexpected result")
}
}
@@ -146,7 +237,7 @@ func TestDisablePair(t *testing.T) {
}
// Test asset type which doesn't exist
initTest()
initTest(t)
if err := p.DisablePair(asset.Futures, Pair{}); err == nil {
t.Error("unexpected result")
}
@@ -158,7 +249,7 @@ func TestDisablePair(t *testing.T) {
}
// Test disabling a pair which isn't enabled
initTest()
initTest(t)
if err := p.DisablePair(asset.Spot, NewPair(LTC, USD)); err == nil {
t.Error("unexpected result")
}
@@ -177,7 +268,7 @@ func TestEnablePair(t *testing.T) {
}
// Test asset type which doesn't exist
initTest()
initTest(t)
if err := p.EnablePair(asset.Futures, Pair{}); err == nil {
t.Error("unexpected result")
}
@@ -189,7 +280,7 @@ func TestEnablePair(t *testing.T) {
}
// Test enabling a pair which isn't in the list of available pairs
initTest()
initTest(t)
if err := p.EnablePair(asset.Spot, NewPair(ETH, USD)); err == nil {
t.Error("unexpected result")
}
@@ -204,3 +295,55 @@ func TestEnablePair(t *testing.T) {
t.Error("unexpected result")
}
}
func TestIsAssetEnabled_SetAssetEnabled(t *testing.T) {
p.Pairs = nil
// Test enabling a pair when the pair manager is not initialised
err := p.IsAssetEnabled(asset.Spot)
if err == nil {
t.Error("unexpected result")
}
err = p.SetAssetEnabled(asset.Spot, true)
if err == nil {
t.Fatal("unexpected result")
}
// Test asset type which doesn't exist
initTest(t)
err = p.IsAssetEnabled(asset.Spot)
if err == nil {
t.Error("unexpected result")
}
err = p.SetAssetEnabled(asset.Spot, false)
if err != nil {
t.Fatal(err)
}
err = p.SetAssetEnabled(asset.Spot, false)
if err == nil {
t.Fatal("unexpected result")
}
err = p.IsAssetEnabled(asset.Spot)
if err == nil {
t.Error("unexpected result")
}
err = p.SetAssetEnabled(asset.Spot, true)
if err != nil {
t.Fatal(err)
}
err = p.SetAssetEnabled(asset.Spot, true)
if err == nil {
t.Fatal("unexpected result")
}
err = p.IsAssetEnabled(asset.Spot)
if err != nil {
t.Error("unexpected result")
}
}

View File

@@ -12,13 +12,13 @@ type PairsManager struct {
ConfigFormat *PairFormat `json:"configFormat,omitempty"`
UseGlobalFormat bool `json:"useGlobalFormat,omitempty"`
LastUpdated int64 `json:"lastUpdated,omitempty"`
AssetTypes asset.Items `json:"assetTypes"`
Pairs map[asset.Item]*PairStore `json:"pairs"`
m sync.Mutex
m sync.RWMutex
}
// PairStore stores a currency pair store
type PairStore struct {
AssetEnabled *bool `json:"assetEnabled"`
Enabled Pairs `json:"enabled"`
Available Pairs `json:"available"`
RequestFormat *PairFormat `json:"requestFormat,omitempty"`

View File

@@ -1,15 +1,24 @@
package currency
import (
"encoding/json"
"fmt"
"strings"
)
// NewPairDelimiter splits the desired currency string at delimeter, the returns
// a Pair struct
func NewPairDelimiter(currencyPair, delimiter string) Pair {
func NewPairDelimiter(currencyPair, delimiter string) (Pair, error) {
if !strings.Contains(currencyPair, delimiter) {
return Pair{},
fmt.Errorf("delimiter: [%s] not found in currencypair string", delimiter)
}
result := strings.Split(currencyPair, delimiter)
if len(result) < 2 {
return Pair{},
fmt.Errorf("supplied pair: [%s] cannot be split with %s",
currencyPair,
delimiter)
}
if len(result) > 2 {
result[1] = strings.Join(result[1:], delimiter)
}
@@ -17,15 +26,24 @@ func NewPairDelimiter(currencyPair, delimiter string) Pair {
Delimiter: delimiter,
Base: NewCode(result[0]),
Quote: NewCode(result[1]),
}
}, nil
}
// NewPairFromStrings returns a CurrencyPair without a delimiter
func NewPairFromStrings(baseCurrency, quoteCurrency string) Pair {
return Pair{
Base: NewCode(baseCurrency),
Quote: NewCode(quoteCurrency),
func NewPairFromStrings(base, quote string) (Pair, error) {
if strings.Contains(base, " ") {
return Pair{},
fmt.Errorf("cannot create pair, invalid base currency string [%s]",
base)
}
if strings.Contains(quote, " ") {
return Pair{},
fmt.Errorf("cannot create pair, invalid quote currency string [%s]",
quote)
}
return Pair{Base: NewCode(base), Quote: NewCode(quote)}, nil
}
// NewPair returns a currency pair from currency codes
@@ -55,23 +73,24 @@ func NewPairFromIndex(currencyPair, index string) (Pair, error) {
}
if i == 0 {
return NewPairFromStrings(currencyPair[0:len(index)],
currencyPair[len(index):]),
nil
currencyPair[len(index):])
}
return NewPairFromStrings(currencyPair[0:i], currencyPair[i:]), nil
return NewPairFromStrings(currencyPair[0:i], currencyPair[i:])
}
// NewPairFromString converts currency string into a new CurrencyPair
// with or without delimeter
func NewPairFromString(currencyPair string) Pair {
delimiters := []string{"_", "-", "/", ":"}
var delimiter string
for _, x := range delimiters {
if strings.Contains(currencyPair, x) {
delimiter = x
return NewPairDelimiter(currencyPair, delimiter)
func NewPairFromString(currencyPair string) (Pair, error) {
for x := range delimiters {
if strings.Contains(currencyPair, delimiters[x]) {
return NewPairDelimiter(currencyPair, delimiters[x])
}
}
if len(currencyPair) < 3 {
return Pair{},
fmt.Errorf("cannot create pair from %s string",
currencyPair)
}
return NewPairFromStrings(currencyPair[0:3], currencyPair[3:])
}
@@ -79,129 +98,12 @@ func NewPairFromString(currencyPair string) Pair {
// with a specific format. This is helpful for exchanges which
// provide currency pairs with no delimiter so we can match it with a list and
// apply the same format
func NewPairFromFormattedPairs(currencyPair string, pairs Pairs, pairFmt PairFormat) Pair {
func NewPairFromFormattedPairs(currencyPair string, pairs Pairs, pairFmt PairFormat) (Pair, error) {
for x := range pairs {
if strings.EqualFold(pairs[x].Format(pairFmt.Delimiter,
pairFmt.Uppercase).String(), currencyPair) {
return pairs[x]
fPair := pairs[x].Format(pairFmt.Delimiter, pairFmt.Uppercase)
if strings.EqualFold(fPair.String(), currencyPair) {
return pairs[x], nil
}
}
return NewPairFromString(currencyPair)
}
// String returns a currency pair string
func (p Pair) String() string {
return p.Base.String() + p.Delimiter + p.Quote.String()
}
// Lower converts the pair object to lowercase
func (p Pair) Lower() Pair {
return Pair{
Delimiter: p.Delimiter,
Base: p.Base.Lower(),
Quote: p.Quote.Lower(),
}
}
// Upper converts the pair object to uppercase
func (p Pair) Upper() Pair {
return Pair{
Delimiter: p.Delimiter,
Base: p.Base.Upper(),
Quote: p.Quote.Upper(),
}
}
// UnmarshalJSON comforms type to the umarshaler interface
func (p *Pair) UnmarshalJSON(d []byte) error {
var pair string
err := json.Unmarshal(d, &pair)
if err != nil {
return err
}
*p = NewPairFromString(pair)
return nil
}
// MarshalJSON conforms type to the marshaler interface
func (p Pair) MarshalJSON() ([]byte, error) {
return json.Marshal(p.String())
}
// Format changes the currency based on user preferences overriding the default
// String() display
func (p Pair) Format(delimiter string, uppercase bool) Pair {
p.Delimiter = delimiter
if uppercase {
return p.Upper()
}
return p.Lower()
}
// Equal compares two currency pairs and returns whether or not they are equal
func (p Pair) Equal(cPair Pair) bool {
return strings.EqualFold(p.Base.String(), cPair.Base.String()) &&
strings.EqualFold(p.Quote.String(), cPair.Quote.String())
}
// EqualIncludeReciprocal compares two currency pairs and returns whether or not
// they are the same including reciprocal currencies.
func (p Pair) EqualIncludeReciprocal(cPair Pair) bool {
if p.Base.Item == cPair.Base.Item &&
p.Quote.Item == cPair.Quote.Item ||
p.Base.Item == cPair.Quote.Item &&
p.Quote.Item == cPair.Base.Item {
return true
}
return false
}
// IsCryptoPair checks to see if the pair is a crypto pair e.g. BTCLTC
func (p Pair) IsCryptoPair() bool {
return storage.IsCryptocurrency(p.Base) &&
storage.IsCryptocurrency(p.Quote)
}
// IsCryptoFiatPair checks to see if the pair is a crypto fiat pair e.g. BTCUSD
func (p Pair) IsCryptoFiatPair() bool {
return storage.IsCryptocurrency(p.Base) &&
storage.IsFiatCurrency(p.Quote) ||
storage.IsFiatCurrency(p.Base) &&
storage.IsCryptocurrency(p.Quote)
}
// IsFiatPair checks to see if the pair is a fiat pair e.g. EURUSD
func (p Pair) IsFiatPair() bool {
return storage.IsFiatCurrency(p.Base) && storage.IsFiatCurrency(p.Quote)
}
// IsInvalid checks invalid pair if base and quote are the same
func (p Pair) IsInvalid() bool {
return p.Base.Item == p.Quote.Item
}
// Swap turns the currency pair into its reciprocal
func (p Pair) Swap() Pair {
p.Base, p.Quote = p.Quote, p.Base
return p
}
// IsEmpty returns whether or not the pair is empty or is missing a currency
// code
func (p Pair) IsEmpty() bool {
return p.Base.IsEmpty() || p.Quote.IsEmpty()
}
// ContainsCurrency checks to see if a pair contains a specific currency
func (p Pair) ContainsCurrency(c Code) bool {
return p.Base.Item == c.Item || p.Quote.Item == c.Item
}
// Pair holds currency pair information
type Pair struct {
Delimiter string `json:"delimiter"`
Base Code `json:"base"`
Quote Code `json:"quote"`
}

117
currency/pair_methods.go Normal file
View File

@@ -0,0 +1,117 @@
package currency
import (
"encoding/json"
"strings"
)
// String returns a currency pair string
func (p Pair) String() string {
return p.Base.String() + p.Delimiter + p.Quote.String()
}
// Lower converts the pair object to lowercase
func (p Pair) Lower() Pair {
return Pair{
Delimiter: p.Delimiter,
Base: p.Base.Lower(),
Quote: p.Quote.Lower(),
}
}
// Upper converts the pair object to uppercase
func (p Pair) Upper() Pair {
return Pair{
Delimiter: p.Delimiter,
Base: p.Base.Upper(),
Quote: p.Quote.Upper(),
}
}
// UnmarshalJSON comforms type to the umarshaler interface
func (p *Pair) UnmarshalJSON(d []byte) error {
var pair string
err := json.Unmarshal(d, &pair)
if err != nil {
return err
}
newPair, err := NewPairFromString(pair)
if err != nil {
return err
}
p.Base = newPair.Base
p.Quote = newPair.Quote
p.Delimiter = newPair.Delimiter
return nil
}
// MarshalJSON conforms type to the marshaler interface
func (p Pair) MarshalJSON() ([]byte, error) {
return json.Marshal(p.String())
}
// Format changes the currency based on user preferences overriding the default
// String() display
func (p Pair) Format(delimiter string, uppercase bool) Pair {
newP := Pair{Base: p.Base, Quote: p.Quote, Delimiter: delimiter}
if uppercase {
return newP.Upper()
}
return newP.Lower()
}
// Equal compares two currency pairs and returns whether or not they are equal
func (p Pair) Equal(cPair Pair) bool {
return strings.EqualFold(p.Base.String(), cPair.Base.String()) &&
strings.EqualFold(p.Quote.String(), cPair.Quote.String())
}
// EqualIncludeReciprocal compares two currency pairs and returns whether or not
// they are the same including reciprocal currencies.
func (p Pair) EqualIncludeReciprocal(cPair Pair) bool {
if (p.Base.Item == cPair.Base.Item && p.Quote.Item == cPair.Quote.Item) ||
(p.Base.Item == cPair.Quote.Item && p.Quote.Item == cPair.Base.Item) {
return true
}
return false
}
// IsCryptoPair checks to see if the pair is a crypto pair e.g. BTCLTC
func (p Pair) IsCryptoPair() bool {
return storage.IsCryptocurrency(p.Base) &&
storage.IsCryptocurrency(p.Quote)
}
// IsCryptoFiatPair checks to see if the pair is a crypto fiat pair e.g. BTCUSD
func (p Pair) IsCryptoFiatPair() bool {
return (storage.IsCryptocurrency(p.Base) && storage.IsFiatCurrency(p.Quote)) ||
(storage.IsFiatCurrency(p.Base) && storage.IsCryptocurrency(p.Quote))
}
// IsFiatPair checks to see if the pair is a fiat pair e.g. EURUSD
func (p Pair) IsFiatPair() bool {
return storage.IsFiatCurrency(p.Base) && storage.IsFiatCurrency(p.Quote)
}
// IsInvalid checks invalid pair if base and quote are the same
func (p Pair) IsInvalid() bool {
return p.Base.Item == p.Quote.Item
}
// Swap turns the currency pair into its reciprocal
func (p Pair) Swap() Pair {
return Pair{Base: p.Quote, Quote: p.Base}
}
// IsEmpty returns whether or not the pair is empty or is missing a currency
// code
func (p Pair) IsEmpty() bool {
return p.Base.IsEmpty() && p.Quote.IsEmpty()
}
// ContainsCurrency checks to see if a pair contains a specific currency
func (p Pair) ContainsCurrency(c Code) bool {
return p.Base.Item == c.Item || p.Quote.Item == c.Item
}

View File

@@ -12,21 +12,35 @@ const (
func TestLower(t *testing.T) {
t.Parallel()
pair := NewPairFromString(defaultPair)
pair, err := NewPairFromString(defaultPair)
if err != nil {
t.Fatal(err)
}
actual := pair.Lower()
expected := NewPairFromString(defaultPair).Lower()
if actual != expected {
expected, err := NewPairFromString(defaultPair)
if err != nil {
t.Fatal(err)
}
if actual.String() != expected.Lower().String() {
t.Errorf("Lower(): %s was not equal to expected value: %s",
actual, expected)
actual,
expected.Lower())
}
}
func TestUpper(t *testing.T) {
t.Parallel()
pair := NewPairFromString(defaultPair)
pair, err := NewPairFromString(defaultPair)
if err != nil {
t.Fatal(err)
}
actual := pair.Upper()
expected := NewPairFromString(defaultPair)
if actual != expected {
expected, err := NewPairFromString(defaultPair)
if err != nil {
t.Fatal(err)
}
if actual.String() != expected.String() {
t.Errorf("Upper(): %s was not equal to expected value: %s",
actual, expected)
}
@@ -34,7 +48,10 @@ func TestUpper(t *testing.T) {
func TestPairUnmarshalJSON(t *testing.T) {
var unmarshalHere Pair
configPair := NewPairDelimiter("btc_usd", "_")
configPair, err := NewPairDelimiter("btc_usd", "_")
if err != nil {
t.Fatal(err)
}
encoded, err := json.Marshal(configPair)
if err != nil {
@@ -59,9 +76,9 @@ func TestPairUnmarshalJSON(t *testing.T) {
func TestPairMarshalJSON(t *testing.T) {
quickstruct := struct {
Pair Pair `json:"superPair"`
Pair *Pair `json:"superPair"`
}{
Pair{Base: BTC, Quote: USD, Delimiter: "-"},
&Pair{Base: BTC, Quote: USD, Delimiter: "-"},
}
encoded, err := json.Marshal(quickstruct)
@@ -158,7 +175,14 @@ func TestPair(t *testing.T) {
func TestDisplay(t *testing.T) {
t.Parallel()
pair := NewPairDelimiter(defaultPairWDelimiter, "-")
_, err := NewPairDelimiter(defaultPairWDelimiter, "wow")
if err == nil {
t.Fatal("error cannot be nil")
}
pair, err := NewPairDelimiter(defaultPairWDelimiter, "-")
if err != nil {
t.Fatal(err)
}
actual := pair.String()
expected := defaultPairWDelimiter
if actual != expected {
@@ -319,7 +343,24 @@ func TestNewPairWithDelimiter(t *testing.T) {
func TestNewPairDelimiter(t *testing.T) {
t.Parallel()
pair := NewPairDelimiter(defaultPairWDelimiter, "-")
_, err := NewPairDelimiter("", "")
if err == nil {
t.Fatal("error cannot be nil")
}
_, err = NewPairDelimiter("BTC_USD", "wow")
if err == nil {
t.Fatal("error cannot be nil")
}
_, err = NewPairDelimiter("BTC_USD", " ")
if err == nil {
t.Fatal("error cannot be nil")
}
pair, err := NewPairDelimiter(defaultPairWDelimiter, "-")
if err != nil {
t.Fatal(err)
}
actual := pair.String()
expected := defaultPairWDelimiter
if actual != expected {
@@ -338,7 +379,10 @@ func TestNewPairDelimiter(t *testing.T) {
)
}
pair = NewPairDelimiter("BTC-MOVE-0626", "-")
pair, err = NewPairDelimiter("BTC-MOVE-0626", "-")
if err != nil {
t.Fatal(err)
}
actual = pair.String()
expected = "BTC-MOVE-0626"
if actual != expected {
@@ -348,7 +392,10 @@ func TestNewPairDelimiter(t *testing.T) {
)
}
pair = NewPairDelimiter("fBTC-USDT", "-")
pair, err = NewPairDelimiter("fBTC-USDT", "-")
if err != nil {
t.Fatal(err)
}
actual = pair.String()
expected = "fBTC-USDT"
if actual != expected {
@@ -404,7 +451,10 @@ func TestNewPairFromIndex(t *testing.T) {
func TestNewPairFromString(t *testing.T) {
t.Parallel()
pairStr := defaultPairWDelimiter
pair := NewPairFromString(pairStr)
pair, err := NewPairFromString(pairStr)
if err != nil {
t.Fatal(err)
}
actual := pair.String()
expected := defaultPairWDelimiter
if actual != expected {
@@ -415,7 +465,10 @@ func TestNewPairFromString(t *testing.T) {
}
pairStr = defaultPair
pair = NewPairFromString(pairStr)
pair, err = NewPairFromString(pairStr)
if err != nil {
t.Fatal(err)
}
actual = pair.String()
expected = defaultPair
if actual != expected {
@@ -428,23 +481,45 @@ func TestNewPairFromString(t *testing.T) {
func TestNewPairFromFormattedPairs(t *testing.T) {
t.Parallel()
p1, err := NewPairDelimiter("BTC-USDT", "-")
if err != nil {
t.Fatal(err)
}
p2, err := NewPairDelimiter("LTC-USD", "-")
if err != nil {
t.Fatal(err)
}
pairs := Pairs{
NewPairDelimiter("BTC-USDT", "-"),
NewPairDelimiter("LTC-USD", "-"),
p1,
p2,
}
p, err := NewPairFromFormattedPairs("BTCUSDT", pairs, PairFormat{
Uppercase: true,
})
if err != nil {
t.Fatal(err)
}
p := NewPairFromFormattedPairs("BTCUSDT", pairs, PairFormat{Uppercase: true})
if p.String() != "BTC-USDT" {
t.Error("TestNewPairFromFormattedPairs: Expected currency was not found")
}
p = NewPairFromFormattedPairs("btcusdt", pairs, PairFormat{Uppercase: false})
p, err = NewPairFromFormattedPairs("btcusdt", pairs, PairFormat{Uppercase: false})
if err != nil {
t.Fatal(err)
}
if p.String() != "BTC-USDT" {
t.Error("TestNewPairFromFormattedPairs: Expected currency was not found")
}
// Now a wrong one, will default to NewPairFromString
p = NewPairFromFormattedPairs("ethusdt", pairs, PairFormat{})
p, err = NewPairFromFormattedPairs("ethusdt", pairs, PairFormat{})
if err != nil {
t.Fatal(err)
}
if p.String() != "ethusdt" && p.Base.String() != "eth" {
t.Error("TestNewPairFromFormattedPairs: Expected currency was not found")
}
@@ -514,29 +589,43 @@ func TestCopyPairFormat(t *testing.T) {
t.Error("TestCopyPairFormat: Expected pair was not found")
}
result = CopyPairFormat(NewPair(ETH, USD), pairs, true)
np := NewPair(ETH, USD)
result = CopyPairFormat(np, pairs, true)
if result.String() != "" {
t.Error("TestCopyPairFormat: Unexpected non empty pair returned")
}
}
func TestFindPairDifferences(t *testing.T) {
pairList := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD"})
pairList, err := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD"})
if err != nil {
t.Fatal(err)
}
dash, err := NewPairsFromStrings([]string{"DASH-USD"})
if err != nil {
t.Fatal(err)
}
// Test new pair update
newPairs, removedPairs := pairList.FindDifferences(NewPairsFromStrings([]string{"DASH-USD"}))
newPairs, removedPairs := pairList.FindDifferences(dash)
if len(newPairs) != 1 && len(removedPairs) != 3 {
t.Error("TestFindPairDifferences: Unexpected values")
}
emptyPairsList, err := NewPairsFromStrings([]string{""})
if err != nil {
t.Fatal(err)
}
// Test that we don't allow empty strings for new pairs
newPairs, removedPairs = pairList.FindDifferences(NewPairsFromStrings([]string{""}))
newPairs, removedPairs = pairList.FindDifferences(emptyPairsList)
if len(newPairs) != 0 && len(removedPairs) != 3 {
t.Error("TestFindPairDifferences: Unexpected values")
}
// Test that we don't allow empty strings for new pairs
newPairs, removedPairs = NewPairsFromStrings([]string{""}).FindDifferences(pairList)
newPairs, removedPairs = emptyPairsList.FindDifferences(pairList)
if len(newPairs) != 3 && len(removedPairs) != 0 {
t.Error("TestFindPairDifferences: Unexpected values")
}

12
currency/pair_types.go Normal file
View File

@@ -0,0 +1,12 @@
package currency
// Pair holds currency pair information
type Pair struct {
ID string `json:"id"`
Delimiter string `json:"delimiter,omitempty"`
Base Code `json:"base,omitempty"`
Quote Code `json:"quote,omitempty"`
}
// Pairs defines a list of pairs
type Pairs []Pair

View File

@@ -10,16 +10,21 @@ import (
// NewPairsFromStrings takes in currency pair strings and returns a currency
// pair list
func NewPairsFromStrings(pairs []string) Pairs {
var ps Pairs
for _, p := range pairs {
if p == "" {
func NewPairsFromStrings(pairs []string) (Pairs, error) {
var newPairs Pairs
for i := range pairs {
if pairs[i] == "" {
continue
}
ps = append(ps, NewPairFromString(p))
newPair, err := NewPairFromString(pairs[i])
if err != nil {
return nil, err
}
newPairs = append(newPairs, newPair)
}
return ps
return newPairs, nil
}
// Strings returns a slice of strings referring to each currency pair
@@ -79,8 +84,13 @@ func (p *Pairs) UnmarshalJSON(d []byte) error {
}
var allThePairs Pairs
for _, data := range strings.Split(pairs, ",") {
allThePairs = append(allThePairs, NewPairFromString(data))
oldPairs := strings.Split(pairs, ",")
for i := range oldPairs {
pair, err := NewPairFromString(oldPairs[i])
if err != nil {
return err
}
allThePairs = append(allThePairs, pair)
}
*p = allThePairs
@@ -101,21 +111,16 @@ func (p Pairs) Upper() Pairs {
return upper
}
// Slice exposes the underlying type
func (p Pairs) Slice() []Pair {
return p
}
// Contains checks to see if a specified pair exists inside a currency pair
// array
func (p Pairs) Contains(check Pair, exact bool) bool {
for _, pair := range p.Slice() {
for i := range p {
if exact {
if pair.Equal(check) {
if p[i].Equal(check) {
return true
}
} else {
if pair.EqualIncludeReciprocal(check) {
if p[i].EqualIncludeReciprocal(check) {
return true
}
}
@@ -127,11 +132,11 @@ func (p Pairs) Contains(check Pair, exact bool) bool {
// and removes it from the list of pairs
func (p Pairs) RemovePairsByFilter(filter Code) Pairs {
var pairs Pairs
for _, pair := range p.Slice() {
if pair.ContainsCurrency(filter) {
for i := range p {
if p[i].ContainsCurrency(filter) {
continue
}
pairs = append(pairs, pair)
pairs = append(pairs, p[i])
}
return pairs
}
@@ -167,12 +172,12 @@ func (p Pairs) FindDifferences(pairs Pairs) (newPairs, removedPairs Pairs) {
newPairs = append(newPairs, pairs[x])
}
}
for _, oldPair := range p {
if oldPair.String() == "" {
for x := range p {
if p[x].String() == "" {
continue
}
if !pairs.Contains(oldPair, true) {
removedPairs = append(removedPairs, oldPair)
if !pairs.Contains(p[x], true) {
removedPairs = append(removedPairs, p[x])
}
}
return
@@ -188,6 +193,3 @@ func (p Pairs) GetRandomPair() Pair {
return p[rand.Intn(pairsLen)]
}
// Pairs defines a list of pairs
type Pairs []Pair

View File

@@ -6,7 +6,10 @@ import (
)
func TestPairsUpper(t *testing.T) {
pairs := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
pairs, err := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
if err != nil {
t.Fatal(err)
}
expected := "BTC_USD,BTC_AUD,BTC_LTC"
if pairs.Upper().Join() != expected {
@@ -16,7 +19,10 @@ func TestPairsUpper(t *testing.T) {
}
func TestPairsString(t *testing.T) {
pairs := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
pairs, err := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
if err != nil {
t.Fatal(err)
}
expected := []string{"btc_usd", "btc_aud", "btc_ltc"}
for i, p := range pairs {
@@ -28,7 +34,10 @@ func TestPairsString(t *testing.T) {
}
func TestPairsJoin(t *testing.T) {
pairs := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
pairs, err := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
if err != nil {
t.Fatal(err)
}
expected := "btc_usd,btc_aud,btc_ltc"
if pairs.Join() != expected {
@@ -38,7 +47,10 @@ func TestPairsJoin(t *testing.T) {
}
func TestPairsFormat(t *testing.T) {
pairs := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
pairs, err := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
if err != nil {
t.Fatal(err)
}
expected := "BTC-USD,BTC-AUD,BTC-LTC"
if pairs.Format("-", "", true).Join() != expected {
@@ -57,7 +69,10 @@ func TestPairsFormat(t *testing.T) {
expected, pairs.Format(":", "KRW", true).Join())
}
pairs = NewPairsFromStrings([]string{"DASHKRW", "BTCKRW"})
pairs, err = NewPairsFromStrings([]string{"DASHKRW", "BTCKRW"})
if err != nil {
t.Fatal(err)
}
expected = "dash-krw,btc-krw"
if pairs.Format("-", "KRW", false).Join() != expected {
t.Errorf("Pairs Join() error expected %s but received %s",
@@ -106,10 +121,15 @@ func TestPairsUnmarshalJSON(t *testing.T) {
}
func TestPairsMarshalJSON(t *testing.T) {
pairs, err := NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"})
if err != nil {
t.Fatal(err)
}
quickstruct := struct {
Pairs Pairs `json:"soManyPairs"`
}{
Pairs: NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"}),
Pairs: pairs,
}
encoded, err := json.Marshal(quickstruct)
@@ -176,12 +196,21 @@ func TestContains(t *testing.T) {
var pairs = Pairs{
NewPair(BTC, USD),
NewPair(LTC, USD),
NewPair(USD, ZRX),
}
if !pairs.Contains(NewPair(BTC, USD), true) {
t.Errorf("TestContains: Expected pair was not found")
}
if pairs.Contains(NewPair(USD, BTC), true) {
t.Errorf("TestContains: Unexpected pair was found")
}
if !pairs.Contains(NewPair(USD, BTC), false) {
t.Errorf("TestContains: Expected pair was not found")
}
if pairs.Contains(NewPair(ETH, USD), false) {
t.Errorf("TestContains: Non-existent pair was found")
}

View File

@@ -23,8 +23,14 @@ func init() {
func (s *Storage) SetDefaults() {
s.defaultBaseCurrency = USD
s.baseCurrency = s.defaultBaseCurrency
s.SetDefaultFiatCurrencies(USD, AUD, EUR, CNY)
s.SetDefaultCryptocurrencies(BTC, LTC, ETH, DOGE, DASH, XRP, XMR)
err := s.SetDefaultFiatCurrencies(USD, AUD, EUR, CNY)
if err != nil {
log.Errorf(log.Global, "Currency Storage: Setting default fiat currencies error: %s", err)
}
err = s.SetDefaultCryptocurrencies(BTC, LTC, ETH, DOGE, DASH, XRP, XMR)
if err != nil {
log.Errorf(log.Global, "Currency Storage: Setting default cryptocurrencies error: %s", err)
}
s.SetupConversionRates()
s.fiatExchangeMarkets = forexprovider.NewDefaultFXProvider()
}
@@ -33,8 +39,9 @@ func (s *Storage) SetDefaults() {
// dump file and keep foreign exchange rates updated as fast as possible without
// triggering rate limiters, it will also run a full cryptocurrency check
// through coin market cap and expose analytics for exchange services
func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration, filePath string, verbose bool) error {
func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration, filePath string) error {
s.mtx.Lock()
s.shutdown = make(chan struct{})
if !settings.Cryptocurrencies.HasData() {
s.mtx.Unlock()
@@ -48,17 +55,17 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration
}
s.baseCurrency = settings.FiatDisplayCurrency
log.Debugf(log.Global,
"Fiat display currency: %s.\n", s.baseCurrency)
"Fiat display currency: %s.\n",
s.baseCurrency)
if settings.CryptocurrencyProvider.Enabled {
log.Debugln(
log.Global,
log.Debugln(log.Global,
"Setting up currency analysis system with Coinmarketcap...")
c := &coinmarketcap.Coinmarketcap{}
c.SetDefaults()
err := c.Setup(coinmarketcap.Settings{
Name: settings.CryptocurrencyProvider.Name,
Enabled: true,
Enabled: settings.CryptocurrencyProvider.Enabled,
AccountPlan: settings.CryptocurrencyProvider.AccountPlan,
APIkey: settings.CryptocurrencyProvider.APIkey,
Verbose: settings.CryptocurrencyProvider.Verbose,
@@ -174,16 +181,34 @@ func (s *Storage) SetupConversionRates() {
// SetDefaultFiatCurrencies assigns the default fiat currency list and adds it
// to the running list
func (s *Storage) SetDefaultFiatCurrencies(c ...Code) {
func (s *Storage) SetDefaultFiatCurrencies(c ...Code) error {
for i := range c {
err := s.currencyCodes.UpdateCurrency("", c[i].String(), "", 0, Fiat)
if err != nil {
return err
}
}
s.defaultFiatCurrencies = append(s.defaultFiatCurrencies, c...)
s.fiatCurrencies = append(s.fiatCurrencies, c...)
return nil
}
// SetDefaultCryptocurrencies assigns the default cryptocurrency list and adds
// it to the running list
func (s *Storage) SetDefaultCryptocurrencies(c ...Code) {
func (s *Storage) SetDefaultCryptocurrencies(c ...Code) error {
for i := range c {
err := s.currencyCodes.UpdateCurrency("",
c[i].String(),
"",
0,
Cryptocurrency)
if err != nil {
return err
}
}
s.defaultCryptoCurrencies = append(s.defaultCryptoCurrencies, c...)
s.cryptocurrencies = append(s.cryptocurrencies, c...)
return nil
}
// SetupForexProviders sets up a new instance of the forex providers
@@ -228,7 +253,7 @@ func (s *Storage) ForeignExchangeUpdater() {
for {
select {
case <-s.shutdownC:
case <-s.shutdown:
return
case <-SeedForeignExchangeTick.C:
@@ -248,36 +273,29 @@ func (s *Storage) ForeignExchangeUpdater() {
// SeedCurrencyAnalysisData sets a new instance of a coinmarketcap data.
func (s *Storage) SeedCurrencyAnalysisData() error {
b, err := ioutil.ReadFile(s.path)
if err != nil {
err = s.FetchCurrencyAnalysisData()
if s.currencyCodes.LastMainUpdate.IsZero() {
b, err := ioutil.ReadFile(s.path)
if err != nil {
return s.WriteCurrencyDataToFile(s.path, false)
return s.FetchCurrencyAnalysisData()
}
var f *File
err = json.Unmarshal(b, &f)
if err != nil {
return err
}
err = s.LoadFileCurrencyData(f)
if err != nil {
return err
}
return s.WriteCurrencyDataToFile(s.path, true)
}
var fromFile File
err = json.Unmarshal(b, &fromFile)
if err != nil {
return err
}
err = s.LoadFileCurrencyData(&fromFile)
if err != nil {
return err
}
// Based on update delay update the file
if fromFile.LastMainUpdate.After(fromFile.LastMainUpdate.Add(s.currencyFileUpdateDelay)) ||
fromFile.LastMainUpdate.IsZero() {
err = s.FetchCurrencyAnalysisData()
if time.Now().After(s.currencyCodes.LastMainUpdate.Add(s.currencyFileUpdateDelay)) ||
s.currencyCodes.LastMainUpdate.IsZero() {
err := s.FetchCurrencyAnalysisData()
if err != nil {
return s.WriteCurrencyDataToFile(s.path, false)
return err
}
return s.WriteCurrencyDataToFile(s.path, true)
}
return nil
@@ -304,7 +322,7 @@ func (s *Storage) WriteCurrencyDataToFile(path string, mainUpdate bool) error {
if mainUpdate {
t := time.Now()
data.LastMainUpdate = t
data.LastMainUpdate = t.Unix()
s.currencyCodes.LastMainUpdate = t
}
@@ -320,41 +338,62 @@ func (s *Storage) WriteCurrencyDataToFile(path string, mainUpdate bool) error {
// LoadFileCurrencyData loads currencies into the currency codes
func (s *Storage) LoadFileCurrencyData(f *File) error {
for i := range f.Contracts {
err := s.currencyCodes.LoadItem(&f.Contracts[i])
contract := f.Contracts[i]
contract.Role = Contract
err := s.currencyCodes.LoadItem(&contract)
if err != nil {
return err
}
}
for i := range f.Cryptocurrency {
err := s.currencyCodes.LoadItem(&f.Cryptocurrency[i])
crypto := f.Cryptocurrency[i]
crypto.Role = Cryptocurrency
err := s.currencyCodes.LoadItem(&crypto)
if err != nil {
return err
}
}
for i := range f.Token {
err := s.currencyCodes.LoadItem(&f.Token[i])
token := f.Token[i]
token.Role = Token
err := s.currencyCodes.LoadItem(&token)
if err != nil {
return err
}
}
for i := range f.FiatCurrency {
err := s.currencyCodes.LoadItem(&f.FiatCurrency[i])
fiat := f.FiatCurrency[i]
fiat.Role = Fiat
err := s.currencyCodes.LoadItem(&fiat)
if err != nil {
return err
}
}
for i := range f.UnsetCurrency {
err := s.currencyCodes.LoadItem(&f.UnsetCurrency[i])
unset := f.UnsetCurrency[i]
unset.Role = Unset
err := s.currencyCodes.LoadItem(&unset)
if err != nil {
return err
}
}
s.currencyCodes.LastMainUpdate = f.LastMainUpdate
switch t := f.LastMainUpdate.(type) {
case string:
parseT, err := time.Parse(time.RFC3339Nano, t)
if err != nil {
return err
}
s.currencyCodes.LastMainUpdate = parseT
case float64:
s.currencyCodes.LastMainUpdate = time.Unix(int64(t), 0)
default:
return errors.New("unhandled type conversion for LastMainUpdate time")
}
return nil
}
@@ -372,19 +411,22 @@ func (s *Storage) UpdateCurrencies() error {
}
if m[x].Platform.Symbol != "" {
err := s.currencyCodes.UpdateToken(m[x].Name,
err = s.currencyCodes.UpdateCurrency(m[x].Name,
m[x].Symbol,
m[x].Platform.Symbol,
m[x].ID)
m[x].ID,
Token)
if err != nil {
return err
}
continue
}
err := s.currencyCodes.UpdateCryptocurrency(m[x].Name,
err = s.currencyCodes.UpdateCurrency(m[x].Name,
m[x].Symbol,
m[x].ID)
"",
m[x].ID,
Cryptocurrency)
if err != nil {
return err
}
@@ -452,8 +494,7 @@ func (s *Storage) GetExchangeRates() (Conversions, error) {
func (s *Storage) SeedForeignExchangeRates() error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
rates, err := s.fiatExchangeMarkets.GetCurrencyData(
s.baseCurrency.String(),
rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
s.fiatCurrencies.Strings())
if err != nil {
return err
@@ -463,15 +504,7 @@ func (s *Storage) SeedForeignExchangeRates() error {
// UpdateForeignExchangeRates sets exchange rates on the FX map
func (s *Storage) updateExchangeRates(m map[string]float64) error {
err := s.fxRates.Update(m)
if err != nil {
return err
}
if s.path != "" {
return s.WriteCurrencyDataToFile(s.path, false)
}
return nil
return s.fxRates.Update(m)
}
// SetupCryptoProvider sets congiguration parameters and starts a new instance
@@ -502,9 +535,9 @@ func (s *Storage) GetTotalMarketCryptocurrencies() (Currencies, error) {
// IsDefaultCurrency returns if a currency is a default currency
func (s *Storage) IsDefaultCurrency(c Code) bool {
t, _ := GetTranslation(c)
for i := range s.defaultFiatCurrencies {
if s.defaultFiatCurrencies[i].Match(c) || s.defaultFiatCurrencies[i].Match(t) {
if s.defaultFiatCurrencies[i].Match(c) ||
s.defaultFiatCurrencies[i].Match(GetTranslation(c)) {
return true
}
}
@@ -514,9 +547,9 @@ func (s *Storage) IsDefaultCurrency(c Code) bool {
// IsDefaultCryptocurrency returns if a cryptocurrency is a default
// cryptocurrency
func (s *Storage) IsDefaultCryptocurrency(c Code) bool {
t, _ := GetTranslation(c)
for _, d := range s.defaultCryptoCurrencies {
if d.Match(c) || d.Match(t) {
for i := range s.defaultCryptoCurrencies {
if s.defaultCryptoCurrencies[i].Match(c) ||
s.defaultCryptoCurrencies[i].Match(GetTranslation(c)) {
return true
}
}
@@ -534,9 +567,9 @@ func (s *Storage) IsFiatCurrency(c Code) bool {
return false
}
t, _ := GetTranslation(c)
for _, d := range s.fiatCurrencies {
if d.Match(c) || d.Match(t) {
for i := range s.fiatCurrencies {
if s.fiatCurrencies[i].Match(c) ||
s.fiatCurrencies[i].Match(GetTranslation(c)) {
return true
}
}
@@ -555,9 +588,9 @@ func (s *Storage) IsCryptocurrency(c Code) bool {
return false
}
t, _ := GetTranslation(c)
for _, d := range s.cryptocurrencies {
if d.Match(c) || d.Match(t) {
for i := range s.cryptocurrencies {
if s.cryptocurrencies[i].Match(c) ||
s.cryptocurrencies[i].Match(GetTranslation(c)) {
return true
}
}
@@ -573,15 +606,12 @@ func (s *Storage) ValidateCode(newCode string) Code {
// ValidateFiatCode validates a fiat currency string and returns a currency
// code
func (s *Storage) ValidateFiatCode(newCode string) (Code, error) {
c, err := s.currencyCodes.RegisterFiat(newCode)
if err != nil {
return c, err
}
func (s *Storage) ValidateFiatCode(newCode string) Code {
c := s.currencyCodes.RegisterFiat(newCode)
if !s.fiatCurrencies.Contains(c) {
s.fiatCurrencies = append(s.fiatCurrencies, c)
}
return c, nil
return c
}
// ValidateCryptoCode validates a cryptocurrency string and returns a currency
@@ -637,9 +667,9 @@ func (s *Storage) GetBaseCurrency() Code {
// UpdateEnabledCryptoCurrencies appends new cryptocurrencies to the enabled
// currency list
func (s *Storage) UpdateEnabledCryptoCurrencies(c Currencies) {
for _, i := range c {
if !s.cryptocurrencies.Contains(i) {
s.cryptocurrencies = append(s.cryptocurrencies, i)
for i := range c {
if !s.cryptocurrencies.Contains(c[i]) {
s.cryptocurrencies = append(s.cryptocurrencies, c[i])
}
}
}
@@ -647,9 +677,10 @@ func (s *Storage) UpdateEnabledCryptoCurrencies(c Currencies) {
// UpdateEnabledFiatCurrencies appends new fiat currencies to the enabled
// currency list
func (s *Storage) UpdateEnabledFiatCurrencies(c Currencies) {
for _, i := range c {
if !s.fiatCurrencies.Contains(i) && !s.cryptocurrencies.Contains(i) {
s.fiatCurrencies = append(s.fiatCurrencies, i)
for i := range c {
if !s.fiatCurrencies.Contains(c[i]) &&
!s.cryptocurrencies.Contains(c[i]) {
s.fiatCurrencies = append(s.fiatCurrencies, c[i])
}
}
}
@@ -709,3 +740,10 @@ func (s *Storage) NewConversion(from, to Code) (Conversion, error) {
func (s *Storage) IsVerbose() bool {
return s.Verbose
}
// Shutdown shuts down the currency storage system and saves to currency.json
func (s *Storage) Shutdown() error {
close(s.shutdown)
s.wg.Wait()
return s.WriteCurrencyDataToFile(s.path, true)
}

View File

@@ -6,7 +6,7 @@ func TestRunUpdater(t *testing.T) {
var newStorage Storage
emptyMainConfig := MainConfiguration{}
err := newStorage.RunUpdater(BotOverrides{}, &emptyMainConfig, "", false)
err := newStorage.RunUpdater(BotOverrides{}, &emptyMainConfig, "")
if err == nil {
t.Fatal("storage RunUpdater() error cannot be nil")
}
@@ -16,12 +16,12 @@ func TestRunUpdater(t *testing.T) {
FiatDisplayCurrency: USD,
}
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "", false)
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "")
if err == nil {
t.Fatal("storage RunUpdater() error cannot be nil")
}
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "/bla", false)
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "/bla")
if err != nil {
t.Fatal("storage RunUpdater() error", err)
}

View File

@@ -56,7 +56,7 @@ type Storage struct {
foreignExchangeUpdateDelay time.Duration
mtx sync.Mutex
wg sync.WaitGroup
shutdownC chan struct{}
shutdown chan struct{}
updaterRunning bool
Verbose bool
}

View File

@@ -2,12 +2,12 @@ package currency
// GetTranslation returns similar strings for a particular currency if not found
// returns the code back
func GetTranslation(currency Code) (Code, bool) {
func GetTranslation(currency Code) Code {
val, ok := translations[currency]
if !ok {
return currency, ok
return currency
}
return val, ok
return val
}
var translations = map[Code]Code{

View File

@@ -5,29 +5,21 @@ import "testing"
func TestGetTranslation(t *testing.T) {
currencyPair := NewPair(BTC, USD)
expected := XBT
actual, ok := GetTranslation(currencyPair.Base)
if !ok {
t.Error("GetTranslation: failed to retrieve translation for BTC")
}
actual := GetTranslation(currencyPair.Base)
if expected != actual {
t.Error("GetTranslation: translation result was different to expected result")
}
currencyPair.Base = NEO
_, ok = GetTranslation(currencyPair.Base)
if ok {
actual = GetTranslation(currencyPair.Base)
if actual != currencyPair.Base {
t.Error("GetTranslation: no error on non translatable currency")
}
expected = BTC
currencyPair.Base = XBT
actual, ok = GetTranslation(currencyPair.Base)
if !ok {
t.Error("GetTranslation: failed to retrieve translation for BTC")
}
actual = GetTranslation(currencyPair.Base)
if expected != actual {
t.Error("GetTranslation: translation result was different to expected result")
}

View File

@@ -49,7 +49,7 @@ Find out which asset types are supported by the exchange and add them to the pai
config.GetDefaultFilePath()
```
```go
```js
{
"name": "FTX",
"enabled": true,
@@ -61,37 +61,35 @@ config.GetDefaultFilePath()
"websocketOrderbookBufferLimit": 5,
"baseCurrencies": "USD",
"currencyPairs": {
"assetTypes": [
"spot",
"futures"
],
"pairs": {
"futures": {
"enabled": "BTC-PERP",
"available": "BTC-PERP",
"requestFormat": {
"uppercase": true,
"delimiter": "-"
},
"configFormat": {
"uppercase": true,
"delimiter": "-"
}
"pairs": {
"futures": {
"assetEnabled": true,
"enabled": "BTC-PERP",
"available": "BTC-PERP",
"requestFormat": {
"uppercase": true,
"delimiter": "-"
},
"spot": {
"enabled": "BTC/USD",
"available": "BTC/USD",
"requestFormat": {
"uppercase": true,
"delimiter": "/"
},
"configFormat": {
"uppercase": true,
"delimiter": "/"
}
"configFormat": {
"uppercase": true,
"delimiter": "-"
}
},
"spot": {
"assetEnabled": true,
"enabled": "BTC/USD",
"available": "BTC/USD",
"requestFormat": {
"uppercase": true,
"delimiter": "/"
},
"configFormat": {
"uppercase": true,
"delimiter": "/"
}
}
},
}
},
"api": {
"authenticatedSupport": false,
"authenticatedWebsocketApiSupport": false,
@@ -177,6 +175,16 @@ Similar to the configs, spot support is inbuilt but other asset types will need
Delimiter: "-",
},
}
err := f.StoreAssetPairFormat(asset.Spot, spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = f.StoreAssetPairFormat(asset.Futures, futures)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
```
### Document the addition of the new exchange (FTX exchange is used as an example below):
@@ -645,16 +653,6 @@ The currency package contains many helper functions to format and process curren
### Websocket addition if exchange supports it:
#### Add websocket to exchange struct in ftx.go
```go
// FTX is the overarching type across this package
type FTX struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection // Add this line
}
```
#### Websocket Setup:
- Set the websocket url in ftx_websocket.go that is provided in the documentation:
@@ -672,27 +670,35 @@ func (f *FTX) WsConnect() error {
return errors.New(wshandler.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := f.WebsocketConn.Dial(&dialer, http.Header{})
err := f.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
f.WebsocketConn.SetupPingHandler(wshandler.WebsocketPingHandler{
// Can set up custom ping handler per websocket connection.
f.Websocket.Conn.SetupPingHandler(wshandler.WebsocketPingHandler{
MessageType: websocket.PingMessage,
Delay: ftxWebsocketTimer,
})
if f.Verbose {
log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", f.Name)
}
// This reader routine is called prior to initiating a subscription for
// efficient processing.
go f.wsReadData()
if f.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err := f.WsAuth()
err = f.WsAuth()
if err != nil {
f.Websocket.DataHandler <- err
f.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
f.GenerateDefaultSubscriptions()
return nil
// Generates the default subscription set, based off enabled pairs.
subs, err := f.GenerateDefaultSubscriptions()
if err != nil {
return err
}
// Finally subscribes to each individual channel.
return f.Websocket.SubscribeToChannels(subs)
}
```
@@ -700,22 +706,44 @@ func (f *FTX) WsConnect() error {
```go
// GenerateDefaultSubscriptions generates default subscription
func (f *FTX) GenerateDefaultSubscriptions() {
var channels = []string{wsTicker, wsTrades, wsOrderbook, wsMarkets, wsFills, wsOrders}
var subscriptions []wshandler.WebsocketChannelSubscription
for a := range f.CurrencyPairs.AssetTypes {
pairs := f.GetEnabledPairs(f.CurrencyPairs.AssetTypes[a])
func (f *FTX) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: wsMarkets,
})
// Ranges over available channels, pairs and asset types to produce a full
// subscription list.
var channels = []string{wsTicker, wsTrades, wsOrderbook}
assets := f.GetAssetTypes()
for a := range assets {
pairs, err := f.GetEnabledPairs(assets[a])
if err != nil {
return nil, err
}
for z := range pairs {
newPair := currency.NewPairWithDelimiter(pairs[z].Base.String(), pairs[z].Quote.String(), "-")
newPair := currency.NewPairWithDelimiter(pairs[z].Base.String(),
pairs[z].Quote.String(),
"-")
for x := range channels {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[x],
Currency: newPair,
})
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channels[x],
Currency: newPair,
Asset: assets[a],
})
}
}
}
f.Websocket.SubscribeToChannels(subscriptions)
// Appends authenticated channels to the subscription list
if f.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
var authchan = []string{wsOrders, wsFills}
for x := range authchan {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: authchan[x],
})
}
}
return subscriptions, nil
}
```
@@ -753,22 +781,47 @@ type WsSub struct {
```go
// Subscribe sends a websocket message to receive data from the channel
func (f *FTX) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var sub WsSub
a, err := f.GetPairAssetType(channelToSubscribe.Currency)
if err != nil {
return err
func (f *FTX) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
// For subscriptions we try to batch as much as possible to limit the amount
// of connection usage but sometimes this is not supported on the exchange
// API.
var errs common.Errors // This is an array of errors useful in the event that one channel subscription errors but we can subscribe to the next iteration.
channels:
for i := range channelsToSubscribe {
// Type we declared above to send via our websocket connection.
var sub WsSub
sub.Channel = channelsToSubscribe[i].Channel
sub.Operation = subscribe
switch channelsToSubscribe[i].Channel {
case wsFills, wsOrders, wsMarkets:
// Authenticated wsFills && wsOrders or wsMarkets which is a channel subscription for the full set of tradable markets do not need a currency pair association.
default:
a, err := f.GetPairAssetType(channelsToSubscribe[i].Currency)
if err != nil {
errs = append(errs, err)
continue channels
}
// Ensures our outbound currency pair is formatted correctly, sometimes our configuration format is different from what our request format needs to be.
formattedPair, err := f.FormatExchangeCurrency(channelsToSubscribe[i].Currency, a)
if err != nil {
errs = append(errs, err)
continue channels
}
sub.Market = formattedPair.String()
}
err := f.Websocket.Conn.SendJSONMessage(sub)
if err != nil {
errs = append(errs, err)
continue
}
// When we have a successful subscription, we can alert our internal management system of the success.
f.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
}
switch channelToSubscribe.Channel {
case wsFills, wsOrders:
sub.Operation = "subscribe"
sub.Channel = channelToSubscribe.Channel
default:
sub.Operation = "subscribe"
sub.Channel = channelToSubscribe.Channel
sub.Market = f.FormatExchangeCurrency(channelToSubscribe.Currency, a).String()
if errs != nil {
return errs
}
return f.WebsocketConn.SendJSONMessage(sub)
return nil
}
```
@@ -782,7 +835,7 @@ Run gocryptotrader with the following settings enabled in config
},
"enabled": {
"autoPairUpdates": true,
"websocketAPI": true
"websocketAPI": true // <- Change this to true if it is false
```
#### Handle websocket data:
@@ -800,13 +853,12 @@ func (f *FTX) wsReadData() {
case <-f.Websocket.ShutdownC:
return
default:
resp, err := f.WebsocketConn.ReadMessage()
if err != nil {
f.Websocket.ReadMessageErrors <- err
resp := f.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
f.Websocket.TrafficAlert <- struct{}{}
err = f.wsHandleData(resp.Raw)
err := f.wsHandleData(resp.Raw)
if err != nil {
f.Websocket.DataHandler <- err
}
@@ -963,7 +1015,7 @@ func (f *FTX) WsAuth() error {
Time: intNonce,
},
}
return f.WebsocketConn.SendJSONMessage(req)
return f.Websocket.Conn.SendJSONMessage(req)
}
```
@@ -971,16 +1023,42 @@ func (f *FTX) WsAuth() error {
```go
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (f *FTX) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var unSub WsSub
a, err := f.GetPairAssetType(channelToSubscribe.Currency)
if err != nil {
return err
func (f *FTX) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
// As with subscribing we want to batch as much as possible, but sometimes this cannot be achieved due to API shortfalls.
var errs common.Errors
channels:
for i := range channelsToUnsubscribe {
var unSub WsSub
unSub.Operation = unsubscribe
unSub.Channel = channelsToUnsubscribe[i].Channel
switch channelsToUnsubscribe[i].Channel {
case wsFills, wsOrders, wsMarkets:
default:
a, err := f.GetPairAssetType(channelsToUnsubscribe[i].Currency)
if err != nil {
errs = append(errs, err)
continue channels
}
formattedPair, err := f.FormatExchangeCurrency(channelsToUnsubscribe[i].Currency, a)
if err != nil {
errs = append(errs, err)
continue channels
}
unSub.Market = formattedPair.String()
}
err := f.Websocket.Conn.SendJSONMessage(unSub)
if err != nil {
errs = append(errs, err)
continue
}
// When we have a successful unsubscription, we can alert our internal management system of the success.
f.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
}
unSub.Operation = "unsubscribe"
unSub.Channel = channelToSubscribe.Channel
unSub.Market = f.FormatExchangeCurrency(channelToSubscribe.Currency, a).String()
return f.WebsocketConn.SendJSONMessage(unSub)
if errs != nil {
return errs
}
return nil
}
```
@@ -989,24 +1067,53 @@ func (f *FTX) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscript
Add websocket functionality if supported to Setup:
```go
err = f.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: ftxWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: f.WsConnect,
Subscriber: f.Subscribe,
UnSubscriber: f.Unsubscribe,
Features: &f.Features.Supports.WebsocketCapabilities,
})
// Setup takes in the supplied exchange configuration details and sets params
func (f *FTX) Setup(exch *config.ExchangeConfig) error {
if !exch.Enabled {
f.SetEnabled(false)
return nil
}
err := f.SetupDefaults(exch)
if err != nil {
return err
}
```
}
// Websocket details setup below
err = f.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: ftxWSURL, // Default ws endpoint so we can roll back via CLI if needed.
ExchangeName: exch.Name, // Sets websocket name to the exchange name.
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: f.WsConnect, // Connector function outlined above.
Subscriber: f.Subscribe, // Subscriber function outlined above.
UnSubscriber: f.Unsubscribe, // Unsubscriber function outlined above.
GenerateSubscriptions: f.GenerateDefaultSubscriptions, // GenerateDefaultSubscriptions function outlined above.
Features: &f.Features.Supports.WebsocketCapabilities, // Defines the capabilities of the websocket outlined in supported features struct. This allows the websocket connection to be flushed appropriately if we have a pair/asset enable/disable change. This is outlined below.
// Orderbook buffer specific variables for processing orderbook updates via websocket feed.
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
// Other orderbook buffer vars:
// BufferEnabled bool
// SortBuffer bool
// SortBufferByUpdateIDs bool
// UpdateEntriesByID bool
})
if err != nil {
return err
}
// Sets up a new connection for the websocket, there are two separate connections denoted by the ConnectionSetup struct auth bool.
return f.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
// RateLimit int64 rudimentary rate limit that sleeps connection in milliseconds before sending designated payload
// Authenticated bool sets if the connection is dedicated for an authenticated websocket stream which can be accessed from the Websocket field variable AuthConn e.g. f.Websocket.AuthConn
})
}
```
Below are the features supported by FTX API protocol:
@@ -1053,32 +1160,35 @@ Below are the features supported by FTX API protocol:
Initially the functions return nil or common.ErrNotYetImplemented
```go
// GetWebsocket returns a pointer to the exchange websocket
func (f *FTX) GetWebsocket() (*wshandler.Websocket, error) {
return f.Websocket, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (f *FTX) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
f.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (f *FTX) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
f.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (f *FTX) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return f.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (f *FTX) AuthenticateWebsocket() error {
return f.WsAuth()
}
```
```
## Last but not least - Live testing
### Live testing websocket via [gctcli](../cmd/gctcli/main.go)
Please test all `websocket` commands below whilst a GoCryptoTrader instance is running and with the exchange websocket setting enabled:
- `getinfo` to ensure fetching websocket information is possible (that the websocket connection is enabled, connected and is running).
- `disable/enable` to ensure disabling/enabling a websocket connection disconnects/connects accordingly.
- `getsubs` to ensure the subscriptions are in sync with the exchange's config settings or by manual subscriptions added/removed via `gctcli`.
- `setproxy` to ensure that a proxy can be set and resets the websocket connection accordingly.
- `seturl` to ensure that a new websocket URL can be set in the event of an API endpoint change whilst an instance of GoCryptoTrader is already running.
Please test all `pair` commands to disable and enable different assets types to witness subscriptions and unsubscriptions:
- `get` to ensure correct enabled and disabled pairs for a supported asset type.
- `disableasset` to ensure disabling of entire asset class and associated unsubscriptions.
- `enableasset` to ensure correct enabling of entire asset class and associated subscriptions.
- `disable` to ensure correct disabling of pair(s) and and associated unsubscriptions.
- `enable` to ensure correct enabling of pair(s) and associated subscriptions.
- `enableall` to ensure correct enabling of all pairs for an asset type and associated subscriptions.
- `disableall` to ensure correct disabling of all pairs for an asset type and associated unsubscriptions.
## Open a PR
Submitting a PR is easy and all are welcome additions to the public repository. Submit via github.com/thrasher-corp/gocryptotrader or contact our team via slack for more information.

View File

@@ -385,8 +385,7 @@ func (e *Engine) Start() error {
CurrencyDelay: e.Config.Currency.CurrencyFileUpdateDuration,
FxRateDelay: e.Config.Currency.ForeignExchangeUpdateDuration,
},
e.Settings.DataDir,
e.Settings.Verbose)
e.Settings.DataDir)
if err != nil {
gctlog.Errorf(gctlog.Global, "Currency updater system failed to start %v", err)
}
@@ -514,6 +513,10 @@ func (e *Engine) Stop() {
}
}
if err := currency.ShutdownStorageUpdater(); err != nil {
gctlog.Errorf(gctlog.Global, "Currency storage system. Error: %v", err)
}
if !e.Settings.EnableDryRun {
err := e.Config.SaveConfig(e.Settings.ConfigFile, false)
if err != nil {

View File

@@ -72,7 +72,7 @@ var Events []*Event
// Add adds an event to the Events chain and returns an index/eventID
// and an error
func Add(exchange, item string, condition EventConditionParams, currencyPair currency.Pair, asset asset.Item, action string) (int64, error) {
func Add(exchange, item string, condition EventConditionParams, p currency.Pair, a asset.Item, action string) (int64, error) {
err := IsValidEvent(exchange, item, condition, action)
if err != nil {
return 0, err
@@ -89,8 +89,8 @@ func Add(exchange, item string, condition EventConditionParams, currencyPair cur
evt.Exchange = exchange
evt.Item = item
evt.Condition = condition
evt.Pair = currencyPair
evt.Asset = asset
evt.Pair = p
evt.Asset = a
evt.Action = action
evt.Executed = false
Events = append(Events, evt)
@@ -99,8 +99,8 @@ func Add(exchange, item string, condition EventConditionParams, currencyPair cur
// Remove deletes and event by its ID
func Remove(eventID int64) bool {
for i, x := range Events {
if x.ID == eventID {
for i := range Events {
if Events[i].ID == eventID {
Events = append(Events[:i], Events[i+1:]...)
return true
}
@@ -112,9 +112,8 @@ func Remove(eventID int64) bool {
// events that have been executed.
func GetEventCounter() (total, executed int) {
total = len(Events)
for _, x := range Events {
if x.Executed {
for i := range Events {
if Events[i].Executed {
executed++
}
}
@@ -126,11 +125,10 @@ func (e *Event) ExecuteAction() bool {
if strings.Contains(e.Action, ",") {
action := strings.Split(e.Action, ",")
if action[0] == ActionSMSNotify {
message := fmt.Sprintf("Event triggered: %s\n", e.String())
if action[1] == "ALL" {
Bot.CommsManager.PushEvent(base.Event{
Type: "event",
Message: message,
Message: "Event triggered: " + e.String(),
})
}
}

View File

@@ -138,10 +138,11 @@ func TestProcessTicker(t *testing.T) {
// now populate it with a 0 entry
tick := ticker.Price{
Pair: currency.NewPair(currency.BTC, currency.USD),
Last: 0,
Pair: currency.NewPair(currency.BTC, currency.USD),
ExchangeName: e.Exchange,
AssetType: e.Asset,
}
if err := ticker.ProcessTicker(e.Exchange, &tick, e.Asset); err != nil {
if err := ticker.ProcessTicker(&tick); err != nil {
t.Fatal("unexpected result:", err)
}
if r := e.processTicker(); r {
@@ -150,7 +151,7 @@ func TestProcessTicker(t *testing.T) {
// now populate it with a number > 0
tick.Last = 1337
if err := ticker.ProcessTicker(e.Exchange, &tick, e.Asset); err != nil {
if err := ticker.ProcessTicker(&tick); err != nil {
t.Fatal("unexpected result:", err)
}
if r := e.processTicker(); !r {

View File

@@ -6,6 +6,7 @@ import (
"sync"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
@@ -237,7 +238,11 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error {
dryrunParamInteraction("enableallpairs")
assets := exchCfg.CurrencyPairs.GetAssetTypes()
for x := range assets {
pairs := exchCfg.CurrencyPairs.GetPairs(assets[x], false)
var pairs currency.Pairs
pairs, err = exchCfg.CurrencyPairs.GetPairs(assets[x], false)
if err != nil {
return err
}
exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true)
}
}

View File

@@ -12,8 +12,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -89,11 +89,11 @@ func (h *FakePassingExchange) FetchTradablePairs(_ asset.Item) ([]string, error)
}
func (h *FakePassingExchange) UpdateTradablePairs(_ bool) error { return nil }
func (h *FakePassingExchange) GetEnabledPairs(_ asset.Item) currency.Pairs {
return currency.Pairs{}
func (h *FakePassingExchange) GetEnabledPairs(_ asset.Item) (currency.Pairs, error) {
return currency.Pairs{}, nil
}
func (h *FakePassingExchange) GetAvailablePairs(_ asset.Item) currency.Pairs {
return currency.Pairs{}
func (h *FakePassingExchange) GetAvailablePairs(_ asset.Item) (currency.Pairs, error) {
return currency.Pairs{}, nil
}
func (h *FakePassingExchange) FetchAccountInfo() (account.Holdings, error) {
return account.Holdings{}, nil
@@ -142,6 +142,11 @@ func (h *FakePassingExchange) GetOrderHistory(_ *order.GetOrdersRequest) ([]orde
return nil, nil
}
func (h *FakePassingExchange) GetActiveOrders(_ *order.GetOrdersRequest) ([]order.Detail, error) {
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
return nil, err
}
return []order.Detail{
{
Price: 1337,
@@ -153,25 +158,25 @@ func (h *FakePassingExchange) GetActiveOrders(_ *order.GetOrdersRequest) ([]orde
Status: order.Active,
AssetType: asset.Spot,
Date: time.Now(),
Pair: currency.NewPairFromString("BTCUSD"),
Pair: pair,
},
}, nil
}
func (h *FakePassingExchange) SetHTTPClientUserAgent(_ string) {}
func (h *FakePassingExchange) GetHTTPClientUserAgent() string { return "" }
func (h *FakePassingExchange) SetClientProxyAddress(_ string) error { return nil }
func (h *FakePassingExchange) SupportsWebsocket() bool { return true }
func (h *FakePassingExchange) SupportsREST() bool { return true }
func (h *FakePassingExchange) IsWebsocketEnabled() bool { return true }
func (h *FakePassingExchange) GetWebsocket() (*wshandler.Websocket, error) { return nil, nil }
func (h *FakePassingExchange) SubscribeToWebsocketChannels(_ []wshandler.WebsocketChannelSubscription) error {
func (h *FakePassingExchange) SetHTTPClientUserAgent(_ string) {}
func (h *FakePassingExchange) GetHTTPClientUserAgent() string { return "" }
func (h *FakePassingExchange) SetClientProxyAddress(_ string) error { return nil }
func (h *FakePassingExchange) SupportsWebsocket() bool { return true }
func (h *FakePassingExchange) SupportsREST() bool { return true }
func (h *FakePassingExchange) IsWebsocketEnabled() bool { return true }
func (h *FakePassingExchange) GetWebsocket() (*stream.Websocket, error) { return nil, nil }
func (h *FakePassingExchange) SubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error {
return nil
}
func (h *FakePassingExchange) UnsubscribeToWebsocketChannels(_ []wshandler.WebsocketChannelSubscription) error {
func (h *FakePassingExchange) UnsubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error {
return nil
}
func (h *FakePassingExchange) AuthenticateWebsocket() error { return nil }
func (h *FakePassingExchange) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
func (h *FakePassingExchange) GetSubscriptions() ([]stream.ChannelSubscription, error) {
return nil, nil
}
func (h *FakePassingExchange) GetDefaultConfig() (*config.ExchangeConfig, error) { return nil, nil }

View File

@@ -292,8 +292,7 @@ func MapCurrenciesByExchange(p currency.Pairs, enabledExchangesOnly bool, assetT
continue
}
exchName := Bot.Config.Exchanges[y].Name
success, err := Bot.Config.SupportsPair(exchName, p[x], assetType)
if err != nil || !success {
if !Bot.Config.SupportsPair(exchName, p[x], assetType) {
continue
}
@@ -324,14 +323,10 @@ func GetExchangeNamesByCurrency(p currency.Pair, enabled bool, assetType asset.I
}
exchName := Bot.Config.Exchanges[x].Name
success, err := Bot.Config.SupportsPair(exchName, p, assetType)
if err != nil {
if !Bot.Config.SupportsPair(exchName, p, assetType) {
continue
}
if success {
exchanges = append(exchanges, exchName)
}
exchanges = append(exchanges, exchName)
}
return exchanges
}
@@ -404,19 +399,18 @@ func GetRelatableCurrencies(p currency.Pair, incOrig, incUSDT bool) currency.Pai
addPair(p)
}
first, ok := currency.GetTranslation(p.Base)
if ok {
first := currency.GetTranslation(p.Base)
if first != p.Base {
addPair(currency.NewPair(first, p.Quote))
var second currency.Code
second, ok = currency.GetTranslation(p.Quote)
if ok {
second := currency.GetTranslation(p.Quote)
if second != p.Quote {
addPair(currency.NewPair(first, second))
}
}
second, ok := currency.GetTranslation(p.Quote)
if ok {
second := currency.GetTranslation(p.Quote)
if second != p.Quote {
addPair(currency.NewPair(p.Base, second))
}
}
@@ -483,8 +477,8 @@ func GetCollatedExchangeAccountInfoByCoin(accounts []account.Holdings) map[curre
// GetExchangeHighestPriceByCurrencyPair returns the exchange with the highest
// price for a given currency pair and asset type
func GetExchangeHighestPriceByCurrencyPair(p currency.Pair, assetType asset.Item) (string, error) {
result := stats.SortExchangesByPrice(p, assetType, true)
func GetExchangeHighestPriceByCurrencyPair(p currency.Pair, a asset.Item) (string, error) {
result := stats.SortExchangesByPrice(p, a, true)
if len(result) == 0 {
return "", fmt.Errorf("no stats for supplied currency pair and asset type")
}
@@ -717,7 +711,14 @@ func GetAllActiveTickers() []EnabledExchangeCurrencies {
exchangeTicker.ExchangeName = exchName
for y := range assets {
currencies := exchanges[x].GetEnabledPairs(assets[y])
currencies, err := exchanges[x].GetEnabledPairs(assets[y])
if err != nil {
log.Errorf(log.ExchangeSys,
"Exchange %s could not retrieve enabled currencies. Err: %s\n",
exchName,
err)
continue
}
for z := range currencies {
tp, err := exchanges[x].FetchTicker(currencies[z], assets[y])
if err != nil {
@@ -739,19 +740,25 @@ func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts {
var response AllEnabledExchangeAccounts
exchanges := GetExchanges()
for x := range exchanges {
if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if Bot.Settings.Verbose {
log.Debugf(log.ExchangeSys, "GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.\n", exchanges[x].GetName())
if exchanges[x] != nil && exchanges[x].IsEnabled() {
if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if Bot.Settings.Verbose {
log.Debugf(log.ExchangeSys,
"GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.\n",
exchanges[x].GetName())
}
continue
}
continue
accountInfo, err := exchanges[x].FetchAccountInfo()
if err != nil {
log.Errorf(log.ExchangeSys,
"Error encountered retrieving exchange account info for %s. Error %s\n",
exchanges[x].GetName(),
err)
continue
}
response.Data = append(response.Data, accountInfo)
}
accountInfo, err := exchanges[x].FetchAccountInfo()
if err != nil {
log.Errorf(log.ExchangeSys, "Error encountered retrieving exchange account info for %s. Error %s\n",
exchanges[x].GetName(), err)
continue
}
response.Data = append(response.Data, accountInfo)
}
return response
}

View File

@@ -171,136 +171,225 @@ func TestGetSpecificAvailablePairs(t *testing.T) {
assetType := asset.Spot
result := GetSpecificAvailablePairs(true, true, true, false, assetType)
if !result.Contains(currency.NewPairFromStrings("BTC", "USD"), true) {
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
if !result.Contains(btsusd, true) {
t.Fatal("Unexpected result")
}
if !result.Contains(currency.NewPairFromStrings("BTC", "USDT"), false) {
btcusdt, err := currency.NewPairFromStrings("BTC", "USDT")
if err != nil {
t.Fatal(err)
}
if !result.Contains(btcusdt, false) {
t.Fatal("Unexpected result")
}
result = GetSpecificAvailablePairs(true, true, false, false, assetType)
if result.Contains(currency.NewPairFromStrings("BTC", "USDT"), false) {
if result.Contains(btcusdt, false) {
t.Fatal("Unexpected result")
}
ltcbtc, err := currency.NewPairFromStrings("LTC", "BTC")
if err != nil {
t.Fatal(err)
}
result = GetSpecificAvailablePairs(true, false, false, true, assetType)
if !result.Contains(currency.NewPairFromStrings("LTC", "BTC"), false) {
if !result.Contains(ltcbtc, false) {
t.Fatal("Unexpected result")
}
}
func TestIsRelatablePairs(t *testing.T) {
SetupTestHelpers(t)
xbtusd, err := currency.NewPairFromStrings("XBT", "USD")
if err != nil {
t.Fatal(err)
}
btcusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with similar names
result := IsRelatablePairs(currency.NewPairFromStrings("XBT", "USD"),
currency.NewPairFromStrings("BTC", "USD"), false)
result := IsRelatablePairs(xbtusd, btcusd, false)
if !result {
t.Fatal("Unexpected result")
}
// Test relational pairs with similar names reversed
result = IsRelatablePairs(currency.NewPairFromStrings("BTC", "USD"),
currency.NewPairFromStrings("XBT", "USD"), false)
result = IsRelatablePairs(btcusd, xbtusd, false)
if !result {
t.Fatal("Unexpected result")
}
btcusdt, err := currency.NewPairFromStrings("BTC", "USDT")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with similar names but with Tether support disabled
result = IsRelatablePairs(currency.NewPairFromStrings("XBT", "USD"),
currency.NewPairFromStrings("BTC", "USDT"), false)
result = IsRelatablePairs(xbtusd, btcusdt, false)
if result {
t.Fatal("Unexpected result")
}
xbtusdt, err := currency.NewPairFromStrings("XBT", "USDT")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with similar names but with Tether support enabled
result = IsRelatablePairs(currency.NewPairFromStrings("XBT", "USDT"),
currency.NewPairFromStrings("BTC", "USD"), true)
result = IsRelatablePairs(xbtusdt, btcusd, true)
if !result {
t.Fatal("Unexpected result")
}
aeusdt, err := currency.NewPairFromStrings("AE", "USDT")
if err != nil {
t.Fatal(err)
}
usdtae, err := currency.NewPairDelimiter("USDT-AE", "-")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with different ordering, a delimiter and with
// Tether support enabled
result = IsRelatablePairs(currency.NewPairFromStrings("AE", "USDT"),
currency.NewPairDelimiter("USDT-AE", "-"), true)
result = IsRelatablePairs(aeusdt, usdtae, true)
if !result {
t.Fatal("Unexpected result")
}
// Test relational pairs with different ordering, a delimiter and with
// Tether support disabled
result = IsRelatablePairs(currency.NewPairFromStrings("AE", "USDT"),
currency.NewPairDelimiter("USDT-AE", "-"), false)
result = IsRelatablePairs(aeusdt, usdtae, false)
if !result {
t.Fatal("Unexpected result")
}
xbteur, err := currency.NewPairFromStrings("XBT", "EUR")
if err != nil {
t.Fatal(err)
}
btcaud, err := currency.NewPairFromStrings("BTC", "AUD")
if err != nil {
t.Fatal(err)
}
// Test relationl pairs with similar names and different fiat currencies
result = IsRelatablePairs(currency.NewPairFromStrings("XBT", "EUR"),
currency.NewPairFromStrings("BTC", "AUD"), false)
result = IsRelatablePairs(xbteur, btcaud, false)
if !result {
t.Fatal("Unexpected result")
}
usdbtc, err := currency.NewPairFromStrings("USD", "BTC")
if err != nil {
t.Fatal(err)
}
btceur, err := currency.NewPairFromStrings("BTC", "EUR")
if err != nil {
t.Fatal(err)
}
// Test relationl pairs with similar names, different fiat currencies and
// with different ordering
result = IsRelatablePairs(currency.NewPairFromStrings("USD", "BTC"),
currency.NewPairFromStrings("BTC", "EUR"), false)
result = IsRelatablePairs(usdbtc, btceur, false)
if !result { // Is this really expected result???
t.Fatal("Unexpected result")
}
// Test relationl pairs with similar names, different fiat currencies and
// with Tether enabled
result = IsRelatablePairs(currency.NewPairFromStrings("USD", "BTC"),
currency.NewPairFromStrings("BTC", "USDT"), true)
result = IsRelatablePairs(usdbtc, btcusdt, true)
if !result {
t.Fatal("Unexpected result")
}
ltcbtc, err := currency.NewPairFromStrings("LTC", "BTC")
if err != nil {
t.Fatal(err)
}
btcltc, err := currency.NewPairFromStrings("BTC", "LTC")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with similar names
result = IsRelatablePairs(currency.NewPairFromStrings("LTC", "BTC"),
currency.NewPairFromStrings("BTC", "LTC"), false)
result = IsRelatablePairs(ltcbtc, btcltc, false)
if !result {
t.Fatal("Unexpected result")
}
ltceth, err := currency.NewPairFromStrings("LTC", "ETH")
if err != nil {
t.Fatal(err)
}
btceth, err := currency.NewPairFromStrings("BTC", "ETH")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with similar different pairs
result = IsRelatablePairs(currency.NewPairFromStrings("LTC", "ETH"),
currency.NewPairFromStrings("BTC", "ETH"), false)
result = IsRelatablePairs(ltceth, btceth, false)
if result {
t.Fatal("Unexpected result")
}
// Test relationl crypto pairs with similar different pairs and with USDT
// enabled
result = IsRelatablePairs(currency.NewPairFromStrings("USDT", "USD"),
currency.NewPairFromStrings("BTC", "USD"), true)
usdtusd, err := currency.NewPairFromStrings("USDT", "USD")
if err != nil {
t.Fatal(err)
}
result = IsRelatablePairs(usdtusd, btcusd, true)
if result {
t.Fatal("Unexpected result")
}
xbtltc, err := currency.NewPairFromStrings("XBT", "LTC")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with with similar names
result = IsRelatablePairs(currency.NewPairFromStrings("XBT", "LTC"),
currency.NewPairFromStrings("BTC", "LTC"), false)
result = IsRelatablePairs(xbtltc, btcltc, false)
if !result {
t.Fatal("Unexpected result")
}
ltcxbt, err := currency.NewPairFromStrings("LTC", "XBT")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with different ordering and similar names
result = IsRelatablePairs(currency.NewPairFromStrings("LTC", "XBT"),
currency.NewPairFromStrings("BTC", "LTC"), false)
result = IsRelatablePairs(ltcxbt, btcltc, false)
if !result {
t.Fatal("Unexpected result")
}
// Test edge case between two pairs when currency translations were causing
// non-relational pairs to be relatable
result = IsRelatablePairs(currency.NewPairFromStrings("EUR", "USD"),
currency.NewPairFromStrings("BTC", "USD"), false)
eurusd, err := currency.NewPairFromStrings("EUR", "USD")
if err != nil {
t.Fatal(err)
}
result = IsRelatablePairs(eurusd, btcusd, false)
if result {
t.Fatal("Unexpected result")
}
@@ -308,44 +397,80 @@ func TestIsRelatablePairs(t *testing.T) {
func TestGetRelatableCryptocurrencies(t *testing.T) {
SetupTestHelpers(t)
p := GetRelatableCryptocurrencies(currency.NewPairFromStrings("BTC", "LTC"))
if p.Contains(currency.NewPairFromStrings("BTC", "LTC"), true) {
btcltc, err := currency.NewPairFromStrings("BTC", "LTC")
if err != nil {
t.Fatal(err)
}
btcbtc, err := currency.NewPairFromStrings("BTC", "BTC")
if err != nil {
t.Fatal(err)
}
ltcltc, err := currency.NewPairFromStrings("LTC", "LTC")
if err != nil {
t.Fatal(err)
}
btceth, err := currency.NewPairFromStrings("BTC", "ETH")
if err != nil {
t.Fatal(err)
}
p := GetRelatableCryptocurrencies(btcltc)
if p.Contains(btcltc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(currency.NewPairFromStrings("BTC", "BTC"), true) {
if p.Contains(btcbtc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(currency.NewPairFromStrings("LTC", "LTC"), true) {
if p.Contains(ltcltc, true) {
t.Fatal("Unexpected result")
}
if !p.Contains(currency.NewPairFromStrings("BTC", "ETH"), true) {
if !p.Contains(btceth, true) {
t.Fatal("Unexpected result")
}
p = GetRelatableCryptocurrencies(currency.NewPairFromStrings("BTC", "LTC"))
if p.Contains(currency.NewPairFromStrings("BTC", "LTC"), true) {
p = GetRelatableCryptocurrencies(btcltc)
if p.Contains(btcltc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(currency.NewPairFromStrings("BTC", "BTC"), true) {
if p.Contains(btcbtc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(currency.NewPairFromStrings("LTC", "LTC"), true) {
if p.Contains(ltcltc, true) {
t.Fatal("Unexpected result")
}
if !p.Contains(currency.NewPairFromStrings("BTC", "ETH"), true) {
if !p.Contains(btceth, true) {
t.Fatal("Unexpected result")
}
}
func TestGetRelatableFiatCurrencies(t *testing.T) {
SetupTestHelpers(t)
p := GetRelatableFiatCurrencies(currency.NewPairFromStrings("BTC", "USD"))
if !p.Contains(currency.NewPairFromStrings("BTC", "EUR"), true) {
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
btceur, err := currency.NewPairFromStrings("BTC", "EUR")
if err != nil {
t.Fatal(err)
}
p := GetRelatableFiatCurrencies(btsusd)
if !p.Contains(btceur, true) {
t.Fatal("Unexpected result")
}
p = GetRelatableFiatCurrencies(currency.NewPairFromStrings("BTC", "USD"))
if !p.Contains(currency.NewPairFromStrings("BTC", "ZAR"), true) {
btczar, err := currency.NewPairFromStrings("BTC", "ZAR")
if err != nil {
t.Fatal(err)
}
p = GetRelatableFiatCurrencies(btsusd)
if !p.Contains(btczar, true) {
t.Fatal("Unexpected result")
}
}
@@ -373,21 +498,36 @@ func TestGetExchangeNamesByCurrency(t *testing.T) {
SetupTestHelpers(t)
assetType := asset.Spot
result := GetExchangeNamesByCurrency(currency.NewPairFromStrings("BTC", "USD"),
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
btcjpy, err := currency.NewPairFromStrings("BTC", "JPY")
if err != nil {
t.Fatal(err)
}
blahjpy, err := currency.NewPairFromStrings("blah", "JPY")
if err != nil {
t.Fatal(err)
}
result := GetExchangeNamesByCurrency(btsusd,
true,
assetType)
if !common.StringDataCompare(result, "Bitstamp") {
t.Fatal("Unexpected result")
}
result = GetExchangeNamesByCurrency(currency.NewPairFromStrings("BTC", "JPY"),
result = GetExchangeNamesByCurrency(btcjpy,
true,
assetType)
if !common.StringDataCompare(result, "Bitflyer") {
t.Fatal("Unexpected result")
}
result = GetExchangeNamesByCurrency(currency.NewPairFromStrings("blah", "JPY"),
result = GetExchangeNamesByCurrency(blahjpy,
true,
assetType)
if len(result) > 0 {
@@ -415,9 +555,12 @@ func TestGetSpecificOrderbook(t *testing.T) {
t.Fatal("Unexpected result", err)
}
ob, err := GetSpecificOrderbook(currency.NewPairFromString("BTCUSD"),
"Bitstamp",
asset.Spot)
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
ob, err := GetSpecificOrderbook(btsusd, "Bitstamp", asset.Spot)
if err != nil {
t.Fatal(err)
}
@@ -426,9 +569,12 @@ func TestGetSpecificOrderbook(t *testing.T) {
t.Fatal("Unexpected result")
}
_, err = GetSpecificOrderbook(currency.NewPairFromStrings("ETH", "LTC"),
"Bitstamp",
asset.Spot)
ethltc, err := currency.NewPairFromStrings("ETH", "LTC")
if err != nil {
t.Fatal(err)
}
_, err = GetSpecificOrderbook(ethltc, "Bitstamp", asset.Spot)
if err == nil {
t.Fatal("Unexpected result")
}
@@ -440,16 +586,21 @@ func TestGetSpecificTicker(t *testing.T) {
SetupTestHelpers(t)
LoadExchange("Bitstamp", false, nil)
p := currency.NewPairFromStrings("BTC", "USD")
err := ticker.ProcessTicker("Bitstamp",
&ticker.Price{Pair: p, Last: 1000},
asset.Spot)
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
err = ticker.ProcessTicker(&ticker.Price{
Pair: p,
Last: 1000,
AssetType: asset.Spot,
ExchangeName: "Bitstamp"})
if err != nil {
t.Fatal("ProcessTicker error", err)
}
tick, err := GetSpecificTicker(currency.NewPairFromStrings("BTC", "USD"), "Bitstamp",
asset.Spot)
tick, err := GetSpecificTicker(p, "Bitstamp", asset.Spot)
if err != nil {
t.Fatal(err)
}
@@ -458,8 +609,12 @@ func TestGetSpecificTicker(t *testing.T) {
t.Fatal("Unexpected result")
}
_, err = GetSpecificTicker(currency.NewPairFromStrings("ETH", "LTC"), "Bitstamp",
asset.Spot)
ethltc, err := currency.NewPairFromStrings("ETH", "LTC")
if err != nil {
t.Fatal(err)
}
_, err = GetSpecificTicker(ethltc, "Bitstamp", asset.Spot)
if err == nil {
t.Fatal("Unexpected result")
}
@@ -530,7 +685,11 @@ func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) {
func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) {
SetupTestHelpers(t)
p := currency.NewPairFromStrings("BTC", "USD")
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
stats.Add("Bitstamp", p, asset.Spot, 1337, 10000)
exchangeName, err := GetExchangeHighestPriceByCurrencyPair(p, asset.Spot)
@@ -542,8 +701,12 @@ func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) {
t.Error("Unexpected result")
}
_, err = GetExchangeHighestPriceByCurrencyPair(currency.NewPairFromStrings("BTC", "AUD"),
asset.Spot)
btcaud, err := currency.NewPairFromStrings("BTC", "AUD")
if err != nil {
t.Fatal(err)
}
_, err = GetExchangeHighestPriceByCurrencyPair(btcaud, asset.Spot)
if err == nil {
t.Error("Unexpected result")
}
@@ -552,7 +715,11 @@ func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) {
func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) {
SetupTestHelpers(t)
p := currency.NewPairFromStrings("BTC", "USD")
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
stats.Add("Bitstamp", p, asset.Spot, 1337, 10000)
exchangeName, err := GetExchangeLowestPriceByCurrencyPair(p, asset.Spot)
@@ -564,8 +731,12 @@ func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) {
t.Error("Unexpected result")
}
_, err = GetExchangeLowestPriceByCurrencyPair(currency.NewPairFromStrings("BTC", "AUD"),
asset.Spot)
btcaud, err := currency.NewPairFromStrings("BTC", "AUD")
if err != nil {
t.Fatal(err)
}
_, err = GetExchangeLowestPriceByCurrencyPair(btcaud, asset.Spot)
if err == nil {
t.Error("Unexpected reuslt")
}

View File

@@ -389,18 +389,45 @@ func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, err
func (o *orderManager) processOrders() {
authExchanges := GetAuthAPISupportedExchanges()
for x := range authExchanges {
log.Debugf(log.OrderMgr, "Order manager: Processing orders for exchange %v.", authExchanges[x])
log.Debugf(log.OrderMgr,
"Order manager: Processing orders for exchange %v.",
authExchanges[x])
exch := GetExchangeByName(authExchanges[x])
supportedAssets := exch.GetAssetTypes()
for y := range supportedAssets {
pairs, err := exch.GetEnabledPairs(supportedAssets[y])
if err != nil {
log.Errorf(log.OrderMgr,
"Order manager: Unable to get enabled pairs for %s and asset type %s: %s",
authExchanges[x],
supportedAssets[y],
err)
continue
}
if len(pairs) == 0 {
if Bot.Settings.Verbose {
log.Debugf(log.OrderMgr,
"Order manager: No pairs enabled for %s and asset type %s, skipping...",
authExchanges[x],
supportedAssets[y])
}
continue
}
req := order.GetOrdersRequest{
Side: order.AnySide,
Type: order.AnyType,
Pairs: exch.GetEnabledPairs(supportedAssets[y]),
Pairs: pairs,
}
result, err := exch.GetActiveOrders(&req)
if err != nil {
log.Warnf(log.OrderMgr, "Order manager: Unable to get active orders: %s", err)
log.Warnf(log.OrderMgr,
"Order manager: Unable to get active orders for %s and asset type %s: %s",
authExchanges[x],
supportedAssets[y],
err)
continue
}

View File

@@ -254,6 +254,11 @@ func TestCancelOrder(t *testing.T) {
t.Error("Expected error due to no order found")
}
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
cancel := &order.Cancel{
Exchange: fakePassExchange,
ID: "TestCancelOrder",
@@ -261,7 +266,7 @@ func TestCancelOrder(t *testing.T) {
Status: order.New,
AssetType: asset.Spot,
Date: time.Now(),
Pair: currency.NewPairFromString("BTCUSD"),
Pair: pair,
}
err = Bot.OrderManager.Cancel(cancel)
if err != nil {
@@ -326,9 +331,14 @@ func TestSubmit(t *testing.T) {
t.Error("Expected error from validation")
}
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
Bot.OrderManager.cfg.EnforceLimitConfig = true
Bot.OrderManager.cfg.AllowMarketOrders = false
o.Pair = currency.NewPairFromString("BTCUSD")
o.Pair = pair
o.AssetType = asset.Spot
o.Side = order.Buy
o.Amount = 1
@@ -351,8 +361,13 @@ func TestSubmit(t *testing.T) {
t.Error("Expected fail due to order exchange not found in allowed list")
}
failPair, err := currency.NewPairFromString("BTCAUD")
if err != nil {
t.Fatal(err)
}
Bot.OrderManager.cfg.AllowedExchanges = nil
Bot.OrderManager.cfg.AllowedPairs = currency.Pairs{currency.NewPairFromString("BTCAUD")}
Bot.OrderManager.cfg.AllowedPairs = currency.Pairs{failPair}
_, err = Bot.OrderManager.Submit(o)
if err == nil {
t.Error("Expected fail due to order pair not found in allowed list")

View File

@@ -66,7 +66,14 @@ func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks {
exchangeOB.ExchangeName = exchName
for y := range assets {
currencies := exchanges[x].GetEnabledPairs(assets[y])
currencies, err := exchanges[x].GetEnabledPairs(assets[y])
if err != nil {
log.Errorf(log.RESTSys,
"Exchange %s could not retrieve enabled currencies. Err: %s\n",
exchName,
err)
continue
}
for z := range currencies {
ob, err := exchanges[x].FetchOrderbook(currencies[z], assets[y])
if err != nil {

View File

@@ -8,12 +8,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -57,26 +56,29 @@ func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64) s
)
}
func printTickerSummary(result *ticker.Price, p currency.Pair, assetType asset.Item, exchangeName, protocol string, err error) {
func printTickerSummary(result *ticker.Price, protocol string, err error) {
if err != nil {
log.Errorf(log.Ticker, "Failed to get %s %s %s %s ticker. Error: %s\n",
exchangeName,
if err == common.ErrNotYetImplemented {
log.Warnf(log.Ticker, "Failed to get %s ticker. Error: %s\n",
protocol,
err)
return
}
log.Errorf(log.Ticker, "Failed to get %s ticker. Error: %s\n",
protocol,
p,
assetType,
err)
return
}
stats.Add(exchangeName, p, assetType, result.Last, result.Volume)
if p.Quote.IsFiatCurrency() &&
p.Quote != Bot.Config.Currency.FiatDisplayCurrency {
origCurrency := p.Quote.Upper()
stats.Add(result.ExchangeName, result.Pair, result.AssetType, result.Last, result.Volume)
if result.Pair.Quote.IsFiatCurrency() &&
result.Pair.Quote != Bot.Config.Currency.FiatDisplayCurrency {
origCurrency := result.Pair.Quote.Upper()
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n",
exchangeName,
result.ExchangeName,
protocol,
FormatCurrency(p),
strings.ToUpper(assetType.String()),
FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
printConvertCurrencyFormat(origCurrency, result.Last),
printConvertCurrencyFormat(origCurrency, result.Ask),
printConvertCurrencyFormat(origCurrency, result.Bid),
@@ -84,13 +86,13 @@ func printTickerSummary(result *ticker.Price, p currency.Pair, assetType asset.I
printConvertCurrencyFormat(origCurrency, result.Low),
result.Volume)
} else {
if p.Quote.IsFiatCurrency() &&
p.Quote == Bot.Config.Currency.FiatDisplayCurrency {
if result.Pair.Quote.IsFiatCurrency() &&
result.Pair.Quote == Bot.Config.Currency.FiatDisplayCurrency {
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n",
exchangeName,
result.ExchangeName,
protocol,
FormatCurrency(p),
strings.ToUpper(assetType.String()),
FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
printCurrencyFormat(result.Last),
printCurrencyFormat(result.Ask),
printCurrencyFormat(result.Bid),
@@ -99,10 +101,10 @@ func printTickerSummary(result *ticker.Price, p currency.Pair, assetType asset.I
result.Volume)
} else {
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f\n",
exchangeName,
result.ExchangeName,
protocol,
FormatCurrency(p),
strings.ToUpper(assetType.String()),
FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
result.Last,
result.Ask,
result.Bid,
@@ -113,13 +115,16 @@ func printTickerSummary(result *ticker.Price, p currency.Pair, assetType asset.I
}
}
func printOrderbookSummary(result *orderbook.Base, p currency.Pair, assetType asset.Item, exchangeName, protocol string, err error) {
func printOrderbookSummary(result *orderbook.Base, protocol string, err error) {
if err != nil {
log.Errorf(log.OrderBook, "Failed to get %s %s %s orderbook of type %s. Error: %s\n",
exchangeName,
if err == common.ErrNotYetImplemented {
log.Warnf(log.Ticker, "Failed to get %s ticker. Error: %s\n",
protocol,
err)
return
}
log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n",
protocol,
p,
assetType,
err)
return
}
@@ -127,53 +132,53 @@ func printOrderbookSummary(result *orderbook.Base, p currency.Pair, assetType as
bidsAmount, bidsValue := result.TotalBidsAmount()
asksAmount, asksValue := result.TotalAsksAmount()
if p.Quote.IsFiatCurrency() &&
p.Quote != Bot.Config.Currency.FiatDisplayCurrency {
origCurrency := p.Quote.Upper()
if result.Pair.Quote.IsFiatCurrency() &&
result.Pair.Quote != Bot.Config.Currency.FiatDisplayCurrency {
origCurrency := result.Pair.Quote.Upper()
log.Infof(log.OrderBook, "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n",
exchangeName,
result.ExchangeName,
protocol,
FormatCurrency(p),
strings.ToUpper(assetType.String()),
FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
len(result.Bids),
bidsAmount,
p.Base,
result.Pair.Base,
printConvertCurrencyFormat(origCurrency, bidsValue),
len(result.Asks),
asksAmount,
p.Base,
result.Pair.Base,
printConvertCurrencyFormat(origCurrency, asksValue),
)
} else {
if p.Quote.IsFiatCurrency() &&
p.Quote == Bot.Config.Currency.FiatDisplayCurrency {
if result.Pair.Quote.IsFiatCurrency() &&
result.Pair.Quote == Bot.Config.Currency.FiatDisplayCurrency {
log.Infof(log.OrderBook, "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n",
exchangeName,
result.ExchangeName,
protocol,
FormatCurrency(p),
strings.ToUpper(assetType.String()),
FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
len(result.Bids),
bidsAmount,
p.Base,
result.Pair.Base,
printCurrencyFormat(bidsValue),
len(result.Asks),
asksAmount,
p.Base,
result.Pair.Base,
printCurrencyFormat(asksValue),
)
} else {
log.Infof(log.OrderBook, "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %f Asks len: %d Amount: %f %s. Total value: %f\n",
exchangeName,
result.ExchangeName,
protocol,
FormatCurrency(p),
strings.ToUpper(assetType.String()),
FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
len(result.Bids),
bidsAmount,
p.Base,
result.Pair.Base,
bidsValue,
len(result.Asks),
asksAmount,
p.Base,
result.Pair.Base,
asksValue,
)
}
@@ -212,28 +217,27 @@ func WebsocketRoutine() {
)
}
// TO-DO: expose IsConnected() and IsConnecting so this can be simplified
if exchanges[i].IsWebsocketEnabled() {
ws, err := exchanges[i].GetWebsocket()
if err != nil {
log.Errorf(
log.WebsocketMgr,
"Exchange %s GetWebsocket error: %s\n",
exchanges[i].GetName(),
err,
)
return
}
ws, err := exchanges[i].GetWebsocket()
if err != nil {
log.Errorf(
log.WebsocketMgr,
"Exchange %s GetWebsocket error: %s\n",
exchanges[i].GetName(),
err,
)
return
}
// Exchange sync manager might have already started ws
// service or is in the process of connecting, so check
if ws.IsConnected() || ws.IsConnecting() {
return
}
// Exchange sync manager might have already started ws
// service or is in the process of connecting, so check
if ws.IsConnected() || ws.IsConnecting() {
return
}
// Data handler routine
go WebsocketDataReceiver(ws)
// Data handler routine
go WebsocketDataReceiver(ws)
if ws.IsEnabled() {
err = ws.Connect()
if err != nil {
log.Errorf(log.WebsocketMgr, "%v\n", err)
@@ -254,7 +258,7 @@ var wg sync.WaitGroup
// WebsocketDataReceiver handles websocket data coming from a websocket feed
// associated with an exchange
func WebsocketDataReceiver(ws *wshandler.Websocket) {
func WebsocketDataReceiver(ws *stream.Websocket) {
wg.Add(1)
defer wg.Done()
@@ -262,7 +266,7 @@ func WebsocketDataReceiver(ws *wshandler.Websocket) {
select {
case <-shutdowner:
return
case data := <-ws.DataHandler:
case data := <-ws.ToRoutine:
err := WebsocketDataHandler(ws.GetName(), data)
if err != nil {
log.Error(log.WebsocketMgr, err)
@@ -278,12 +282,13 @@ func WebsocketDataHandler(exchName string, data interface{}) error {
return fmt.Errorf("routines.go - exchange %s nil data sent to websocket",
exchName)
}
switch d := data.(type) {
case string:
log.Info(log.WebsocketMgr, d)
case error:
return fmt.Errorf("routines.go exchange %s websocket error - %s", exchName, data)
case wshandler.TradeData:
case stream.TradeData:
if Bot.Settings.Verbose {
log.Infof(log.WebsocketMgr, "%s websocket %s %s trade updated %+v",
exchName,
@@ -291,7 +296,7 @@ func WebsocketDataHandler(exchName string, data interface{}) error {
d.AssetType,
d)
}
case wshandler.FundingData:
case stream.FundingData:
if Bot.Settings.Verbose {
log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v",
exchName,
@@ -307,9 +312,9 @@ func WebsocketDataHandler(exchName string, data interface{}) error {
SyncItemTicker,
nil)
}
err := ticker.ProcessTicker(exchName, d, d.AssetType)
printTickerSummary(d, d.Pair, d.AssetType, exchName, "websocket", err)
case wshandler.KlineData:
err := ticker.ProcessTicker(d)
printTickerSummary(d, "websocket", err)
case stream.KlineData:
if Bot.Settings.Verbose {
log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v",
exchName,
@@ -317,22 +322,15 @@ func WebsocketDataHandler(exchName string, data interface{}) error {
d.AssetType,
d)
}
case wshandler.WebsocketOrderbookUpdate:
case *orderbook.Base:
if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil {
Bot.ExchangeCurrencyPairManager.update(exchName,
d.Pair,
d.Asset,
d.AssetType,
SyncItemOrderbook,
nil)
}
if Bot.Settings.Verbose {
log.Infof(log.WebsocketMgr,
"%s websocket %s %s orderbook updated",
exchName,
FormatCurrency(d.Pair),
d.Asset)
}
printOrderbookSummary(d, "websocket", nil)
case *order.Detail:
if !Bot.OrderManager.orderStore.exists(d) {
err := Bot.OrderManager.orderStore.Add(d)
@@ -356,7 +354,7 @@ func WebsocketDataHandler(exchName string, data interface{}) error {
od.UpdateOrderFromModify(d)
case order.ClassificationError:
return errors.New(d.Error())
case wshandler.UnhandledMessageWarning:
case stream.UnhandledMessageWarning:
log.Warn(log.WebsocketMgr, d.Message)
default:
if Bot.Settings.Verbose {

View File

@@ -5,19 +5,16 @@ import (
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
func TestWebsocketDataHandlerProcess(t *testing.T) {
ws := wshandler.New()
err := ws.Setup(&wshandler.WebsocketSetup{Enabled: true})
if err != nil {
t.Error(err)
}
ws.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
ws := sharedtestvalues.NewTestWebsocket()
go WebsocketDataReceiver(ws)
ws.DataHandler <- "string"
time.Sleep(time.Second)
@@ -36,11 +33,11 @@ func TestHandleData(t *testing.T) {
if err == nil {
t.Error("Expected nil data error")
}
err = WebsocketDataHandler(exchName, wshandler.TradeData{})
err = WebsocketDataHandler(exchName, stream.TradeData{})
if err != nil {
t.Error(err)
}
err = WebsocketDataHandler(exchName, wshandler.FundingData{})
err = WebsocketDataHandler(exchName, stream.FundingData{})
if err != nil {
t.Error(err)
}
@@ -48,11 +45,7 @@ func TestHandleData(t *testing.T) {
if err != nil {
t.Error(err)
}
err = WebsocketDataHandler(exchName, wshandler.KlineData{})
if err != nil {
t.Error(err)
}
err = WebsocketDataHandler(exchName, wshandler.WebsocketOrderbookUpdate{})
err = WebsocketDataHandler(exchName, stream.KlineData{})
if err != nil {
t.Error(err)
}
@@ -107,7 +100,9 @@ func TestHandleData(t *testing.T) {
t.Error(err)
}
err = WebsocketDataHandler(exchName, wshandler.UnhandledMessageWarning{Message: "there's an issue here's a tissue"})
err = WebsocketDataHandler(exchName, stream.UnhandledMessageWarning{
Message: "there's an issue here's a tissue"},
)
if err != nil {
t.Error(err)
}
@@ -124,4 +119,16 @@ func TestHandleData(t *testing.T) {
if err.Error() != classificationError.Error() {
t.Errorf("Problem formatting error. Expected %v Received %v", classificationError.Error(), err.Error())
}
err = WebsocketDataHandler(exchName, &orderbook.Base{
ExchangeName: fakePassExchange,
Pair: currency.NewPair(currency.BTC, currency.USD),
})
if err != nil {
t.Error(err)
}
err = WebsocketDataHandler(exchName, "this is a test string")
if err != nil {
t.Error(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -200,16 +200,18 @@ func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair
}
switch syncType {
case SyncItemOrderbook, SyncItemTrade, SyncItemTicker:
if !e.Cfg.SyncOrderbook && syncType == SyncItemOrderbook {
case SyncItemOrderbook:
if !e.Cfg.SyncOrderbook {
return
}
if !e.Cfg.SyncTicker && syncType == SyncItemTicker {
case SyncItemTicker:
if !e.Cfg.SyncTicker {
return
}
if !e.Cfg.SyncTrades && syncType == SyncItemTrade {
case SyncItemTrade:
if !e.Cfg.SyncTrades {
return
}
default:
@@ -236,7 +238,10 @@ func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair
if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s ticker sync complete %v [%d/%d].\n",
exchangeName, FormatCurrency(p).String(), removedCounter, createdCounter)
exchangeName,
FormatCurrency(p).String(),
removedCounter,
createdCounter)
e.initSyncWG.Done()
}
@@ -251,7 +256,10 @@ func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair
if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s orderbook sync complete %v [%d/%d].\n",
exchangeName, FormatCurrency(p).String(), removedCounter, createdCounter)
exchangeName,
FormatCurrency(p).String(),
removedCounter,
createdCounter)
e.initSyncWG.Done()
}
@@ -266,7 +274,10 @@ func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair
if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s trade sync complete %v [%d/%d].\n",
exchangeName, FormatCurrency(p).String(), removedCounter, createdCounter)
exchangeName,
FormatCurrency(p).String(),
removedCounter,
createdCounter)
e.initSyncWG.Done()
}
}
@@ -276,7 +287,8 @@ func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair
func (e *ExchangeCurrencyPairSyncer) worker() {
cleanup := func() {
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer worker shutting down.")
log.Debugln(log.SyncMgr,
"Exchange CurrencyPairSyncer worker shutting down.")
}
defer cleanup()
@@ -293,8 +305,10 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
if exchanges[x].SupportsWebsocket() && exchanges[x].IsWebsocketEnabled() {
ws, err := exchanges[x].GetWebsocket()
if err != nil {
log.Errorf(log.SyncMgr, "%s unable to get websocket pointer. Err: %s\n",
exchangeName, err)
log.Errorf(log.SyncMgr,
"%s unable to get websocket pointer. Err: %s\n",
exchangeName,
err)
usingREST = true
}
@@ -308,7 +322,17 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
}
for y := range assetTypes {
enabledPairs := exchanges[x].GetEnabledPairs(assetTypes[y])
if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil {
continue
}
enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y])
if err != nil {
log.Errorf(log.SyncMgr,
"%s failed to get enabled pairs. Err: %s\n",
exchangeName,
err)
continue
}
for i := range enabledPairs {
if atomic.LoadInt32(&e.shutdown) == 1 {
return
@@ -410,7 +434,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
} else {
result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType)
}
printTickerSummary(result, c.Pair, c.AssetType, exchangeName, "REST", err)
printTickerSummary(result, "REST", err)
if err == nil {
if Bot.Config.RemoteControl.WebsocketRPC.Enabled {
relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName)
@@ -449,7 +473,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true)
result, err := exchanges[x].UpdateOrderbook(c.Pair, c.AssetType)
printOrderbookSummary(result, c.Pair, c.AssetType, exchangeName, "REST", err)
printOrderbookSummary(result, "REST", err)
if err == nil {
if Bot.Config.RemoteControl.WebsocketRPC.Enabled {
relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), exchangeName)
@@ -497,8 +521,10 @@ func (e *ExchangeCurrencyPairSyncer) Start() {
if supportsWebsocket && exchanges[x].IsWebsocketEnabled() {
ws, err := exchanges[x].GetWebsocket()
if err != nil {
log.Errorf(log.SyncMgr, "%s failed to get websocket. Err: %s\n",
exchangeName, err)
log.Errorf(log.SyncMgr,
"%s failed to get websocket. Err: %s\n",
exchangeName,
err)
usingREST = true
}
@@ -507,8 +533,10 @@ func (e *ExchangeCurrencyPairSyncer) Start() {
err = ws.Connect()
if err != nil {
log.Errorf(log.SyncMgr, "%s websocket failed to connect. Err: %s\n",
exchangeName, err)
log.Errorf(log.SyncMgr,
"%s websocket failed to connect. Err: %s\n",
exchangeName,
err)
usingREST = true
} else {
usingWebsocket = true
@@ -521,7 +549,22 @@ func (e *ExchangeCurrencyPairSyncer) Start() {
}
for y := range assetTypes {
enabledPairs := exchanges[x].GetEnabledPairs(assetTypes[y])
if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil {
log.Warnf(log.SyncMgr,
"%s asset type %s is disabled, fetching enabled pairs is paused",
exchangeName,
assetTypes[y])
continue
}
enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y])
if err != nil {
log.Errorf(log.SyncMgr,
"%s failed to get enabled pairs. Err: %s\n",
exchangeName,
err)
continue
}
for i := range enabledPairs {
if e.exists(exchangeName, enabledPairs[i], assetTypes[y]) {
continue

View File

@@ -351,8 +351,14 @@ func wsGetTicker(client *WebsocketClient, data interface{}) error {
return err
}
result, err := GetSpecificTicker(currency.NewPairFromString(tickerReq.Currency),
tickerReq.Exchange, asset.Item(tickerReq.AssetType))
p, err := currency.NewPairFromString(tickerReq.Currency)
if err != nil {
return err
}
result, err := GetSpecificTicker(p,
tickerReq.Exchange,
asset.Item(tickerReq.AssetType))
if err != nil {
wsResp.Error = err.Error()
@@ -383,7 +389,12 @@ func wsGetOrderbook(client *WebsocketClient, data interface{}) error {
return err
}
result, err := GetSpecificOrderbook(currency.NewPairFromString(orderbookReq.Currency),
p, err := currency.NewPairFromString(orderbookReq.Currency)
if err != nil {
return err
}
result, err := GetSpecificOrderbook(p,
orderbookReq.Exchange, asset.Item(orderbookReq.AssetType))
if err != nil {

View File

@@ -40,7 +40,6 @@ func (a *Alphapoint) WebsocketClient() {
for a.Enabled {
msgType, resp, err := a.WebsocketConn.ReadMessage()
if err != nil {
a.Websocket.ReadMessageErrors <- err
log.Error(log.ExchangeSys, err)
break
}

View File

@@ -16,7 +16,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -35,10 +34,6 @@ func (a *Alphapoint) SetDefaults() {
a.API.CredentialsValidator.RequiresKey = true
a.API.CredentialsValidator.RequiresSecret = true
a.CurrencyPairs.AssetTypes = asset.Items{
asset.Spot,
}
a.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
@@ -129,23 +124,24 @@ func (a *Alphapoint) FetchAccountInfo() (account.Holdings, error) {
// UpdateTicker updates and returns the ticker for a currency pair
func (a *Alphapoint) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
tick, err := a.GetTicker(p.String())
if err != nil {
return tickerPrice, err
return nil, err
}
tickerPrice.Pair = p
tickerPrice.Ask = tick.Ask
tickerPrice.Bid = tick.Bid
tickerPrice.Low = tick.Low
tickerPrice.High = tick.High
tickerPrice.Volume = tick.Volume
tickerPrice.Last = tick.Last
err = ticker.ProcessTicker(a.Name, tickerPrice, assetType)
err = ticker.ProcessTicker(&ticker.Price{
Pair: p,
Ask: tick.Ask,
Bid: tick.Bid,
Low: tick.Low,
High: tick.High,
Volume: tick.Volume,
Last: tick.Last,
ExchangeName: a.Name,
AssetType: assetType,
})
if err != nil {
return tickerPrice, err
return nil, err
}
return ticker.GetTicker(a.Name, p, assetType)
@@ -313,11 +309,6 @@ func (a *Alphapoint) WithdrawFiatFundsToInternationalBank(withdrawRequest *withd
return "", common.ErrNotYetImplemented
}
// GetWebsocket returns a pointer to the exchange websocket
func (a *Alphapoint) GetWebsocket() (*wshandler.Websocket, error) {
return nil, common.ErrNotYetImplemented
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (a *Alphapoint) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
return 0, common.ErrFunctionNotSupported
@@ -406,28 +397,6 @@ func (a *Alphapoint) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (a *Alphapoint) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (a *Alphapoint) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (a *Alphapoint) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (a *Alphapoint) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (a *Alphapoint) ValidateCredentials() error {

View File

@@ -14,6 +14,7 @@ type Items []Item
const (
Spot = Item("spot")
Margin = Item("margin")
MarginFunding = Item("marginfunding")
Index = Item("index")
Binary = Item("binary")
PerpetualContract = Item("perpetualcontract")
@@ -26,6 +27,7 @@ const (
var supported = Items{
Spot,
Margin,
MarginFunding,
Index,
Binary,
PerpetualContract,
@@ -66,7 +68,6 @@ func (a Items) Contains(i Item) bool {
return true
}
}
return false
}

View File

@@ -17,9 +17,7 @@ import (
"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/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -63,7 +61,6 @@ const (
// Binance is the overarching type across the Bithumb package
type Binance struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
// Valid string list that is required by the exchange
validLimits []int
@@ -569,17 +566,6 @@ func (b *Binance) CheckLimit(limit int) error {
return errors.New("incorrect limit values - valid values are 5, 10, 20, 50, 100, 500, 1000")
}
// CheckSymbol checks value against a variable list
func (b *Binance) CheckSymbol(symbol string, assetType asset.Item) error {
enPairs := b.GetAvailablePairs(assetType)
for x := range enPairs {
if b.FormatExchangeCurrency(enPairs[x], assetType).String() == symbol {
return nil
}
}
return errors.New("incorrect symbol values - please check available pairs in configuration")
}
// SetValues sets the default valid values
func (b *Binance) SetValues() {
b.validLimits = []int{5, 10, 20, 50, 100, 500, 1000, 5000}

View File

@@ -29,6 +29,7 @@ func TestMain(m *testing.M) {
binanceConfig.API.Credentials.Key = apiKey
binanceConfig.API.Credentials.Secret = apiSecret
b.SetDefaults()
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(binanceConfig)
if err != nil {
log.Fatal("Binance setup error", err)

View File

@@ -33,6 +33,7 @@ func TestMain(m *testing.M) {
binanceConfig.API.Credentials.Key = apiKey
binanceConfig.API.Credentials.Secret = apiSecret
b.SetDefaults()
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(binanceConfig)
if err != nil {
log.Fatal("Binance setup error", err)
@@ -45,7 +46,6 @@ func TestMain(m *testing.M) {
b.HTTPClient = newClient
b.API.Endpoints.URL = serverDetails
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
log.Printf(sharedtestvalues.MockTesting, b.Name, b.API.Endpoints.URL)
os.Exit(m.Run())
}

View File

@@ -206,7 +206,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) {
var feeBuilder = setFeeBuilder()
b.GetFeeByType(feeBuilder)
if !areTestAPIKeysSet() {
if !areTestAPIKeysSet() || mockTests {
if feeBuilder.FeeType != exchange.OfflineTradeFee {
t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType)
}
@@ -439,6 +439,7 @@ func TestCancelExchangeOrder(t *testing.T) {
WalletAddress: core.BitcoinDonationAddress,
AccountID: "1",
Pair: currency.NewPair(currency.LTC, currency.BTC),
AssetType: asset.Spot,
}
err := b.CancelOrder(orderCancellation)
@@ -904,10 +905,13 @@ func TestExecutionTypeToOrderStatus(t *testing.T) {
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
currencyPair, err := currency.NewPairFromString("BTCUSDT")
if err != nil {
t.Fatal(err)
}
startTime := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, startTime, end, kline.OneDay)
_, err = b.GetHistoricCandles(currencyPair, asset.Spot, startTime, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
@@ -919,10 +923,13 @@ func TestGetHistoricCandles(t *testing.T) {
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
currencyPair, err := currency.NewPairFromString("BTCUSDT")
if err != nil {
t.Fatal(err)
}
startTime := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, end, kline.OneDay)
_, err = b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}

View File

@@ -25,15 +25,19 @@ type ExchangeInfo struct {
} `json:"rateLimits"`
ExchangeFilters interface{} `json:"exchangeFilters"`
Symbols []struct {
Symbol string `json:"symbol"`
Status string `json:"status"`
BaseAsset string `json:"baseAsset"`
BaseAssetPrecision int `json:"baseAssetPrecision"`
QuoteAsset string `json:"quoteAsset"`
QuotePrecision int `json:"quotePrecision"`
OrderTypes []string `json:"orderTypes"`
IcebergAllowed bool `json:"icebergAllowed"`
Filters []struct {
Symbol string `json:"symbol"`
Status string `json:"status"`
BaseAsset string `json:"baseAsset"`
BaseAssetPrecision int `json:"baseAssetPrecision"`
QuoteAsset string `json:"quoteAsset"`
QuotePrecision int `json:"quotePrecision"`
OrderTypes []string `json:"orderTypes"`
IcebergAllowed bool `json:"icebergAllowed"`
OCOAllowed bool `json:"ocoAllowed"`
QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"`
IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"`
IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"`
Filters []struct {
FilterType string `json:"filterType"`
MinPrice float64 `json:"minPrice,string"`
MaxPrice float64 `json:"maxPrice,string"`
@@ -594,89 +598,111 @@ type UserAccountStream struct {
}
type wsAccountInfo struct {
CanDeposit bool `json:"D"`
CanTrade bool `json:"T"`
CanWithdraw bool `json:"W"`
EventTime int64 `json:"E"`
LastUpdated int64 `json:"u"`
BuyerCommission float64 `json:"b"`
MakerCommission float64 `json:"m"`
SellerCommission float64 `json:"s"`
TakerCommission float64 `json:"t"`
EventType string `json:"e"`
Currencies []struct {
Asset string `json:"a"`
Available float64 `json:"f,string"`
Locked float64 `json:"l,string"`
} `json:"B"`
Stream string `json:"stream"`
Data struct {
CanDeposit bool `json:"D"`
CanTrade bool `json:"T"`
CanWithdraw bool `json:"W"`
EventTime int64 `json:"E"`
LastUpdated int64 `json:"u"`
BuyerCommission float64 `json:"b"`
MakerCommission float64 `json:"m"`
SellerCommission float64 `json:"s"`
TakerCommission float64 `json:"t"`
EventType string `json:"e"`
Currencies []struct {
Asset string `json:"a"`
Available float64 `json:"f,string"`
Locked float64 `json:"l,string"`
} `json:"B"`
} `json:"data"`
}
type wsAccountPosition struct {
Currencies []struct {
Asset string `json:"a"`
Available float64 `json:"f,string"`
Locked float64 `json:"l,string"`
} `json:"B"`
EventTime int64 `json:"E"`
LastUpdated int64 `json:"u"`
EventType string `json:"e"`
Stream string `json:"stream"`
Data struct {
Currencies []struct {
Asset string `json:"a"`
Available float64 `json:"f,string"`
Locked float64 `json:"l,string"`
} `json:"B"`
EventTime int64 `json:"E"`
LastUpdated int64 `json:"u"`
EventType string `json:"e"`
} `json:"data"`
}
type wsBalanceUpdate struct {
EventTime int64 `json:"E"`
ClearTime int64 `json:"T"`
BalanceDelta float64 `json:"d,string"`
Asset string `json:"a"`
EventType string `json:"e"`
Stream string `json:"stream"`
Data struct {
EventTime int64 `json:"E"`
ClearTime int64 `json:"T"`
BalanceDelta float64 `json:"d,string"`
Asset string `json:"a"`
EventType string `json:"e"`
} `json:"data"`
}
type wsOrderUpdate struct {
ClientOrderID string `json:"C"`
EventTime int64 `json:"E"`
IcebergQuantity float64 `json:"F,string"`
LastExecutedPrice float64 `json:"L,string"`
CommissionAsset float64 `json:"N"`
OrderCreationTime int64 `json:"O"`
StopPrice float64 `json:"P,string"`
QuoteOrderQuantity float64 `json:"Q,string"`
Side string `json:"S"`
TransactionTime int64 `json:"T"`
OrderStatus string `json:"X"`
LastQuoteAssetTransactedQuantity float64 `json:"Y,string"`
CumulativeQuoteTransactedQuantity float64 `json:"Z,string"`
CancelledClientOrderID string `json:"c"`
EventType string `json:"e"`
TimeInForce string `json:"f"`
OrderListID int64 `json:"g"`
OrderID int64 `json:"i"`
LastExecutedQuantity float64 `json:"l,string"`
IsMaker bool `json:"m"`
Commission float64 `json:"n,string"`
OrderType string `json:"o"`
Price float64 `json:"p,string"`
Quantity float64 `json:"q,string"`
RejectionReason string `json:"r"`
Symbol string `json:"s"`
TradeID int64 `json:"t"`
IsOnOrderBook bool `json:"w"`
CurrentExecutionType string `json:"x"`
CumulativeFilledQuantity float64 `json:"z,string"`
Stream string `json:"stream"`
Data struct {
ClientOrderID string `json:"C"`
EventTime int64 `json:"E"`
IcebergQuantity float64 `json:"F,string"`
LastExecutedPrice float64 `json:"L,string"`
CommissionAsset float64 `json:"N"`
OrderCreationTime int64 `json:"O"`
StopPrice float64 `json:"P,string"`
QuoteOrderQuantity float64 `json:"Q,string"`
Side string `json:"S"`
TransactionTime int64 `json:"T"`
OrderStatus string `json:"X"`
LastQuoteAssetTransactedQuantity float64 `json:"Y,string"`
CumulativeQuoteTransactedQuantity float64 `json:"Z,string"`
CancelledClientOrderID string `json:"c"`
EventType string `json:"e"`
TimeInForce string `json:"f"`
OrderListID int64 `json:"g"`
OrderID int64 `json:"i"`
LastExecutedQuantity float64 `json:"l,string"`
IsMaker bool `json:"m"`
Commission float64 `json:"n,string"`
OrderType string `json:"o"`
Price float64 `json:"p,string"`
Quantity float64 `json:"q,string"`
RejectionReason string `json:"r"`
Symbol string `json:"s"`
TradeID int64 `json:"t"`
IsOnOrderBook bool `json:"w"`
CurrentExecutionType string `json:"x"`
CumulativeFilledQuantity float64 `json:"z,string"`
} `json:"data"`
}
type wsListStauts struct {
ListClientOrderID string `json:"C"`
EventTime int64 `json:"E"`
ListOrderStatus string `json:"L"`
Orders []struct {
ClientOrderID string `json:"c"`
OrderID int64 `json:"i"`
Symbol string `json:"s"`
} `json:"O"`
TransactionTime int64 `json:"T"`
ContingencyType string `json:"c"`
EventType string `json:"e"`
OrderListID int64 `json:"g"`
ListStatusType string `json:"l"`
RejectionReason string `json:"r"`
Symbol string `json:"s"`
type wsListStatus struct {
Stream string `json:"stream"`
Data struct {
ListClientOrderID string `json:"C"`
EventTime int64 `json:"E"`
ListOrderStatus string `json:"L"`
Orders []struct {
ClientOrderID string `json:"c"`
OrderID int64 `json:"i"`
Symbol string `json:"s"`
} `json:"O"`
TransactionTime int64 `json:"T"`
ContingencyType string `json:"c"`
EventType string `json:"e"`
OrderListID int64 `json:"g"`
ListStatusType string `json:"l"`
RejectionReason string `json:"r"`
Symbol string `json:"s"`
} `json:"data"`
}
// WsPayload defines the payload through the websocket connection
type WsPayload struct {
Method string `json:"method"`
Params []string `json:"params"`
ID int64 `json:"id"`
}

View File

@@ -14,23 +14,23 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
binanceDefaultWebsocketURL = "wss://stream.binance.com:9443"
binanceDefaultWebsocketURL = "wss://stream.binance.com:9443/stream"
pingDelay = time.Minute * 9
)
var listenKey string
// WsConnect intiates a websocket connection
// WsConnect initiates a websocket connection
func (b *Binance) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
@@ -39,54 +39,43 @@ func (b *Binance) WsConnect() error {
listenKey, err = b.GetWsAuthStreamKey()
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
log.Errorf(log.ExchangeSys, "%v unable to connect to authenticated Websocket. Error: %s", b.Name, err)
log.Errorf(log.ExchangeSys,
"%v unable to connect to authenticated Websocket. Error: %s",
b.Name,
err)
} else {
// cleans on failed connection
clean := strings.Split(b.Websocket.GetWebsocketURL(), "?streams=")
authPayload := clean[0] + "?streams=" + listenKey
err = b.Websocket.SetWebsocketURL(authPayload, false, false)
if err != nil {
return err
}
}
}
pairs := b.GetEnabledPairs(asset.Spot).Strings()
tick := strings.ToLower(
strings.Replace(
strings.Join(pairs, "@ticker/"), "-", "", -1)) + "@ticker"
trade := strings.ToLower(
strings.Replace(
strings.Join(pairs, "@trade/"), "-", "", -1)) + "@trade"
kline := strings.ToLower(
strings.Replace(
strings.Join(pairs, "@kline_1m/"), "-", "", -1)) + "@kline_1m"
depth := strings.ToLower(
strings.Replace(
strings.Join(pairs, "@depth/"), "-", "", -1)) + "@depth"
wsurl := b.Websocket.GetWebsocketURL() +
"/stream?streams=" +
tick +
"/" +
trade +
"/" +
kline +
"/" +
depth
if listenKey != "" {
wsurl += "/" +
listenKey
}
b.WebsocketConn.URL = wsurl
b.WebsocketConn.Verbose = b.Verbose
err = b.WebsocketConn.Dial(&dialer, http.Header{})
err = b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return fmt.Errorf("%v - Unable to connect to Websocket. Error: %s",
b.Name,
err)
}
b.WebsocketConn.SetupPingHandler(wshandler.WebsocketPingHandler{
if b.Websocket.CanUseAuthenticatedEndpoints() {
go b.KeepAuthKeyAlive()
}
b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
UseGorillaHandler: true,
MessageType: websocket.PongMessage,
Delay: pingDelay,
})
enabledPairs := b.GetEnabledPairs(asset.Spot)
enabledPairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
for i := range enabledPairs {
err = b.SeedLocalCache(enabledPairs[i])
if err != nil {
@@ -95,17 +84,19 @@ func (b *Binance) WsConnect() error {
}
go b.wsReadData()
go b.KeepAuthKeyAlive()
return nil
subs, err := b.GenerateSubscriptions()
if err != nil {
return err
}
return b.Websocket.SubscribeToChannels(subs)
}
// KeepAuthKeyAlive will continuously send messages to
// keep the WS auth key active
func (b *Binance) KeepAuthKeyAlive() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
defer b.Websocket.Wg.Done()
ticks := time.NewTicker(time.Minute * 30)
for {
select {
@@ -116,7 +107,8 @@ func (b *Binance) KeepAuthKeyAlive() {
err := b.MaintainWsAuthStreamKey()
if err != nil {
b.Websocket.DataHandler <- err
log.Warnf(log.ExchangeSys, b.Name+" - Unable to renew auth websocket token, may experience shutdown")
log.Warnf(log.ExchangeSys,
b.Name+" - Unable to renew auth websocket token, may experience shutdown")
}
}
}
@@ -125,25 +117,16 @@ func (b *Binance) KeepAuthKeyAlive() {
// wsReadData receives and passes on websocket messages for processing
func (b *Binance) wsReadData() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
for {
select {
case <-b.Websocket.ShutdownC:
return
defer b.Websocket.Wg.Done()
default:
resp, err := b.WebsocketConn.ReadMessage()
if err != nil {
b.Websocket.ReadMessageErrors <- err
return
}
b.Websocket.TrafficAlert <- struct{}{}
err = b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
for {
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
@@ -163,112 +146,122 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
return nil
}
}
if e, ok := multiStreamData["e"].(string); ok {
switch e {
case "outboundAccountInfo":
var data wsAccountInfo
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to outboundAccountInfo structure %s",
b.Name,
err)
}
b.Websocket.DataHandler <- data
case "outboundAccountPosition":
var data wsAccountPosition
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to outboundAccountPosition structure %s",
b.Name,
err)
}
b.Websocket.DataHandler <- data
case "balanceUpdate":
var data wsBalanceUpdate
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to balanceUpdate structure %s",
b.Name,
err)
}
b.Websocket.DataHandler <- data
case "executionReport":
var data wsOrderUpdate
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to executionReport structure %s",
b.Name,
err)
}
var orderID = strconv.FormatInt(data.OrderID, 10)
oType, err := order.StringToOrderType(data.OrderType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
if newdata, ok := multiStreamData["data"].(map[string]interface{}); ok {
if e, ok := newdata["e"].(string); ok {
switch e {
case "outboundAccountInfo":
var data wsAccountInfo
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to outboundAccountInfo structure %s",
b.Name,
err)
}
}
var oSide order.Side
oSide, err = order.StringToOrderSide(data.Side)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
b.Websocket.DataHandler <- data
case "outboundAccountPosition":
var data wsAccountPosition
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to outboundAccountPosition structure %s",
b.Name,
err)
}
}
var oStatus order.Status
oStatus, err = stringToOrderStatus(data.CurrentExecutionType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
b.Websocket.DataHandler <- data
case "balanceUpdate":
var data wsBalanceUpdate
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to balanceUpdate structure %s",
b.Name,
err)
}
b.Websocket.DataHandler <- data
case "executionReport":
var data wsOrderUpdate
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to executionReport structure %s",
b.Name,
err)
}
var orderID = strconv.FormatInt(data.Data.OrderID, 10)
oType, err := order.StringToOrderType(data.Data.OrderType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
var oSide order.Side
oSide, err = order.StringToOrderSide(data.Data.Side)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
var oStatus order.Status
oStatus, err = stringToOrderStatus(data.Data.CurrentExecutionType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
var p currency.Pair
var a asset.Item
p, a, err = b.GetRequestFormattedPairAndAssetType(data.Data.Symbol)
if err != nil {
return err
}
b.Websocket.DataHandler <- &order.Detail{
Price: data.Data.Price,
Amount: data.Data.Quantity,
ExecutedAmount: data.Data.CumulativeFilledQuantity,
RemainingAmount: data.Data.Quantity - data.Data.CumulativeFilledQuantity,
Exchange: b.Name,
ID: orderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: time.Unix(0, data.Data.OrderCreationTime*int64(time.Millisecond)),
Pair: p,
}
case "listStatus":
var data wsListStatus
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to listStatus structure %s",
b.Name,
err)
}
b.Websocket.DataHandler <- data
}
var p currency.Pair
var a asset.Item
p, a, err = b.GetRequestFormattedPairAndAssetType(data.Symbol)
if err != nil {
return err
}
b.Websocket.DataHandler <- &order.Detail{
Price: data.Price,
Amount: data.Quantity,
ExecutedAmount: data.CumulativeFilledQuantity,
RemainingAmount: data.Quantity - data.CumulativeFilledQuantity,
Exchange: b.Name,
ID: orderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: time.Unix(0, data.OrderCreationTime*int64(time.Millisecond)),
Pair: p,
}
case "listStatus":
var data wsListStauts
err := json.Unmarshal(respRaw, &data)
if err != nil {
return fmt.Errorf("%v - Could not convert to listStatus structure %s",
b.Name,
err)
}
b.Websocket.DataHandler <- data
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
return nil
}
}
if stream, ok := multiStreamData["stream"].(string); ok {
streamType := strings.Split(stream, "@")
if wsStream, ok := multiStreamData["stream"].(string); ok {
streamType := strings.Split(wsStream, "@")
if len(streamType) > 1 {
if data, ok := multiStreamData["data"]; ok {
rawData, err := json.Marshal(data)
if err != nil {
return err
}
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
format, err := b.GetPairFormat(asset.Spot, true)
if err != nil {
return err
}
switch streamType[1] {
case "trade":
var trade TradeStream
@@ -293,14 +286,18 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
err)
}
b.Websocket.DataHandler <- wshandler.TradeData{
CurrencyPair: currency.NewPairFromFormattedPairs(trade.Symbol, b.GetEnabledPairs(asset.Spot),
b.GetPairFormat(asset.Spot, true)),
Timestamp: time.Unix(0, trade.TimeStamp*int64(time.Millisecond)),
Price: price,
Amount: amount,
Exchange: b.Name,
AssetType: asset.Spot,
pair, err := currency.NewPairFromFormattedPairs(trade.Symbol, pairs, format)
if err != nil {
return err
}
b.Websocket.DataHandler <- stream.TradeData{
CurrencyPair: pair,
Timestamp: time.Unix(0, trade.TimeStamp*int64(time.Millisecond)),
Price: price,
Amount: amount,
Exchange: b.Name,
AssetType: asset.Spot,
}
case "ticker":
var t TickerStream
@@ -311,6 +308,11 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
err.Error())
}
pair, err := currency.NewPairFromFormattedPairs(t.Symbol, pairs, format)
if err != nil {
return err
}
b.Websocket.DataHandler <- &ticker.Price{
ExchangeName: b.Name,
Open: t.OpenPrice,
@@ -324,8 +326,7 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
Last: t.LastPrice,
LastUpdated: time.Unix(0, t.EventTime*int64(time.Millisecond)),
AssetType: asset.Spot,
Pair: currency.NewPairFromFormattedPairs(t.Symbol, b.GetEnabledPairs(asset.Spot),
b.GetPairFormat(asset.Spot, true)),
Pair: pair,
}
case "kline_1m", "kline_3m", "kline_5m", "kline_15m", "kline_30m", "kline_1h", "kline_2h", "kline_4h",
"kline_6h", "kline_8h", "kline_12h", "kline_1d", "kline_3d", "kline_1w", "kline_1M":
@@ -337,10 +338,14 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
err)
}
b.Websocket.DataHandler <- wshandler.KlineData{
Timestamp: time.Unix(0, kline.EventTime*int64(time.Millisecond)),
Pair: currency.NewPairFromFormattedPairs(kline.Symbol, b.GetEnabledPairs(asset.Spot),
b.GetPairFormat(asset.Spot, true)),
pair, err := currency.NewPairFromFormattedPairs(kline.Symbol, pairs, format)
if err != nil {
return err
}
b.Websocket.DataHandler <- stream.KlineData{
Timestamp: time.Unix(0, kline.EventTime*int64(time.Millisecond)),
Pair: pair,
AssetType: asset.Spot,
Exchange: b.Name,
StartTime: time.Unix(0, kline.Kline.StartTime*int64(time.Millisecond)),
@@ -361,22 +366,16 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
err)
}
err = b.UpdateLocalCache(&depth)
err = b.UpdateLocalBuffer(&depth)
if err != nil {
return fmt.Errorf("%v - UpdateLocalCache error: %s",
b.Name,
err)
}
currencyPair := currency.NewPairFromFormattedPairs(depth.Pair, b.GetEnabledPairs(asset.Spot),
b.GetPairFormat(asset.Spot, true))
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: currencyPair,
Asset: asset.Spot,
Exchange: b.Name,
}
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{
Message: b.Name + stream.UnhandledMessage + string(respRaw),
}
}
}
}
@@ -403,11 +402,15 @@ func stringToOrderStatus(status string) (order.Status, error) {
// SeedLocalCache seeds depth data
func (b *Binance) SeedLocalCache(p currency.Pair) error {
ob, err := b.GetOrderBook(
OrderBookDataRequestParams{
Symbol: b.FormatExchangeCurrency(p, asset.Spot).String(),
Limit: 1000,
})
fPair, err := b.FormatExchangeCurrency(p, asset.Spot)
if err != nil {
return err
}
ob, err := b.GetOrderBook(OrderBookDataRequestParams{
Symbol: fPair.String(),
Limit: 1000,
})
if err != nil {
return err
}
@@ -439,11 +442,34 @@ func (b *Binance) SeedLocalCacheWithBook(p currency.Pair, orderbookNew *OrderBoo
return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
}
// UpdateLocalCache updates and returns the most recent iteration of the orderbook
func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error {
currencyPair := currency.NewPairFromFormattedPairs(wsdp.Pair, b.GetEnabledPairs(asset.Spot),
b.GetPairFormat(asset.Spot, true))
// UpdateLocalBuffer updates and returns the most recent iteration of the orderbook
func (b *Binance) UpdateLocalBuffer(wsdp *WebsocketDepthStream) error {
enabledPairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
format, err := b.GetPairFormat(asset.Spot, true)
if err != nil {
return err
}
currencyPair, err := currency.NewPairFromFormattedPairs(wsdp.Pair,
enabledPairs,
format)
if err != nil {
return err
}
currentBook := b.Websocket.Orderbook.GetOrderbook(currencyPair, asset.Spot)
if currentBook == nil {
// Used when a pair/s is enabled while connected
err = b.SeedLocalCache(currencyPair)
if err != nil {
return err
}
currentBook = b.Websocket.Orderbook.GetOrderbook(currencyPair, asset.Spot)
}
// Drop any event where u is <= lastUpdateId in the snapshot.
// The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1.
@@ -479,7 +505,7 @@ func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error {
updateAsk = append(updateAsk, orderbook.Item{Price: p, Amount: a})
}
return b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
return b.Websocket.Orderbook.Update(&buffer.Update{
Bids: updateBid,
Asks: updateAsk,
Pair: currencyPair,
@@ -487,3 +513,62 @@ func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error {
Asset: asset.Spot,
})
}
// GenerateSubscriptions generates the default subscription set
func (b *Binance) GenerateSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{"@ticker", "@trade", "@kline_1m", "@depth@100ms"}
var subscriptions []stream.ChannelSubscription
assets := b.GetAssetTypes()
for x := range assets {
pairs, err := b.GetEnabledPairs(assets[x])
if err != nil {
return nil, err
}
for y := range pairs {
for z := range channels {
lp := pairs[y].Lower()
lp.Delimiter = ""
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: lp.String() + channels[z],
Currency: pairs[y],
Asset: assets[x],
})
}
}
}
return subscriptions, nil
}
// Subscribe subscribes to a set of channels
func (b *Binance) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
payload := WsPayload{
Method: "SUBSCRIBE",
}
for i := range channelsToSubscribe {
payload.Params = append(payload.Params, channelsToSubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(payload)
if err != nil {
return err
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
return nil
}
// Unsubscribe unsubscribes from a set of channels
func (b *Binance) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
payload := WsPayload{
Method: "UNSUBSCRIBE",
}
for i := range channelsToUnsubscribe {
payload.Params = append(payload.Params, channelsToUnsubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(payload)
if err != nil {
return err
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...)
return nil
}

View File

@@ -18,8 +18,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -56,21 +56,23 @@ func (b *Binance) SetDefaults() {
b.API.CredentialsValidator.RequiresSecret = true
b.SetValues()
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
},
fmt1 := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
ConfigFormat: &currency.PairFormat{
Delimiter: "-",
Delimiter: currency.DashDelimiter,
Uppercase: true,
},
}
err := b.StoreAssetPairFormat(asset.Spot, fmt1)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = b.StoreAssetPairFormat(asset.Margin, fmt1)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
@@ -145,7 +147,7 @@ func (b *Binance) SetDefaults() {
b.API.Endpoints.URLDefault = apiURL
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
b.Websocket = wshandler.New()
b.Websocket = stream.New()
b.API.Endpoints.WebsocketURL = binanceDefaultWebsocketURL
b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
@@ -164,40 +166,31 @@ func (b *Binance) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: binanceDefaultWebsocketURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Features: &b.Features.Supports.WebsocketCapabilities,
})
err = b.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: binanceDefaultWebsocketURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
GenerateSubscriptions: b.GenerateSubscriptions,
Features: &b.Features.Supports.WebsocketCapabilities,
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
SortBuffer: true,
SortBufferByUpdateIDs: true,
})
if err != nil {
return err
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: b.Websocket.GetWebsocketURL(),
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
false,
true,
true,
false,
exch.Name)
return nil
})
}
// Start starts the Binance go routine
@@ -221,30 +214,59 @@ func (b *Binance) Run() {
}
forceUpdate := false
delim := b.GetPairFormat(asset.Spot, false).Delimiter
if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), delim) ||
!common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), delim) {
enabledPairs := currency.NewPairsFromStrings(
[]string{currency.BTC.String() + delim + currency.USDT.String()},
)
log.Warn(log.ExchangeSys,
"Available pairs for Binance reset due to config upgrade, please enable the ones you would like to use again")
forceUpdate = true
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to get enabled currencies. Err %s\n",
b.Name,
err)
return
}
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to get enabled currencies. Err %s\n",
b.Name,
err)
return
}
err := b.UpdatePairs(enabledPairs, asset.Spot, true, true)
avail, err := b.GetAvailablePairs(asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to get available currencies. Err %s\n",
b.Name,
err)
return
}
if !common.StringDataContains(pairs.Strings(), format.Delimiter) ||
!common.StringDataContains(avail.Strings(), format.Delimiter) {
var enabledPairs currency.Pairs
enabledPairs, err = currency.NewPairsFromStrings([]string{
currency.BTC.String() +
format.Delimiter +
currency.USDT.String()})
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err %s\n",
b.Name,
err)
} else {
log.Warn(log.ExchangeSys,
"Available pairs for Binance reset due to config upgrade, please enable the ones you would like to use again")
forceUpdate = true
err = b.UpdatePairs(enabledPairs, asset.Spot, true, true)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
}
}
}
if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate {
return
}
err := b.UpdateTradablePairs(forceUpdate)
err = b.UpdateTradablePairs(forceUpdate)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
@@ -254,36 +276,55 @@ func (b *Binance) Run() {
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (b *Binance) FetchTradablePairs(asset asset.Item) ([]string, error) {
var validCurrencyPairs []string
func (b *Binance) FetchTradablePairs(a asset.Item) ([]string, error) {
info, err := b.GetExchangeInfo()
if err != nil {
return nil, err
}
format, err := b.GetPairFormat(a, false)
if err != nil {
return nil, err
}
var pairs []string
for x := range info.Symbols {
if info.Symbols[x].Status == "TRADING" {
validCurrencyPairs = append(validCurrencyPairs, info.Symbols[x].BaseAsset+
b.GetPairFormat(asset, false).Delimiter+
info.Symbols[x].QuoteAsset)
pair := info.Symbols[x].BaseAsset +
format.Delimiter +
info.Symbols[x].QuoteAsset
if a == asset.Spot && info.Symbols[x].IsSpotTradingAllowed {
pairs = append(pairs, pair)
}
if a == asset.Margin && info.Symbols[x].IsMarginTradingAllowed {
pairs = append(pairs, pair)
}
}
}
return validCurrencyPairs, nil
return pairs, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (b *Binance) UpdateTradablePairs(forceUpdate bool) error {
pairs, err := b.FetchTradablePairs(asset.Spot)
if err != nil {
return err
}
assetTypes := b.GetAssetTypes()
for i := range assetTypes {
p, err := b.FetchTradablePairs(assetTypes[i])
if err != nil {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs),
asset.Spot,
false,
forceUpdate)
pairs, err := currency.NewPairsFromStrings(p)
if err != nil {
return err
}
err = b.UpdatePairs(pairs, assetTypes[i], false, forceUpdate)
if err != nil {
return err
}
}
return nil
}
// UpdateTicker updates and returns the ticker for a currency pair
@@ -292,28 +333,39 @@ func (b *Binance) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.P
if err != nil {
return nil, err
}
pairs := b.GetEnabledPairs(assetType)
pairs, err := b.GetEnabledPairs(assetType)
if err != nil {
return nil, err
}
for i := range pairs {
for y := range tick {
pairFmt := b.FormatExchangeCurrency(pairs[i], assetType).String()
if tick[y].Symbol != pairFmt {
pairFmt, err := b.FormatExchangeCurrency(pairs[i], assetType)
if err != nil {
return nil, err
}
if tick[y].Symbol != pairFmt.String() {
continue
}
tickerPrice := &ticker.Price{
Last: tick[y].LastPrice,
High: tick[y].HighPrice,
Low: tick[y].LowPrice,
Bid: tick[y].BidPrice,
Ask: tick[y].AskPrice,
Volume: tick[y].Volume,
QuoteVolume: tick[y].QuoteVolume,
Open: tick[y].OpenPrice,
Close: tick[y].PrevClosePrice,
Pair: pairs[i],
}
err = ticker.ProcessTicker(b.Name, tickerPrice, assetType)
err = ticker.ProcessTicker(&ticker.Price{
Last: tick[y].LastPrice,
High: tick[y].HighPrice,
Low: tick[y].LowPrice,
Bid: tick[y].BidPrice,
Ask: tick[y].AskPrice,
Volume: tick[y].Volume,
QuoteVolume: tick[y].QuoteVolume,
Open: tick[y].OpenPrice,
Close: tick[y].PrevClosePrice,
Pair: pairs[i],
ExchangeName: b.Name,
AssetType: assetType,
})
if err != nil {
log.Error(log.Ticker, err)
return nil, err
}
}
}
@@ -340,13 +392,19 @@ func (b *Binance) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *Binance) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
orderbookNew, err := b.GetOrderBook(OrderBookDataRequestParams{Symbol: b.FormatExchangeCurrency(p,
assetType).String(), Limit: 1000})
fpair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return orderBook, err
return nil, err
}
orderbookNew, err := b.GetOrderBook(OrderBookDataRequestParams{
Symbol: fpair.String(),
Limit: 1000})
if err != nil {
return nil, err
}
orderBook := new(orderbook.Base)
for x := range orderbookNew.Bids {
orderBook.Bids = append(orderBook.Bids,
orderbook.Item{
@@ -499,8 +557,12 @@ func (b *Binance) CancelOrder(order *order.Cancel) error {
return err
}
_, err = b.CancelExistingOrder(b.FormatExchangeCurrency(order.Pair,
order.AssetType).String(),
fpair, err := b.FormatExchangeCurrency(order.Pair, order.AssetType)
if err != nil {
return err
}
_, err = b.CancelExistingOrder(fpair.String(),
orderIDInt,
order.AccountID)
return err
@@ -567,11 +629,6 @@ func (b *Binance) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Binance) GetWebsocket() (*wshandler.Websocket, error) {
return b.Websocket, nil
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (b *Binance) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
if (!b.AllowAuthenticatedRequest() || b.SkipAuthCheck) && // Todo check connection status
@@ -589,8 +646,13 @@ func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
var orders []order.Detail
for x := range req.Pairs {
resp, err := b.OpenOrders(b.FormatExchangeCurrency(req.Pairs[x],
asset.Spot).String())
fpair, err := b.FormatExchangeCurrency(req.Pairs[x],
asset.Spot)
if err != nil {
return nil, err
}
resp, err := b.OpenOrders(fpair.String())
if err != nil {
return nil, err
}
@@ -600,6 +662,11 @@ func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
orderType := order.Type(strings.ToUpper(resp[i].Type))
orderDate := time.Unix(0, int64(resp[i].Time)*int64(time.Millisecond))
pair, err := currency.NewPairFromString(resp[i].Symbol)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
Amount: resp[i].OrigQty,
Date: orderDate,
@@ -609,7 +676,7 @@ func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
Type: orderType,
Price: resp[i].Price,
Status: order.Status(resp[i].Status),
Pair: currency.NewPairFromString(resp[i].Symbol),
Pair: pair,
})
}
}
@@ -629,8 +696,11 @@ func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
var orders []order.Detail
for x := range req.Pairs {
resp, err := b.AllOrders(b.FormatExchangeCurrency(req.Pairs[x],
asset.Spot).String(),
fpair, err := b.FormatExchangeCurrency(req.Pairs[x], asset.Spot)
if err != nil {
return nil, err
}
resp, err := b.AllOrders(fpair.String(),
"",
"1000")
if err != nil {
@@ -646,6 +716,11 @@ func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
continue
}
pair, err := currency.NewPairFromString(resp[i].Symbol)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
Amount: resp[i].OrigQty,
Date: orderDate,
@@ -654,7 +729,7 @@ func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
Side: orderSide,
Type: orderType,
Price: resp[i].Price,
Pair: currency.NewPairFromString(resp[i].Symbol),
Pair: pair,
Status: order.Status(resp[i].Status),
})
}
@@ -666,28 +741,6 @@ func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Binance) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Binance) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Binance) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Binance) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *Binance) ValidateCredentials() error {
@@ -718,9 +771,13 @@ func (b *Binance) GetHistoricCandles(pair currency.Pair, a asset.Item, start, en
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
fpair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
req := KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(interval),
Symbol: b.FormatExchangeCurrency(pair, a).String(),
Symbol: fpair.String(),
StartTime: start.Unix() * 1000,
EndTime: end.Unix() * 1000,
Limit: int(b.Features.Enabled.Kline.ResultLimit),
@@ -768,11 +825,16 @@ func (b *Binance) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, s
Interval: interval,
}
formattedPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
for x := range dates {
req := KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(interval),
Symbol: b.FormatExchangeCurrency(pair, a).String(),
Symbol: formattedPair.String(),
StartTime: dates[x].Start.UTC().Unix() * 1000,
EndTime: dates[x].End.UTC().Unix() * 1000,
Limit: int(b.Features.Enabled.Kline.ResultLimit),

View File

@@ -17,7 +17,6 @@ import (
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -83,9 +82,7 @@ const (
// Bitfinex is the overarching type across the bitfinex package
type Bitfinex struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
AuthenticatedWebsocketConn *wshandler.WebsocketConnection
WebsocketSubdChannels map[int]WebsocketChanInfo
WebsocketSubdChannels map[int]WebsocketChanInfo
}
// GetPlatformStatus returns the Bifinex platform status
@@ -778,7 +775,7 @@ func (b *Bitfinex) WithdrawFIAT(withdrawalType, walletType string, withdrawReque
// Major Upgrade needed on this function to include all query params
func (b *Bitfinex) NewOrder(currencyPair, orderType string, amount, price float64, buy, hidden bool) (Order, error) {
if !common.StringDataCompare(AcceptedOrderType, orderType) {
return Order{}, errors.New("order type not accepted")
return Order{}, fmt.Errorf("order type %s not accepted", orderType)
}
response := Order{}

View File

@@ -16,7 +16,6 @@ import (
"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/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -42,6 +41,7 @@ func TestMain(m *testing.M) {
if err != nil {
log.Fatal("Bitfinex Setup() init error")
}
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(bfxConfig)
if err != nil {
log.Fatal("Bitfinex setup error", err)
@@ -58,11 +58,32 @@ func TestMain(m *testing.M) {
b.API.AuthenticatedWebsocketSupport = true
}
b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo)
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
os.Exit(m.Run())
}
func TestAppendOptionalDelimiter(t *testing.T) {
t.Parallel()
curr1, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
b.appendOptionalDelimiter(&curr1)
if curr1.Delimiter != "" {
t.Errorf("Expected no delimiter, received %v", curr1.Delimiter)
}
curr2, err := currency.NewPairFromString("DUSK:USD")
if err != nil {
t.Fatal(err)
}
curr2.Delimiter = ""
b.appendOptionalDelimiter(&curr2)
if curr2.Delimiter != ":" {
t.Errorf("Expected \":\" as a delimiter, received %v", curr2.Delimiter)
}
}
func TestGetPlatformStatus(t *testing.T) {
t.Parallel()
result, err := b.GetPlatformStatus()
@@ -320,7 +341,7 @@ func TestNewOrder(t *testing.T) {
_, err := b.NewOrder("BTCUSD",
order.Limit.Lower(),
1,
-1,
2,
false,
true)
@@ -330,27 +351,17 @@ func TestNewOrder(t *testing.T) {
}
func TestUpdateTicker(t *testing.T) {
_, err := b.UpdateTicker(currency.NewPairFromString("BTCUSD"), asset.Spot)
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
_, err = b.UpdateTicker(pair, asset.Spot)
if err != nil {
t.Error(err)
}
}
func TestAppendOptionalDelimiter(t *testing.T) {
t.Parallel()
curr1 := currency.NewPairFromString("BTCUSD")
b.appendOptionalDelimiter(&curr1)
if curr1.Delimiter != "" {
t.Errorf("Expected no delimiter, received %v", curr1.Delimiter)
}
curr2 := currency.NewPairFromString("DUSK:USD")
curr2.Delimiter = ""
b.appendOptionalDelimiter(&curr2)
if curr2.Delimiter != ":" {
t.Errorf("Expected \"-\" as a delimiter, received %v", curr2.Delimiter)
}
}
func TestNewOrderMulti(t *testing.T) {
if !b.ValidateAPICredentials() {
t.SkipNow()
@@ -762,19 +773,20 @@ func TestSubmitOrder(t *testing.T) {
var orderSubmission = &order.Submit{
Pair: currency.Pair{
Delimiter: "_",
Base: currency.BTC,
Base: currency.XRP,
Quote: currency.USD,
},
Side: order.Buy,
Type: order.Limit,
Price: 1,
Amount: 1,
ClientID: "meowOrder",
AssetType: asset.Spot,
Side: order.Sell,
Type: order.Limit,
Price: 1000,
Amount: 20,
ClientID: "meowOrder",
}
response, err := b.SubmitOrder(orderSubmission)
if areTestAPIKeysSet() && err != nil {
t.Errorf("Could not cancel orders: %v", err)
t.Errorf("Could not place order: %v", err)
}
if areTestAPIKeysSet() && !response.IsOrderPlaced {
t.Error("Order not placed")
@@ -944,21 +956,12 @@ func TestGetDepositAddress(t *testing.T) {
}
func setupWs() {
b.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: authenticatedBitfinexWebsocketEndpoint,
Verbose: b.Verbose,
ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit,
ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout,
}
var dialer websocket.Dialer
err := b.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.AuthConn.Dial(&dialer, http.Header{})
if err != nil {
log.Fatal(err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go b.wsReadData(b.AuthenticatedWebsocketConn)
go b.wsReadData(b.Websocket.AuthConn)
go b.WsDataHandler()
}
@@ -1001,12 +1004,13 @@ func TestWsPlaceOrder(t *testing.T) {
if !wsAuthExecuted {
runAuth(t)
}
_, err := b.WsNewOrder(&WsNewOrderRequest{
CustomID: 1337,
Type: order.Buy.String(),
Symbol: "tBTCUSD",
Amount: 10,
Price: -10,
GroupID: 1,
Type: "EXCHANGE LIMIT",
Symbol: "tXRPUSD",
Amount: -20,
Price: 1000,
})
if err != nil {
t.Error(err)
@@ -1169,6 +1173,24 @@ func TestWsTickerResponse(t *testing.T) {
if err != nil {
t.Error(err)
}
b.WebsocketSubdChannels[123412] = WebsocketChanInfo{Pair: "XAUTF0:USTF0", Channel: wsTicker}
pressXToJSON = `[123412,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]`
err = b.wsHandleData([]byte(pressXToJSON))
if err != nil {
t.Error(err)
}
b.WebsocketSubdChannels[123413] = WebsocketChanInfo{Pair: "trade:1m:tXRPUSD", Channel: wsTicker}
pressXToJSON = `[123413,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]`
err = b.wsHandleData([]byte(pressXToJSON))
if err != nil {
t.Error(err)
}
b.WebsocketSubdChannels[123414] = WebsocketChanInfo{Pair: "trade:1m:fZRX:p30", Channel: wsTicker}
pressXToJSON = `[123414,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]`
err = b.wsHandleData([]byte(pressXToJSON))
if err != nil {
t.Error(err)
}
}
func TestWsCandleResponse(t *testing.T) {
@@ -1186,6 +1208,7 @@ func TestWsCandleResponse(t *testing.T) {
}
func TestWsOrderSnapshot(t *testing.T) {
b.WsAddSubscriptionChannel(0, "account", "N/A")
pressXToJSON := `[0,"os",[[34930659963,null,1574955083558,"tETHUSD",1574955083558,1574955083573,0.201104,0.201104,"EXCHANGE LIMIT",null,null,null,0,"ACTIVE",null,null,120,0,0,0,null,null,null,0,0,null,null,null,"BFX",null,null,null]]]`
err := b.wsHandleData([]byte(pressXToJSON))
if err != nil {
@@ -1213,9 +1236,12 @@ func TestWsNotifications(t *testing.T) {
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSD")
currencyPair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
_, err = b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
@@ -1227,9 +1253,12 @@ func TestGetHistoricCandles(t *testing.T) {
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("TBTCUSD")
currencyPair, err := currency.NewPairFromString("TBTCUSD")
if err != nil {
t.Fatal(err)
}
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneHour)
_, err = b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
@@ -1241,42 +1270,95 @@ func TestGetHistoricCandlesExtended(t *testing.T) {
}
func TestFixCasing(t *testing.T) {
ret := b.fixCasing(currency.NewPairFromString("BTCUSD"), asset.Spot)
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
ret, err := b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("TBTCUSD"), asset.Spot)
pair, err = currency.NewPairFromString("TBTCUSD")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("tBTCUSD"), asset.Spot)
pair, err = currency.NewPairFromString("tBTCUSD")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("BTCUSD"), asset.Margin)
pair, err = currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Margin)
if err != nil {
t.Fatal(err)
}
if ret != "fBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("BTCUSD"), asset.Spot)
pair, err = currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("FUNETH"), asset.Spot)
pair, err = currency.NewPairFromString("FUNETH")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if ret != "tFUNETH" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("TNBUSD"), asset.Spot)
pair, err = currency.NewPairFromString("TNBUSD")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if ret != "tTNBUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("tTNBUSD"), asset.Spot)
pair, err = currency.NewPairFromString("tTNBUSD")
if err != nil {
t.Fatal(err)
}
ret, err = b.fixCasing(pair, asset.Spot)
if err != nil {
t.Fatal(err)
}
if ret != "tTNBUSD" {
t.Errorf("unexpected result: %v", ret)
}

View File

@@ -374,9 +374,11 @@ type WebsocketChanInfo struct {
// WebsocketBook holds booking information
type WebsocketBook struct {
Price float64
ID int64
Price float64
Amount float64
Rate float64
Period int64
}
// WebsocketTrade holds trade information

View File

@@ -10,69 +10,74 @@ import (
"time"
"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/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
"github.com/thrasher-corp/gocryptotrader/log"
)
var comms = make(chan wshandler.WebsocketResponse)
var comms = make(chan stream.Response)
// WsConnect starts a new websocket connection
func (b *Bitfinex) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.WebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return fmt.Errorf("%v unable to connect to Websocket. Error: %s", b.Name, err)
return fmt.Errorf("%v unable to connect to Websocket. Error: %s",
b.Name,
err)
}
go b.wsReadData(b.WebsocketConn)
go b.wsReadData(b.Websocket.Conn)
if b.Websocket.CanUseAuthenticatedEndpoints() {
err = b.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{})
err = b.Websocket.AuthConn.Dial(&dialer, http.Header{})
if err != nil {
log.Errorf(log.ExchangeSys, "%v unable to connect to authenticated Websocket. Error: %s", b.Name, err)
log.Errorf(log.ExchangeSys,
"%v unable to connect to authenticated Websocket. Error: %s",
b.Name,
err)
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
go b.wsReadData(b.AuthenticatedWebsocketConn)
go b.wsReadData(b.Websocket.AuthConn)
err = b.WsSendAuth()
if err != nil {
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err)
log.Errorf(log.ExchangeSys,
"%v - authentication failed: %v\n",
b.Name,
err)
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
b.GenerateDefaultSubscriptions()
subs, err := b.GenerateDefaultSubscriptions()
if err != nil {
return err
}
go b.WsDataHandler()
return nil
return b.Websocket.SubscribeToChannels(subs)
}
// wsReadData receives and passes on websocket messages for processing
func (b *Bitfinex) wsReadData(ws *wshandler.WebsocketConnection) {
func (b *Bitfinex) wsReadData(ws stream.Connection) {
b.Websocket.Wg.Add(1)
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
resp := ws.ReadMessage()
if resp.Raw == nil {
return
default:
resp, err := ws.ReadMessage()
if err != nil {
b.Websocket.ReadMessageErrors <- err
return
}
b.Websocket.TrafficAlert <- struct{}{}
comms <- resp
}
comms <- resp
}
}
@@ -82,8 +87,6 @@ func (b *Bitfinex) WsDataHandler() {
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
return
case resp := <-comms:
if resp.Type == websocket.TextMessage {
err := b.wsHandleData(resp.Raw)
@@ -91,6 +94,8 @@ func (b *Bitfinex) WsDataHandler() {
b.Websocket.DataHandler <- err
}
}
case <-b.Websocket.ShutdownC:
return
}
}
}
@@ -106,12 +111,21 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
event := d["event"]
switch event {
case "subscribed":
if symbol, ok := d["pair"].(string); ok {
if symbol, ok := d["symbol"].(string); ok {
b.WsAddSubscriptionChannel(int(d["chanId"].(float64)),
d["channel"].(string),
symbol,
)
} else if key, ok := d["key"].(string); ok {
// Capture trading subscriptions
contents := strings.Split(d["key"].(string), ":")
if len(contents) > 3 {
// Edge case to parse margin strings.
// map[chanId:139136 channel:candles event:subscribed key:trade:1m:tXAUTF0:USTF0]
if contents[2][0] == 't' {
key = contents[2] + ":" + contents[3]
}
}
b.WsAddSubscriptionChannel(int(d["chanId"].(float64)),
d["channel"].(string),
key,
@@ -134,6 +148,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
return nil
}
}
chanID := int(d[0].(float64))
chanInfo, ok := b.WebsocketSubdChannels[chanID]
if !ok && chanID != 0 {
@@ -141,67 +156,122 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
chanID)
}
var chanAsset = asset.Spot
var pair currency.Pair
pairInfo := strings.Split(chanInfo.Pair, ":")
switch {
case len(pairInfo) >= 3:
newPair := pairInfo[2]
if newPair[0] == 'f' {
chanAsset = asset.MarginFunding
}
pair, err = currency.NewPairFromString(newPair[1:])
if err != nil {
return err
}
case len(pairInfo) == 1:
newPair := pairInfo[0]
if newPair[0] == 'f' {
chanAsset = asset.MarginFunding
}
pair, err = currency.NewPairFromString(newPair[1:])
if err != nil {
return err
}
case chanInfo.Pair != "":
if strings.Contains(chanInfo.Pair, ":") {
chanAsset = asset.Margin
}
pair, err = currency.NewPairFromString(chanInfo.Pair[1:])
if err != nil {
return err
}
}
switch chanInfo.Channel {
case wsBook:
var newOrderbook []WebsocketBook
curr := currency.NewPairFromString(chanInfo.Pair)
if obSnapBundle, ok := d[1].([]interface{}); ok {
switch id := obSnapBundle[0].(type) {
case []interface{}:
for i := range obSnapBundle {
data := obSnapBundle[i].([]interface{})
obSnapBundle, ok := d[1].([]interface{})
if !ok {
return errors.New("orderbook interface cast failed")
}
switch id := obSnapBundle[0].(type) {
case []interface{}:
for i := range obSnapBundle {
data := obSnapBundle[i].([]interface{})
if len(data) == 4 {
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(data[0].(float64)),
Period: int64(data[1].(float64)),
Rate: data[2].(float64),
Amount: data[3].(float64)})
} else {
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(data[0].(float64)),
Price: data[1].(float64),
Amount: data[2].(float64)})
}
err := b.WsInsertSnapshot(curr,
asset.Spot,
newOrderbook)
if err != nil {
return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s",
err)
}
case float64:
}
err := b.WsInsertSnapshot(pair, chanAsset, newOrderbook)
if err != nil {
return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s",
err)
}
case float64:
if len(obSnapBundle) == 4 {
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(id),
Period: int64(obSnapBundle[1].(float64)),
Rate: obSnapBundle[2].(float64),
Amount: obSnapBundle[3].(float64)})
} else {
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(id),
Price: obSnapBundle[1].(float64),
Amount: obSnapBundle[2].(float64)})
err := b.WsUpdateOrderbook(curr,
asset.Spot,
newOrderbook)
if err != nil {
return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s",
err)
}
}
err := b.WsUpdateOrderbook(pair, chanAsset, newOrderbook)
if err != nil {
return fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s",
err)
}
}
return nil
case wsCandles:
curr := currency.NewPairFromString(chanInfo.Pair)
if candleBundle, ok := d[1].([]interface{}); ok {
if len(candleBundle) == 0 {
return nil
}
switch candleData := candleBundle[0].(type) {
case []interface{}:
b.Websocket.DataHandler <- wshandler.KlineData{
Timestamp: time.Unix(0, int64(candleData[0].(float64))),
Exchange: b.Name,
AssetType: asset.Spot,
Pair: curr,
OpenPrice: candleData[1].(float64),
ClosePrice: candleData[2].(float64),
HighPrice: candleData[3].(float64),
LowPrice: candleData[4].(float64),
Volume: candleData[5].(float64),
for i := range candleBundle {
element := candleBundle[i].([]interface{})
b.Websocket.DataHandler <- stream.KlineData{
Timestamp: time.Unix(0, int64(element[0].(float64))*int64(time.Millisecond)),
Exchange: b.Name,
AssetType: chanAsset,
Pair: pair,
OpenPrice: element[1].(float64),
ClosePrice: element[2].(float64),
HighPrice: element[3].(float64),
LowPrice: element[4].(float64),
Volume: element[5].(float64),
}
}
case float64:
b.Websocket.DataHandler <- wshandler.KlineData{
Timestamp: time.Unix(0, int64(candleData)),
b.Websocket.DataHandler <- stream.KlineData{
Timestamp: time.Unix(0, int64(candleData)*int64(time.Millisecond)),
Exchange: b.Name,
AssetType: asset.Spot,
Pair: curr,
AssetType: chanAsset,
Pair: pair,
OpenPrice: candleBundle[1].(float64),
ClosePrice: candleBundle[2].(float64),
HighPrice: candleBundle[3].(float64),
@@ -221,8 +291,8 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
Volume: tickerData[7].(float64),
High: tickerData[8].(float64),
Low: tickerData[9].(float64),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(chanInfo.Pair),
AssetType: chanAsset,
Pair: pair,
}
return nil
case wsTrades:
@@ -233,22 +303,20 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
for i := range snapshot {
elem := snapshot[i].([]interface{})
if len(elem) == 5 {
trades = append(trades,
WebsocketTrade{
ID: int64(elem[0].(float64)),
Timestamp: int64(elem[1].(float64)),
Amount: elem[2].(float64),
Rate: elem[3].(float64),
Period: int64(elem[4].(float64)),
})
trades = append(trades, WebsocketTrade{
ID: int64(elem[0].(float64)),
Timestamp: int64(elem[1].(float64)),
Amount: elem[2].(float64),
Rate: elem[3].(float64),
Period: int64(elem[4].(float64)),
})
} else {
trades = append(trades,
WebsocketTrade{
ID: int64(elem[0].(float64)),
Timestamp: int64(elem[1].(float64)),
Amount: elem[2].(float64),
Price: elem[3].(float64),
})
trades = append(trades, WebsocketTrade{
ID: int64(elem[0].(float64)),
Timestamp: int64(elem[1].(float64)),
Amount: elem[2].(float64),
Price: elem[3].(float64),
})
}
}
case 3:
@@ -260,11 +328,22 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
return nil
}
data := d[2].([]interface{})
trades = append(trades, WebsocketTrade{
ID: int64(data[0].(float64)),
Timestamp: int64(data[1].(float64)),
Price: data[3].(float64),
Amount: data[2].(float64)})
if len(data) == 5 {
trades = append(trades, WebsocketTrade{
ID: int64(data[0].(float64)),
Timestamp: int64(data[1].(float64)),
Amount: data[2].(float64),
Rate: data[3].(float64),
Period: int64(data[4].(float64)),
})
} else {
trades = append(trades, WebsocketTrade{
ID: int64(data[0].(float64)),
Timestamp: int64(data[1].(float64)),
Amount: data[2].(float64),
Price: data[3].(float64),
})
}
}
for i := range trades {
@@ -276,29 +355,30 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
}
if trades[i].Rate > 0 {
b.Websocket.DataHandler <- wshandler.FundingData{
CurrencyPair: currency.NewPairFromString(chanInfo.Pair),
b.Websocket.DataHandler <- stream.FundingData{
CurrencyPair: pair,
Timestamp: time.Unix(0, trades[i].Timestamp*int64(time.Millisecond)),
Amount: newAmount,
Exchange: b.Name,
AssetType: asset.Spot,
AssetType: chanAsset,
Side: side,
Rate: trades[i].Rate,
Period: trades[i].Period,
}
return nil
continue
}
b.Websocket.DataHandler <- wshandler.TradeData{
CurrencyPair: currency.NewPairFromString(chanInfo.Pair),
b.Websocket.DataHandler <- stream.TradeData{
CurrencyPair: pair,
Timestamp: time.Unix(0, trades[i].Timestamp*int64(time.Millisecond)),
Price: trades[i].Price,
Amount: newAmount,
Exchange: b.Name,
AssetType: asset.Spot,
AssetType: chanAsset,
Side: side,
}
}
return nil
}
if authResp, ok := d[1].(string); ok {
@@ -315,8 +395,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
strings.Contains(channelName, wsFundingOrderCancelRequest):
if data[0] != nil && data[0].(float64) > 0 {
id := int64(data[0].(float64))
if b.WebsocketConn.IsIDWaitingForResponse(id) {
b.AuthenticatedWebsocketConn.SetResponseIDAndData(id, respRaw)
if b.Websocket.Match.IncomingWithData(id, respRaw) {
return nil
}
b.wsHandleFundingOffer(data)
@@ -326,19 +405,23 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
strings.Contains(channelName, wsOrderCancelRequest):
if data[2] != nil && data[2].(float64) > 0 {
id := int64(data[2].(float64))
if b.WebsocketConn.IsIDWaitingForResponse(id) {
b.AuthenticatedWebsocketConn.SetResponseIDAndData(id, respRaw)
if b.Websocket.Match.IncomingWithData(id, respRaw) {
return nil
}
b.wsHandleOrder(data)
}
default:
return fmt.Errorf("%s - Unexpected data returned %s", b.Name, respRaw)
return fmt.Errorf("%s - Unexpected data returned %s",
b.Name,
respRaw)
}
}
if notification[5] != nil && strings.EqualFold(notification[5].(string), wsError) {
return fmt.Errorf("%s - Error %s", b.Name, notification[6].(string))
if notification[5] != nil &&
strings.EqualFold(notification[5].(string), wsError) {
return fmt.Errorf("%s - Error %s",
b.Name,
notification[6].(string))
}
case wsOrderSnapshot:
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
@@ -634,7 +717,9 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error {
}
}
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{
Message: b.Name + stream.UnhandledMessage + string(respRaw),
}
return nil
}
}
@@ -781,20 +866,13 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books
newOrderBook.Pair = p
newOrderBook.ExchangeName = b.Name
err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
if err != nil {
return fmt.Errorf("bitfinex.go error - %s", err)
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p,
Asset: assetType,
Exchange: b.Name}
return nil
return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
}
// WsUpdateOrderbook updates the orderbook list, removing and adding to the
// orderbook sides
func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook) error {
orderbookUpdate := wsorderbook.WebsocketOrderbookUpdate{
orderbookUpdate := buffer.Update{
Asset: assetType,
Pair: p,
}
@@ -833,97 +911,115 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book
}
}
}
err := b.Websocket.Orderbook.Update(&orderbookUpdate)
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p,
Asset: assetType,
Exchange: b.Name}
return nil
return b.Websocket.Orderbook.Update(&orderbookUpdate)
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitfinex) GenerateDefaultSubscriptions() {
func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{
wsBook,
wsTrades,
wsTicker,
wsCandles,
}
var subscriptions []wshandler.WebsocketChannelSubscription
for i := range channels {
enabledPairs := b.GetEnabledPairs(asset.Spot)
for j := range enabledPairs {
if strings.HasPrefix(enabledPairs[j].Base.String(), "f") {
log.Warnf(log.WebsocketMgr,
"%v - Websocket does not support funding currency %v, skipping",
b.Name, enabledPairs[j])
continue
}
b.appendOptionalDelimiter(&enabledPairs[j])
params := make(map[string]interface{})
if channels[i] == wsBook {
params["prec"] = "R0"
params["len"] = "100"
}
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[i],
Currency: enabledPairs[j],
Params: params,
})
var subscriptions []stream.ChannelSubscription
assets := b.GetAssetTypes()
for i := range assets {
enabledPairs, err := b.GetEnabledPairs(assets[i])
if err != nil {
return nil, err
}
for j := range channels {
for k := range enabledPairs {
params := make(map[string]interface{})
if channels[j] == wsBook {
params["prec"] = "R0"
params["len"] = "100"
}
if channels[j] == wsCandles {
// TODO: Add ability to select timescale && funding period
var fundingPeriod string
prefix := "t"
if assets[i] == asset.MarginFunding {
prefix = "f"
fundingPeriod = ":p30"
}
params["key"] = "trade:1m:" + prefix + enabledPairs[k].String() + fundingPeriod
} else {
params["symbol"] = enabledPairs[k].String()
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[j],
Currency: enabledPairs[k],
Params: params,
Asset: assets[i],
})
}
}
}
b.Websocket.SubscribeToChannels(subscriptions)
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *Bitfinex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
req := make(map[string]interface{})
req["event"] = "subscribe"
req["channel"] = channelToSubscribe.Channel
func (b *Bitfinex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var errs common.Errors
for i := range channelsToSubscribe {
req := make(map[string]interface{})
req["event"] = "subscribe"
req["channel"] = channelsToSubscribe[i].Channel
if channelToSubscribe.Currency.String() != "" {
if channelToSubscribe.Channel == wsCandles {
// TODO: Add ability to select timescale
req["key"] = fmt.Sprintf("trade:1D:%v",
b.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String())
} else {
req["symbol"] = b.FormatExchangeCurrency(channelToSubscribe.Currency,
asset.Spot).String()
}
}
if len(channelToSubscribe.Params) > 0 {
for k, v := range channelToSubscribe.Params {
for k, v := range channelsToSubscribe[i].Params {
req[k] = v
}
}
return b.WebsocketConn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(req)
if err != nil {
errs = append(errs, err)
continue
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
}
if errs != nil {
return errs
}
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitfinex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
req := make(map[string]interface{})
req["event"] = "unsubscribe"
req["channel"] = channelToSubscribe.Channel
func (b *Bitfinex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var errs common.Errors
for i := range channelsToUnsubscribe {
req := make(map[string]interface{})
req["event"] = "unsubscribe"
req["channel"] = channelsToUnsubscribe[i].Channel
if len(channelToSubscribe.Params) > 0 {
for k, v := range channelToSubscribe.Params {
for k, v := range channelsToUnsubscribe[i].Params {
req[k] = v
}
err := b.Websocket.Conn.SendJSONMessage(req)
if err != nil {
errs = append(errs, err)
continue
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
}
return b.WebsocketConn.SendJSONMessage(req)
if errs != nil {
return errs
}
return nil
}
// WsSendAuth sends a autheticated event payload
func (b *Bitfinex) WsSendAuth() error {
if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name)
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled",
b.Name)
}
nonce := strconv.FormatInt(time.Now().Unix(), 10)
payload := "AUTH" + nonce
@@ -931,15 +1027,13 @@ func (b *Bitfinex) WsSendAuth() error {
Event: "auth",
APIKey: b.API.Credentials.Key,
AuthPayload: payload,
AuthSig: crypto.HexEncodeToString(
crypto.GetHMAC(
crypto.HashSHA512_384,
[]byte(payload),
[]byte(b.API.Credentials.Secret))),
AuthSig: crypto.HexEncodeToString(crypto.GetHMAC(crypto.HashSHA512_384,
[]byte(payload),
[]byte(b.API.Credentials.Secret))),
AuthNonce: nonce,
DeadManSwitch: 0,
}
err := b.AuthenticatedWebsocketConn.SendJSONMessage(request)
err := b.Websocket.AuthConn.SendJSONMessage(request)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
@@ -954,7 +1048,8 @@ func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) {
b.WebsocketSubdChannels[chanID] = chanInfo
if b.Verbose {
log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n",
log.Debugf(log.ExchangeSys,
"%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n",
b.Name,
channel,
pair,
@@ -964,9 +1059,9 @@ func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) {
// WsNewOrder authenticated new order request
func (b *Bitfinex) WsNewOrder(data *WsNewOrderRequest) (string, error) {
data.CustomID = b.AuthenticatedWebsocketConn.GenerateMessageID(false)
data.CustomID = b.Websocket.AuthConn.GenerateMessageID(false)
request := makeRequestInterface(wsOrderNew, data)
resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(data.CustomID, request)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(data.CustomID, request)
if err != nil {
return "", err
}
@@ -997,7 +1092,7 @@ func (b *Bitfinex) WsNewOrder(data *WsNewOrderRequest) (string, error) {
// WsModifyOrder authenticated modify order request
func (b *Bitfinex) WsModifyOrder(data *WsUpdateOrderRequest) error {
request := makeRequestInterface(wsOrderUpdate, data)
resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(data.OrderID, request)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(data.OrderID, request)
if err != nil {
return err
}
@@ -1026,7 +1121,7 @@ func (b *Bitfinex) WsCancelMultiOrders(orderIDs []int64) error {
OrderID: orderIDs,
}
request := makeRequestInterface(wsCancelMultipleOrders, cancel)
return b.AuthenticatedWebsocketConn.SendJSONMessage(request)
return b.Websocket.AuthConn.SendJSONMessage(request)
}
// WsCancelOrder authenticated cancel order request
@@ -1035,7 +1130,7 @@ func (b *Bitfinex) WsCancelOrder(orderID int64) error {
OrderID: orderID,
}
request := makeRequestInterface(wsOrderCancel, cancel)
resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(orderID, request)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(orderID, request)
if err != nil {
return err
}
@@ -1061,13 +1156,13 @@ func (b *Bitfinex) WsCancelOrder(orderID int64) error {
func (b *Bitfinex) WsCancelAllOrders() error {
cancelAll := WsCancelAllOrdersRequest{All: 1}
request := makeRequestInterface(wsCancelMultipleOrders, cancelAll)
return b.AuthenticatedWebsocketConn.SendJSONMessage(request)
return b.Websocket.AuthConn.SendJSONMessage(request)
}
// WsNewOffer authenticated new offer request
func (b *Bitfinex) WsNewOffer(data *WsNewOfferRequest) error {
request := makeRequestInterface(wsFundingOrderNew, data)
return b.AuthenticatedWebsocketConn.SendJSONMessage(request)
return b.Websocket.AuthConn.SendJSONMessage(request)
}
// WsCancelOffer authenticated cancel offer request
@@ -1076,7 +1171,7 @@ func (b *Bitfinex) WsCancelOffer(orderID int64) error {
OrderID: orderID,
}
request := makeRequestInterface(wsFundingOrderCancel, cancel)
resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(orderID, request)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(orderID, request)
if err != nil {
return err
}

View File

@@ -19,8 +19,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -57,17 +57,27 @@ func (b *Bitfinex) SetDefaults() {
b.API.CredentialsValidator.RequiresKey = true
b.API.CredentialsValidator.RequiresSecret = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
},
fmt1 := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
ConfigFormat: &currency.PairFormat{Uppercase: true},
}
fmt2 := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
ConfigFormat: &currency.PairFormat{Uppercase: true, Delimiter: ":"},
}
err := b.StoreAssetPairFormat(asset.Spot, fmt1)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = b.StoreAssetPairFormat(asset.Margin, fmt2)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = b.StoreAssetPairFormat(asset.MarginFunding, fmt1)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
@@ -111,7 +121,6 @@ func (b *Bitfinex) SetDefaults() {
OrderbookFetching: true,
AccountInfo: true,
Subscribe: true,
Unsubscribe: true,
AuthenticatedEndpoints: true,
MessageCorrelation: true,
DeadMansSwitch: true,
@@ -155,7 +164,7 @@ func (b *Bitfinex) SetDefaults() {
b.API.Endpoints.URLDefault = bitfinexAPIURLBase
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
b.API.Endpoints.WebsocketURL = publicBitfinexWebsocketEndpoint
b.Websocket = wshandler.New()
b.Websocket = stream.New()
b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
@@ -173,49 +182,41 @@ func (b *Bitfinex) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: publicBitfinexWebsocketEndpoint,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
Features: &b.Features.Supports.WebsocketCapabilities,
})
err = b.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: publicBitfinexWebsocketEndpoint,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
GenerateSubscriptions: b.GenerateDefaultSubscriptions,
Features: &b.Features.Supports.WebsocketCapabilities,
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
UpdateEntriesByID: true,
})
if err != nil {
return err
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: b.Websocket.GetWebsocketURL(),
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
b.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: authenticatedBitfinexWebsocketEndpoint,
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
err = b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
URL: publicBitfinexWebsocketEndpoint,
})
if err != nil {
return err
}
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
false,
false,
false,
true,
exch.Name)
return nil
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
URL: authenticatedBitfinexWebsocketEndpoint,
Authenticated: true,
})
}
// Start starts the Bitfinex go routine
@@ -244,7 +245,9 @@ func (b *Bitfinex) Run() {
err := b.UpdateTradablePairs(false)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s", b.Name, err)
"%s failed to update tradable pairs. Err: %s",
b.Name,
err)
}
}
@@ -265,6 +268,13 @@ func (b *Bitfinex) FetchTradablePairs(a asset.Item) ([]string, error) {
symbols = append(symbols, k[1:])
}
case asset.Margin:
for k := range items {
if !strings.Contains(k, ":") {
continue
}
symbols = append(symbols, k[1:])
}
case asset.MarginFunding:
for k := range items {
if !strings.HasPrefix(k, "f") {
continue
@@ -281,15 +291,19 @@ func (b *Bitfinex) FetchTradablePairs(a asset.Item) ([]string, error) {
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (b *Bitfinex) UpdateTradablePairs(forceUpdate bool) error {
for i := range b.CurrencyPairs.AssetTypes {
pairs, err := b.FetchTradablePairs(b.CurrencyPairs.AssetTypes[i])
assets := b.CurrencyPairs.GetAssetTypes()
for i := range assets {
pairs, err := b.FetchTradablePairs(assets[i])
if err != nil {
return err
}
err = b.UpdatePairs(currency.NewPairsFromStrings(pairs),
b.CurrencyPairs.AssetTypes[i],
false,
forceUpdate)
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
err = b.UpdatePairs(p, assets[i], false, forceUpdate)
if err != nil {
return err
}
@@ -299,34 +313,40 @@ func (b *Bitfinex) UpdateTradablePairs(forceUpdate bool) error {
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Bitfinex) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
enabledPairs := b.GetEnabledPairs(assetType)
enabledPairs, err := b.GetEnabledPairs(assetType)
if err != nil {
return nil, err
}
tickerNew, err := b.GetTickerBatch()
if err != nil {
return nil, err
}
for k, v := range tickerNew {
if strings.HasPrefix(k, "f") {
continue
}
pair := currency.NewPairFromString(k[1:]) // Remove prefix
if !enabledPairs.Contains(p, true) {
continue
}
tick := ticker.Price{
Last: v.Last,
High: v.High,
Low: v.Low,
Bid: v.Bid,
Ask: v.Ask,
Volume: v.Volume,
Pair: pair,
}
err = ticker.ProcessTicker(b.Name, &tick, assetType)
pair, err := currency.NewPairFromString(k[1:]) // Remove prefix
if err != nil {
log.Error(log.Ticker, err)
return nil, err
}
if !enabledPairs.Contains(pair, true) {
continue
}
err = ticker.ProcessTicker(&ticker.Price{
Last: v.Last,
High: v.High,
Low: v.Low,
Bid: v.Bid,
Ask: v.Ask,
Volume: v.Volume,
Pair: pair,
AssetType: assetType,
ExchangeName: b.Name})
if err != nil {
return nil, err
}
}
return ticker.GetTicker(b.Name, p, assetType)
}
@@ -354,7 +374,7 @@ func (b *Bitfinex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order
func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
b.appendOptionalDelimiter(&p)
var prefix = "t"
if assetType == asset.Margin {
if assetType == asset.MarginFunding {
prefix = "f"
}
@@ -459,11 +479,17 @@ func (b *Bitfinex) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) {
if err != nil {
return submitOrderResponse, err
}
fpair, err := b.FormatExchangeCurrency(o.Pair, o.AssetType)
if err != nil {
return submitOrderResponse, err
}
if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
submitOrderResponse.OrderID, err = b.WsNewOrder(&WsNewOrderRequest{
CustomID: b.AuthenticatedWebsocketConn.GenerateMessageID(false),
CustomID: b.Websocket.AuthConn.GenerateMessageID(false),
Type: o.Type.String(),
Symbol: b.FormatExchangeCurrency(o.Pair, asset.Spot).String(),
Symbol: fpair.String(),
Amount: o.Amount,
Price: o.Price,
})
@@ -473,18 +499,22 @@ func (b *Bitfinex) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) {
} else {
var response Order
isBuying := o.Side == order.Buy
b.appendOptionalDelimiter(&o.Pair)
response, err = b.NewOrder(o.Pair.String(),
o.Type.String(),
b.appendOptionalDelimiter(&fpair)
orderType := o.Type.Lower()
if o.AssetType == asset.Spot {
orderType = "exchange " + orderType
}
response, err = b.NewOrder(fpair.String(),
orderType,
o.Amount,
o.Price,
false,
isBuying)
isBuying,
false)
if err != nil {
return submitOrderResponse, err
}
if response.OrderID > 0 {
submitOrderResponse.OrderID = strconv.FormatInt(response.OrderID, 10)
if response.ID > 0 {
submitOrderResponse.OrderID = strconv.FormatInt(response.ID, 10)
}
if response.RemainingAmount == 0 {
submitOrderResponse.FullyMatched = true
@@ -615,11 +645,6 @@ func (b *Bitfinex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdra
}, nil
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Bitfinex) GetWebsocket() (*wshandler.Websocket, error) {
return b.Websocket, nil
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (b *Bitfinex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
if !b.AllowAuthenticatedRequest() && // Todo check connection status
@@ -639,23 +664,27 @@ func (b *Bitfinex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
for i := range resp {
orderSide := order.Side(strings.ToUpper(resp[i].Side))
timestamp, err := strconv.ParseInt(resp[i].Timestamp, 10, 64)
timestamp, err := strconv.ParseFloat(resp[i].Timestamp, 64)
if err != nil {
log.Warnf(log.ExchangeSys,
"Unable to convert timestamp '%s', leaving blank",
resp[i].Timestamp)
}
orderDate := time.Unix(timestamp, 0)
pair, err := currency.NewPairFromString(resp[i].Symbol)
if err != nil {
return nil, err
}
orderDetail := order.Detail{
Amount: resp[i].OriginalAmount,
Date: orderDate,
Date: time.Unix(int64(timestamp), 0),
Exchange: b.Name,
ID: strconv.FormatInt(resp[i].OrderID, 10),
ID: strconv.FormatInt(resp[i].ID, 10),
Side: orderSide,
Price: resp[i].Price,
RemainingAmount: resp[i].RemainingAmount,
Pair: currency.NewPairFromString(resp[i].Symbol),
Pair: pair,
ExecutedAmount: resp[i].ExecutedAmount,
}
@@ -706,16 +735,21 @@ func (b *Bitfinex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
}
orderDate := time.Unix(timestamp, 0)
pair, err := currency.NewPairFromString(resp[i].Symbol)
if err != nil {
return nil, err
}
orderDetail := order.Detail{
Amount: resp[i].OriginalAmount,
Date: orderDate,
Exchange: b.Name,
ID: strconv.FormatInt(resp[i].OrderID, 10),
ID: strconv.FormatInt(resp[i].ID, 10),
Side: orderSide,
Price: resp[i].Price,
RemainingAmount: resp[i].RemainingAmount,
ExecutedAmount: resp[i].ExecutedAmount,
Pair: currency.NewPairFromString(resp[i].Symbol),
Pair: pair,
}
switch {
@@ -751,31 +785,6 @@ func (b *Bitfinex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Bitfinex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
for i := range channels {
b.appendOptionalDelimiter(&channels[i].Currency)
}
b.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Bitfinex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
for i := range channels {
b.appendOptionalDelimiter(&channels[i].Currency)
}
b.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitfinex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitfinex) AuthenticateWebsocket() error {
return b.WsSendAuth()
@@ -822,7 +831,12 @@ func (b *Bitfinex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, e
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
candles, err := b.GetCandles(b.fixCasing(pair, a), b.FormatExchangeKlineInterval(interval),
cf, err := b.fixCasing(pair, a)
if err != nil {
return kline.Item{}, err
}
candles, err := b.GetCandles(cf, b.FormatExchangeKlineInterval(interval),
start.Unix()*1000, end.Unix()*1000,
b.Features.Enabled.Kline.ResultLimit, true)
if err != nil {
@@ -866,8 +880,13 @@ func (b *Bitfinex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item,
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
cf, err := b.fixCasing(pair, a)
if err != nil {
return kline.Item{}, err
}
for x := range dates {
candles, err := b.GetCandles(b.fixCasing(pair, a), b.FormatExchangeKlineInterval(interval),
candles, err := b.GetCandles(cf, b.FormatExchangeKlineInterval(interval),
dates[x].Start.Unix()*1000, dates[x].End.Unix()*1000,
b.Features.Enabled.Kline.ResultLimit, true)
if err != nil {
@@ -890,7 +909,7 @@ func (b *Bitfinex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item,
return ret, nil
}
func (b *Bitfinex) fixCasing(in currency.Pair, a asset.Item) string {
func (b *Bitfinex) fixCasing(in currency.Pair, a asset.Item) (string, error) {
var checkString [2]byte
if a == asset.Spot {
checkString[0] = 't'
@@ -900,13 +919,18 @@ func (b *Bitfinex) fixCasing(in currency.Pair, a asset.Item) string {
checkString[1] = 'F'
}
fmt, err := b.FormatExchangeCurrency(in, a)
if err != nil {
return "", err
}
y := in.Base.String()
if (y[0] != checkString[0] && y[0] != checkString[1]) ||
(y[0] == checkString[1] && y[1] == checkString[1]) || in.Base == currency.TNB {
return string(checkString[0]) + b.FormatExchangeCurrency(in, a).Upper().String()
return string(checkString[0]) + fmt.Upper().String(), nil
}
runes := []rune(b.FormatExchangeCurrency(in, a).Upper().String())
runes := []rune(fmt.Upper().String())
runes[0] = unicode.ToLower(runes[0])
return string(runes)
return string(runes), nil
}

View File

@@ -133,7 +133,10 @@ func TestGetExchangeStatus(t *testing.T) {
func TestCheckFXString(t *testing.T) {
t.Parallel()
p := currency.NewPairDelimiter("FXBTC_JPY", "_")
p, err := currency.NewPairDelimiter("FXBTC_JPY", "_")
if err != nil {
t.Fatal(err)
}
p = b.CheckFXString(p)
if p.Base.String() != "FX_BTC" {
t.Error("Bitflyer - CheckFXString() error")
@@ -144,15 +147,19 @@ func TestFetchTicker(t *testing.T) {
t.Parallel()
var p currency.Pair
currencies := b.GetAvailablePairs(asset.Spot)
for _, pair := range currencies {
if pair.String() == "FXBTC_JPY" {
p = pair
currencies, err := b.GetAvailablePairs(asset.Spot)
if err != nil {
t.Fatal(err)
}
for i := range currencies {
if currencies[i].String() == "FXBTC_JPY" {
p = currencies[i]
break
}
}
_, err := b.FetchTicker(p, asset.Spot)
_, err = b.FetchTicker(p, asset.Spot)
if err != nil {
t.Error("Bitflyer - FetchTicker() error", err)
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -53,20 +52,20 @@ func (b *Bitflyer) SetDefaults() {
b.API.CredentialsValidator.RequiresKey = true
b.API.CredentialsValidator.RequiresSecret = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Delimiter: "_",
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Delimiter: "_",
Uppercase: true,
},
requestFmt := &currency.PairFormat{
Delimiter: currency.UnderscoreDelimiter,
Uppercase: true,
}
configFmt := &currency.PairFormat{
Delimiter: currency.UnderscoreDelimiter,
Uppercase: true,
}
err := b.SetGlobalPairsManager(requestFmt,
configFmt,
asset.Spot,
asset.Futures)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
@@ -105,7 +104,6 @@ func (b *Bitflyer) Setup(exch *config.ExchangeConfig) error {
b.SetEnabled(false)
return nil
}
return b.SetupDefaults(exch)
}
@@ -141,6 +139,11 @@ func (b *Bitflyer) FetchTradablePairs(assetType asset.Item) ([]string, error) {
return nil, err
}
format, err := b.GetPairFormat(assetType, false)
if err != nil {
return nil, err
}
var products []string
for i := range pairs {
if pairs[i].Alias != "" && assetType == asset.Futures {
@@ -148,7 +151,7 @@ func (b *Bitflyer) FetchTradablePairs(assetType asset.Item) ([]string, error) {
} else if pairs[i].Alias == "" &&
assetType == asset.Spot &&
strings.Contains(pairs[i].ProductCode,
b.GetPairFormat(assetType, false).Delimiter) {
format.Delimiter) {
products = append(products, pairs[i].ProductCode)
}
}
@@ -158,17 +161,19 @@ func (b *Bitflyer) FetchTradablePairs(assetType asset.Item) ([]string, error) {
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (b *Bitflyer) UpdateTradablePairs(forceUpdate bool) error {
for x := range b.CurrencyPairs.AssetTypes {
a := b.CurrencyPairs.AssetTypes[x]
pairs, err := b.FetchTradablePairs(a)
assets := b.CurrencyPairs.GetAssetTypes()
for x := range assets {
pairs, err := b.FetchTradablePairs(assets[x])
if err != nil {
return err
}
err = b.UpdatePairs(currency.NewPairsFromStrings(pairs),
a,
false,
forceUpdate)
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
err = b.UpdatePairs(p, assets[x], false, forceUpdate)
if err != nil {
return err
}
@@ -178,23 +183,21 @@ func (b *Bitflyer) UpdateTradablePairs(forceUpdate bool) error {
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Bitflyer) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
p = b.CheckFXString(p)
tickerNew, err := b.GetTicker(p.String())
tickerNew, err := b.GetTicker(b.CheckFXString(p).String())
if err != nil {
return tickerPrice, err
return nil, err
}
tickerPrice.Pair = p
tickerPrice.Ask = tickerNew.BestAsk
tickerPrice.Bid = tickerNew.BestBid
tickerPrice.Last = tickerNew.Last
tickerPrice.Volume = tickerNew.Volume
err = ticker.ProcessTicker(b.Name, tickerPrice, assetType)
err = ticker.ProcessTicker(&ticker.Price{
Pair: p,
Ask: tickerNew.BestAsk,
Bid: tickerNew.BestBid,
Last: tickerNew.Last,
Volume: tickerNew.Volume,
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return tickerPrice, err
return nil, err
}
return ticker.GetTicker(b.Name, p, assetType)
@@ -231,9 +234,7 @@ func (b *Bitflyer) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order
func (b *Bitflyer) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
p = b.CheckFXString(p)
orderbookNew, err := b.GetOrderBook(p.String())
orderbookNew, err := b.GetOrderBook(b.CheckFXString(p).String())
if err != nil {
return orderBook, err
}
@@ -337,11 +338,6 @@ func (b *Bitflyer) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdra
return nil, common.ErrNotYetImplemented
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Bitflyer) GetWebsocket() (*wshandler.Websocket, error) {
return nil, common.ErrNotYetImplemented
}
// GetActiveOrders retrieves any orders that are active/open
func (b *Bitflyer) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) {
return nil, common.ErrNotYetImplemented
@@ -362,28 +358,6 @@ func (b *Bitflyer) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error
return b.GetFee(feeBuilder)
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Bitflyer) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Bitflyer) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitflyer) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitflyer) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *Bitflyer) ValidateCredentials() error {

View File

@@ -207,6 +207,20 @@ func TestMarketSellOrder(t *testing.T) {
}
}
func TestUpdateTicker(t *testing.T) {
cp := currency.NewPair(currency.QTUM, currency.KRW)
_, err := b.UpdateTicker(cp, asset.Spot)
if err != nil {
t.Fatal(err)
}
cp = currency.NewPair(currency.DASH, currency.KRW)
_, err = b.UpdateTicker(cp, asset.Spot)
if err != nil {
t.Fatal(err)
}
}
func setFeeBuilder() *exchange.FeeBuilder {
return &exchange.FeeBuilder{
Amount: 1,
@@ -436,8 +450,11 @@ func TestGetAccountInfo(t *testing.T) {
func TestModifyOrder(t *testing.T) {
t.Parallel()
curr := currency.NewPairFromString("BTCUSD")
_, err := b.ModifyOrder(&order.Modify{
curr, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
_, err = b.ModifyOrder(&order.Modify{
ID: "1337",
Price: 100,
Amount: 1000,
@@ -535,18 +552,24 @@ func TestGetCandleStick(t *testing.T) {
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_KRW")
currencyPair, err := currency.NewPairFromString("BTC_KRW")
if err != nil {
t.Fatal(err)
}
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneDay)
_, err = b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_KRW")
currencyPair, err := currency.NewPairFromString("BTC_KRW")
if err != nil {
t.Fatal(err)
}
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneDay)
_, err = b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -56,20 +55,11 @@ func (b *Bithumb) SetDefaults() {
b.API.CredentialsValidator.RequiresKey = true
b.API.CredentialsValidator.RequiresSecret = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Index: "KRW",
},
requestFmt := &currency.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}
configFmt := &currency.PairFormat{Uppercase: true, Index: "KRW"}
err := b.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
@@ -135,7 +125,6 @@ func (b *Bithumb) Setup(exch *config.ExchangeConfig) error {
b.SetEnabled(false)
return nil
}
return b.SetupDefaults(exch)
}
@@ -186,34 +175,46 @@ func (b *Bithumb) UpdateTradablePairs(forceUpdate bool) error {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate)
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
return b.UpdatePairs(p, asset.Spot, false, forceUpdate)
}
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Bithumb) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
tickers, err := b.GetAllTickers()
if err != nil {
return tickerPrice, err
return nil, err
}
pairs := b.GetEnabledPairs(assetType)
pairs, err := b.GetEnabledPairs(assetType)
if err != nil {
return nil, err
}
for i := range pairs {
curr := pairs[i].Base.String()
t, ok := tickers[curr]
if !ok {
continue
return nil,
fmt.Errorf("enabled pair %s [%s] not found in returned ticker map %v",
pairs[i], pairs, tickers)
}
tp := ticker.Price{
High: t.MaxPrice,
Low: t.MinPrice,
Volume: t.UnitsTraded24Hr,
Open: t.OpeningPrice,
Close: t.ClosingPrice,
Pair: pairs[i],
}
err = ticker.ProcessTicker(b.Name, &tp, assetType)
err = ticker.ProcessTicker(&ticker.Price{
High: t.MaxPrice,
Low: t.MinPrice,
Volume: t.UnitsTraded24Hr,
Open: t.OpeningPrice,
Close: t.ClosingPrice,
Pair: pairs[i],
ExchangeName: b.Name,
AssetType: assetType,
})
if err != nil {
log.Error(log.Ticker, err)
return nil, err
}
}
return ticker.GetTicker(b.Name, p, assetType)
@@ -398,7 +399,11 @@ func (b *Bithumb) CancelAllOrders(orderCancellation *order.Cancel) (order.Cancel
}
var allOrders []OrderData
currs := b.GetEnabledPairs(asset.Spot)
currs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return cancelAllOrdersResponse, err
}
for i := range currs {
orders, err := b.GetOrders("",
orderCancellation.Side.String(),
@@ -484,11 +489,6 @@ func (b *Bithumb) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Bithumb) GetWebsocket() (*wshandler.Websocket, error) {
return nil, common.ErrFunctionNotSupported
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (b *Bithumb) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
if !b.AllowAuthenticatedRequest() && // Todo check connection status
@@ -506,6 +506,11 @@ func (b *Bithumb) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
return nil, err
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
for i := range resp.Data {
if resp.Data[i].Status != "placed" {
continue
@@ -522,7 +527,7 @@ func (b *Bithumb) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
Status: order.Active,
Pair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency,
resp.Data[i].PaymentCurrency,
b.GetPairFormat(asset.Spot, false).Delimiter),
format.Delimiter),
}
if resp.Data[i].Type == "bid" {
@@ -549,6 +554,11 @@ func (b *Bithumb) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
return nil, err
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
for i := range resp.Data {
if resp.Data[i].Status == "placed" {
continue
@@ -564,7 +574,7 @@ func (b *Bithumb) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
RemainingAmount: resp.Data[i].UnitsRemaining,
Pair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency,
resp.Data[i].PaymentCurrency,
b.GetPairFormat(asset.Spot, false).Delimiter),
format.Delimiter),
}
if resp.Data[i].Type == "bid" {
@@ -582,28 +592,6 @@ func (b *Bithumb) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Bithumb) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Bithumb) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bithumb) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bithumb) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *Bithumb) ValidateCredentials() error {
@@ -624,7 +612,13 @@ func (b *Bithumb) GetHistoricCandles(pair currency.Pair, a asset.Item, start, en
}
}
candle, err := b.GetCandleStick(b.FormatExchangeCurrency(pair, a).String(), b.FormatExchangeKlineInterval(interval))
formattedPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
candle, err := b.GetCandleStick(formattedPair.String(),
b.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}

View File

@@ -14,13 +14,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
// Bitmex is the overarching type across this package
type Bitmex struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
}
const (

View File

@@ -16,7 +16,7 @@ import (
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -45,13 +45,11 @@ func TestMain(m *testing.M) {
bitmexConfig.API.AuthenticatedWebsocketSupport = true
bitmexConfig.API.Credentials.Key = apiKey
bitmexConfig.API.Credentials.Secret = apiSecret
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(bitmexConfig)
if err != nil {
log.Fatal("Bitmex setup error", err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
os.Exit(m.Run())
}
@@ -678,17 +676,10 @@ func TestGetDepositAddress(t *testing.T) {
// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(wshandler.WebsocketNotEnabled)
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: b.Websocket.GetWebsocketURL(),
Verbose: b.Verbose,
ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit,
ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout,
t.Skip(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.WebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
t.Fatal(err)
}
@@ -710,6 +701,13 @@ func TestWsAuth(t *testing.T) {
timer.Stop()
}
func TestUpdateTradablePairs(t *testing.T) {
err := b.UpdateTradablePairs(true)
if err != nil {
t.Fatal(err)
}
}
func TestWsPositionUpdate(t *testing.T) {
pressXToJSON := []byte(`{"table":"position",
"action":"update",
@@ -800,7 +798,6 @@ func TestWSPositionUpdateHandling(t *testing.T) {
}
func TestWSOrderbookHandling(t *testing.T) {
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
pressXToJSON := []byte(`{
"table":"orderBookL2_25",
"keys":["symbol","id","side"],
@@ -871,7 +868,6 @@ func TestWSOrderbookHandling(t *testing.T) {
}
func TestWSDeleveragePositionUpdateHandling(t *testing.T) {
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
pressXToJSON := []byte(`{"table":"position",
"action":"update",
"data":[{

View File

@@ -16,8 +16,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -67,67 +67,72 @@ const (
// WsConnect initiates a new websocket connection
func (b *Bitmex) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.WebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
p, err := b.WebsocketConn.ReadMessage()
if err != nil {
b.Websocket.ReadMessageErrors <- err
return err
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return errors.New("connection closed")
}
b.Websocket.TrafficAlert <- struct{}{}
var welcomeResp WebsocketWelcome
err = json.Unmarshal(p.Raw, &welcomeResp)
err = json.Unmarshal(resp.Raw, &welcomeResp)
if err != nil {
return err
}
if b.Verbose {
log.Debugf(log.ExchangeSys, "Successfully connected to Bitmex %s at time: %s Limit: %d",
log.Debugf(log.ExchangeSys,
"Successfully connected to Bitmex %s at time: %s Limit: %d",
welcomeResp.Info,
welcomeResp.Timestamp,
welcomeResp.Limit.Remaining)
}
go b.wsReadData()
b.GenerateDefaultSubscriptions()
subs, err := b.GenerateDefaultSubscriptions()
if err != nil {
return err
}
err = b.Websocket.SubscribeToChannels(subs)
if err != nil {
return err
}
err = b.websocketSendAuth()
if err != nil {
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err)
log.Errorf(log.ExchangeSys,
"%v - authentication failed: %v\n",
b.Name,
err)
} else {
authsubs, err := b.GenerateAuthenticatedSubscriptions()
if err != nil {
return err
}
return b.Websocket.SubscribeToChannels(authsubs)
}
b.GenerateAuthenticatedSubscriptions()
return nil
}
// wsReadData receives and passes on websocket messages for processing
func (b *Bitmex) wsReadData() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
default:
resp, err := b.WebsocketConn.ReadMessage()
if err != nil {
b.Websocket.DataHandler <- err
return
}
b.Websocket.TrafficAlert <- struct{}{}
err = b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
@@ -188,7 +193,12 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
if len(orderbooks.Data) == 0 {
return fmt.Errorf("%s - Empty orderbook data received: %s", b.Name, respRaw)
}
p := currency.NewPairFromString(orderbooks.Data[0].Symbol)
var p currency.Pair
p, err = currency.NewPairFromString(orderbooks.Data[0].Symbol)
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
@@ -210,13 +220,14 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
return err
}
if trades.Action == bitmexActionInitialData {
return nil
}
for i := range trades.Data {
var p currency.Pair
p, err = currency.NewPairFromString(trades.Data[i].Symbol)
if err != nil {
return err
}
var a asset.Item
p := currency.NewPairFromString(trades.Data[i].Symbol)
a, err = b.GetPairAssetType(p)
if err != nil {
return err
@@ -230,7 +241,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
}
}
b.Websocket.DataHandler <- wshandler.TradeData{
b.Websocket.DataHandler <- stream.TradeData{
Timestamp: trades.Data[i].Timestamp,
Price: trades.Data[i].Price,
Amount: float64(trades.Data[i].Size),
@@ -271,7 +282,12 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
}
for i := range response.Data {
p := currency.NewPairFromString(response.Data[i].Symbol)
var p currency.Pair
p, err = currency.NewPairFromString(response.Data[i].Symbol)
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
@@ -459,7 +475,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
}
b.Websocket.DataHandler <- response
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
return nil
}
}
@@ -467,7 +483,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
}
// ProcessOrderbook processes orderbook updates
func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPair currency.Pair, assetType asset.Item) error {
func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.Pair, a asset.Item) error {
if len(data) < 1 {
return errors.New("bitmex_websocket.go error - no orderbook data")
}
@@ -490,8 +506,8 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai
ID: data[i].ID,
})
}
newOrderBook.AssetType = assetType
newOrderBook.Pair = currencyPair
newOrderBook.AssetType = a
newOrderBook.Pair = p
newOrderBook.ExchangeName = b.Name
err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
@@ -499,11 +515,6 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai
return fmt.Errorf("bitmex_websocket.go process orderbook error - %s",
err)
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: currencyPair,
Asset: assetType,
Exchange: b.Name,
}
default:
var asks, bids []orderbook.Item
for i := range data {
@@ -520,40 +531,42 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai
})
}
err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
err := b.Websocket.Orderbook.Update(&buffer.Update{
Bids: bids,
Asks: asks,
Pair: currencyPair,
Asset: assetType,
Pair: p,
Asset: a,
Action: action,
})
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: currencyPair,
Asset: assetType,
Exchange: b.Name,
}
}
return nil
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateDefaultSubscriptions() {
func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
assets := b.GetAssetTypes()
var allPairs currency.Pairs
var associatedAssets []asset.Item
for x := range assets {
contracts := b.GetEnabledPairs(assets[x])
contracts, err := b.GetEnabledPairs(assets[x])
if err != nil {
return nil, err
}
for y := range contracts {
allPairs = allPairs.Add(contracts[y])
associatedAssets = append(associatedAssets, assets[x])
}
}
if len(allPairs) != len(associatedAssets) {
return nil, fmt.Errorf("%s generate default subscriptions: pair and asset type len mismatch", b.Name)
}
channels := []string{bitmexWSOrderbookL2, bitmexWSTrade}
subscriptions := []wshandler.WebsocketChannelSubscription{
subscriptions := []stream.ChannelSubscription{
{
Channel: bitmexWSAnnouncement,
},
@@ -561,25 +574,29 @@ func (b *Bitmex) GenerateDefaultSubscriptions() {
for i := range channels {
for j := range allPairs {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i] + ":" + allPairs[j].String(),
Currency: allPairs[j],
Asset: associatedAssets[j],
})
}
}
b.Websocket.SubscribeToChannels(subscriptions)
return subscriptions, nil
}
// GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateAuthenticatedSubscriptions() {
func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscription, error) {
if !b.Websocket.CanUseAuthenticatedEndpoints() {
return
return nil, nil
}
contracts, err := b.GetEnabledPairs(asset.PerpetualContract)
if err != nil {
return nil, err
}
contracts := b.GetEnabledPairs(asset.PerpetualContract)
channels := []string{bitmexWSExecution,
bitmexWSPosition,
}
subscriptions := []wshandler.WebsocketChannelSubscription{
subscriptions := []stream.ChannelSubscription{
{
Channel: bitmexWSAffiliate,
},
@@ -601,31 +618,48 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() {
}
for i := range channels {
for j := range contracts {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i] + ":" + contracts[j].String(),
Currency: contracts[j],
Asset: asset.PerpetualContract,
})
}
}
b.Websocket.SubscribeToChannels(subscriptions)
return subscriptions, nil
}
// Subscribe subscribes to a websocket channel
func (b *Bitmex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
func (b *Bitmex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var subscriber WebsocketRequest
subscriber.Command = "subscribe"
subscriber.Arguments = append(subscriber.Arguments, channelToSubscribe.Channel)
return b.WebsocketConn.SendJSONMessage(subscriber)
for i := range channelsToSubscribe {
subscriber.Arguments = append(subscriber.Arguments,
channelsToSubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(subscriber)
if err != nil {
return err
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitmex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var subscriber WebsocketRequest
subscriber.Command = "unsubscribe"
subscriber.Arguments = append(subscriber.Arguments,
channelToSubscribe.Params["args"],
channelToSubscribe.Channel+":"+channelToSubscribe.Currency.String())
return b.WebsocketConn.SendJSONMessage(subscriber)
func (b *Bitmex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var unsubscriber WebsocketRequest
unsubscriber.Command = "unsubscribe"
for i := range channelsToUnsubscribe {
unsubscriber.Arguments = append(unsubscriber.Arguments,
channelsToUnsubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(unsubscriber)
if err != nil {
return err
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...)
return nil
}
// WebsocketSendAuth sends an authenticated subscription
@@ -645,7 +679,7 @@ func (b *Bitmex) websocketSendAuth() error {
sendAuth.Command = "authKeyExpires"
sendAuth.Arguments = append(sendAuth.Arguments, b.API.Credentials.Key, timestamp,
signature)
err := b.WebsocketConn.SendJSONMessage(sendAuth)
err := b.Websocket.Conn.SendJSONMessage(sendAuth)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err

View File

@@ -18,8 +18,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -55,41 +55,17 @@ func (b *Bitmex) SetDefaults() {
b.API.CredentialsValidator.RequiresKey = true
b.API.CredentialsValidator.RequiresSecret = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.PerpetualContract,
asset.Futures,
asset.DownsideProfitContract,
asset.UpsideProfitContract,
},
requestFmt := &currency.PairFormat{Uppercase: true}
configFmt := &currency.PairFormat{Uppercase: true}
err := b.SetGlobalPairsManager(requestFmt,
configFmt,
asset.PerpetualContract,
asset.Futures,
asset.Index)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// Same format used for perpetual contracts and futures
fmt1 := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
},
}
b.CurrencyPairs.Store(asset.PerpetualContract, fmt1)
b.CurrencyPairs.Store(asset.Futures, fmt1)
// Upside and Downside profit contracts use the same format
fmt2 := currency.PairStore{
RequestFormat: &currency.PairFormat{
Delimiter: "_",
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Delimiter: "_",
Uppercase: true,
},
}
b.CurrencyPairs.Store(asset.DownsideProfitContract, fmt2)
b.CurrencyPairs.Store(asset.UpsideProfitContract, fmt2)
b.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
@@ -144,7 +120,7 @@ func (b *Bitmex) SetDefaults() {
b.API.Endpoints.URLDefault = bitmexAPIURL
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
b.API.Endpoints.WebsocketURL = bitmexWSURL
b.Websocket = wshandler.New()
b.Websocket = stream.New()
b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
@@ -162,41 +138,30 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: bitmexWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
Features: &b.Features.Supports.WebsocketCapabilities,
})
err = b.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: bitmexWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
GenerateSubscriptions: b.GenerateDefaultSubscriptions,
Features: &b.Features.Supports.WebsocketCapabilities,
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
UpdateEntriesByID: true,
})
if err != nil {
return err
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: b.Websocket.GetWebsocketURL(),
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
false,
false,
false,
true,
exch.Name)
return nil
})
}
// Start starts the Bitmex go routine
@@ -211,7 +176,11 @@ func (b *Bitmex) Start(wg *sync.WaitGroup) {
// Run implements the Bitmex wrapper
func (b *Bitmex) Run() {
if b.Verbose {
log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", b.Name, common.IsEnabled(b.Websocket.IsEnabled()), b.API.Endpoints.WebsocketURL)
log.Debugf(log.ExchangeSys,
"%s Websocket: %s. (url: %s).\n",
b.Name,
common.IsEnabled(b.Websocket.IsEnabled()),
b.API.Endpoints.WebsocketURL)
b.PrintEnabledPairs()
}
@@ -226,8 +195,8 @@ func (b *Bitmex) Run() {
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (b *Bitmex) FetchTradablePairs(_ asset.Item) ([]string, error) {
marketInfo, err := b.GetActiveInstruments(&GenericRequestParams{})
func (b *Bitmex) FetchTradablePairs(asset asset.Item) ([]string, error) {
marketInfo, err := b.GetActiveAndIndexInstruments()
if err != nil {
return nil, err
}
@@ -248,72 +217,78 @@ func (b *Bitmex) UpdateTradablePairs(forceUpdate bool) error {
return err
}
var assetPairs []string
for x := range b.CurrencyPairs.AssetTypes {
switch b.CurrencyPairs.AssetTypes[x] {
case asset.PerpetualContract:
for y := range pairs {
if strings.Contains(pairs[y], "USD") {
assetPairs = append(assetPairs, pairs[y])
}
}
case asset.Futures:
for y := range pairs {
if strings.Contains(pairs[y], "20") {
assetPairs = append(assetPairs, pairs[y])
}
}
case asset.DownsideProfitContract:
for y := range pairs {
if strings.Contains(pairs[y], "_D") {
assetPairs = append(assetPairs, pairs[y])
}
}
case asset.UpsideProfitContract:
for y := range pairs {
if strings.Contains(pairs[y], "_U") {
assetPairs = append(assetPairs, pairs[y])
}
}
// Zerovalue current list which will remove old asset pairs when contract
// types expire or become obsolete
var assetPairs = map[asset.Item][]string{
asset.Index: {},
asset.PerpetualContract: {},
asset.Futures: {},
}
for x := range pairs {
if strings.Contains(pairs[x], ".") {
assetPairs[asset.Index] = append(assetPairs[asset.Index], pairs[x])
continue
}
err = b.UpdatePairs(currency.NewPairsFromStrings(assetPairs), b.CurrencyPairs.AssetTypes[x], false, false)
if err != nil {
log.Warnf(log.ExchangeSys, "%s failed to update available pairs. Err: %v", b.Name, err)
if strings.Contains(pairs[x], "USD") {
assetPairs[asset.PerpetualContract] = append(assetPairs[asset.PerpetualContract],
pairs[x])
continue
}
assetPairs = nil
assetPairs[asset.Futures] = append(assetPairs[asset.Futures], pairs[x])
}
for a, values := range assetPairs {
p, err := currency.NewPairsFromStrings(values)
if err != nil {
return err
}
err = b.UpdatePairs(p, a, false, false)
if err != nil {
log.Warnf(log.ExchangeSys,
"%s failed to update available pairs. Err: %v",
b.Name,
err)
}
}
return nil
}
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Bitmex) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
tick, err := b.GetActiveInstruments(&GenericRequestParams{})
tick, err := b.GetActiveAndIndexInstruments()
if err != nil {
return tickerPrice, err
return nil, err
}
pairs := b.GetEnabledPairs(assetType)
for i := range pairs {
for j := range tick {
if !pairs[i].Equal(tick[j].Symbol) {
continue
}
tickerPrice = &ticker.Price{
Last: tick[j].LastPrice,
High: tick[j].HighPrice,
Low: tick[j].LowPrice,
Bid: tick[j].BidPrice,
Ask: tick[j].AskPrice,
Volume: tick[j].Volume24h,
Close: tick[j].PrevClosePrice,
Pair: tick[j].Symbol,
LastUpdated: tick[j].Timestamp,
}
err = ticker.ProcessTicker(b.Name, tickerPrice, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
pairs, err := b.GetEnabledPairs(assetType)
if err != nil {
return nil, err
}
for j := range tick {
if !pairs.Contains(tick[j].Symbol, true) {
continue
}
err = ticker.ProcessTicker(&ticker.Price{
Last: tick[j].LastPrice,
High: tick[j].HighPrice,
Low: tick[j].LowPrice,
Bid: tick[j].BidPrice,
Ask: tick[j].AskPrice,
Volume: tick[j].Volume24h,
Close: tick[j].PrevClosePrice,
Pair: tick[j].Symbol,
LastUpdated: tick[j].Timestamp,
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return nil, err
}
}
return ticker.GetTicker(b.Name, p, assetType)
@@ -339,25 +314,34 @@ func (b *Bitmex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
orderbookNew, err := b.GetOrderbook(OrderBookGetL2Params{
Symbol: b.FormatExchangeCurrency(p, assetType).String(),
Depth: 500})
if err != nil {
return orderBook, err
if assetType == asset.Index {
return nil, common.ErrFunctionNotSupported
}
for _, ob := range orderbookNew {
if strings.EqualFold(ob.Side, order.Sell.String()) {
orderBook.Asks = append(orderBook.Asks,
orderbook.Item{Amount: float64(ob.Size), Price: ob.Price})
fpair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
orderbookNew, err := b.GetOrderbook(OrderBookGetL2Params{
Symbol: fpair.String(),
Depth: 500})
if err != nil {
return nil, err
}
orderBook := new(orderbook.Base)
for i := range orderbookNew {
if strings.EqualFold(orderbookNew[i].Side, order.Sell.String()) {
orderBook.Asks = append(orderBook.Asks, orderbook.Item{
Amount: float64(orderbookNew[i].Size),
Price: orderbookNew[i].Price})
continue
}
if strings.EqualFold(ob.Side, order.Buy.String()) {
orderBook.Bids = append(orderBook.Bids,
orderbook.Item{Amount: float64(ob.Size), Price: ob.Price})
continue
if strings.EqualFold(orderbookNew[i].Side, order.Buy.String()) {
orderBook.Bids = append(orderBook.Bids, orderbook.Item{
Amount: float64(orderbookNew[i].Size),
Price: orderbookNew[i].Price})
}
}
@@ -561,11 +545,6 @@ func (b *Bitmex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Bitmex) GetWebsocket() (*wshandler.Websocket, error) {
return b.Websocket, nil
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (b *Bitmex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
if !b.AllowAuthenticatedRequest() && // Todo check connection status
@@ -587,6 +566,11 @@ func (b *Bitmex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e
return nil, err
}
format, err := b.GetPairFormat(asset.PerpetualContract, false)
if err != nil {
return nil, err
}
for i := range resp {
orderSide := orderSideMap[resp[i].Side]
orderType := orderTypeMap[resp[i].OrdType]
@@ -604,7 +588,7 @@ func (b *Bitmex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e
Status: order.Status(resp[i].OrdStatus),
Pair: currency.NewPairWithDelimiter(resp[i].Symbol,
resp[i].SettlCurrency,
b.GetPairFormat(asset.PerpetualContract, false).Delimiter),
format.Delimiter),
}
orders = append(orders, orderDetail)
@@ -628,6 +612,11 @@ func (b *Bitmex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e
return nil, err
}
format, err := b.GetPairFormat(asset.PerpetualContract, false)
if err != nil {
return nil, err
}
for i := range resp {
orderSide := orderSideMap[resp[i].Side]
orderType := orderTypeMap[resp[i].OrdType]
@@ -645,7 +634,7 @@ func (b *Bitmex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e
Status: order.Status(resp[i].OrdStatus),
Pair: currency.NewPairWithDelimiter(resp[i].Symbol,
resp[i].SettlCurrency,
b.GetPairFormat(asset.PerpetualContract, false).Delimiter),
format.Delimiter),
}
orders = append(orders, orderDetail)
@@ -658,25 +647,6 @@ func (b *Bitmex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Bitmex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
b.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Bitmex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
b.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitmex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitmex) AuthenticateWebsocket() error {
return b.websocketSendAuth()

View File

@@ -18,7 +18,6 @@ import (
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -64,7 +63,6 @@ const (
// Bitstamp is the overarching type across the bitstamp package
type Bitstamp struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
}
// GetFee returns an estimate of fee based on type of transaction

View File

@@ -30,12 +30,11 @@ func TestMain(m *testing.M) {
bitstampConfig.API.Credentials.Secret = apiSecret
bitstampConfig.API.Credentials.ClientID = customerID
b.SetDefaults()
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(bitstampConfig)
if err != nil {
log.Fatal("Bitstamp setup error", err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
log.Printf(sharedtestvalues.LiveTesting, b.Name, b.API.Endpoints.URL)
os.Exit(m.Run())
}

View File

@@ -34,6 +34,7 @@ func TestMain(m *testing.M) {
bitstampConfig.API.Credentials.Secret = apiSecret
bitstampConfig.API.Credentials.ClientID = customerID
b.SetDefaults()
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(bitstampConfig)
if err != nil {
log.Fatal("Bitstamp setup error", err)
@@ -46,8 +47,6 @@ func TestMain(m *testing.M) {
b.HTTPClient = newClient
b.API.Endpoints.URL = serverDetails + "/api"
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
log.Printf(sharedtestvalues.MockTesting, b.Name, b.API.Endpoints.URL)
os.Exit(m.Run())
}

View File

@@ -678,21 +678,27 @@ func TestBitstamp_OHLC(t *testing.T) {
}
func TestBitstamp_GetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("btcusd")
currencyPair, err := currency.NewPairFromString("btcusd")
if err != nil {
t.Fatal(err)
}
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, start, end, kline.OneDay)
_, err = b.GetHistoricCandles(currencyPair, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
}
func TestBitstamp_GetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("btcusd")
currencyPair, err := currency.NewPairFromString("btcusd")
if err != nil {
t.Fatal(err)
}
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, start, end, kline.OneDay)
_, err = b.GetHistoricCandlesExtended(currencyPair, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}

View File

@@ -9,11 +9,12 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -24,10 +25,10 @@ const (
// WsConnect connects to a websocket feed
func (b *Bitstamp) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.WebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
@@ -38,33 +39,27 @@ func (b *Bitstamp) WsConnect() error {
if err != nil {
b.Websocket.DataHandler <- err
}
b.generateDefaultSubscriptions()
subs, err := b.generateDefaultSubscriptions()
if err != nil {
return err
}
go b.wsReadData()
return nil
return b.Websocket.SubscribeToChannels(subs)
}
// wsReadData receives and passes on websocket messages for processing
func (b *Bitstamp) wsReadData() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
default:
resp, err := b.WebsocketConn.ReadMessage()
if err != nil {
b.Websocket.ReadMessageErrors <- err
return
}
b.Websocket.TrafficAlert <- struct{}{}
err = b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
@@ -97,7 +92,11 @@ func (b *Bitstamp) wsHandleData(respRaw []byte) error {
return err
}
currencyPair := strings.Split(wsResponse.Channel, "_")
p := currency.NewPairFromString(strings.ToUpper(currencyPair[2]))
p, err := currency.NewPairFromString(strings.ToUpper(currencyPair[2]))
if err != nil {
return err
}
err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, asset.Spot)
if err != nil {
return err
@@ -109,7 +108,11 @@ func (b *Bitstamp) wsHandleData(respRaw []byte) error {
return err
}
currencyPair := strings.Split(wsResponse.Channel, "_")
p := currency.NewPairFromString(strings.ToUpper(currencyPair[2]))
p, err := currency.NewPairFromString(strings.ToUpper(currencyPair[2]))
if err != nil {
return err
}
side := order.Buy
if wsTradeTemp.Data.Type == -1 {
side = order.Sell
@@ -119,7 +122,7 @@ func (b *Bitstamp) wsHandleData(respRaw []byte) error {
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.TradeData{
b.Websocket.DataHandler <- stream.TradeData{
Timestamp: time.Unix(wsTradeTemp.Data.Timestamp, 0),
CurrencyPair: p,
AssetType: a,
@@ -134,45 +137,73 @@ func (b *Bitstamp) wsHandleData(respRaw []byte) error {
log.Debugf(log.ExchangeSys, "%v - Websocket order acknowledgement", b.Name)
}
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
}
return nil
}
func (b *Bitstamp) generateDefaultSubscriptions() {
func (b *Bitstamp) generateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{"live_trades_", "order_book_"}
enabledCurrencies := b.GetEnabledPairs(asset.Spot)
var subscriptions []wshandler.WebsocketChannelSubscription
enabledCurrencies, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i] + enabledCurrencies[j].Lower().String(),
Asset: asset.Spot,
})
}
}
b.Websocket.SubscribeToChannels(subscriptions)
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *Bitstamp) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
req := websocketEventRequest{
Event: "bts:subscribe",
Data: websocketData{
Channel: channelToSubscribe.Channel,
},
func (b *Bitstamp) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var errs common.Errors
for i := range channelsToSubscribe {
req := websocketEventRequest{
Event: "bts:subscribe",
Data: websocketData{
Channel: channelsToSubscribe[i].Channel,
},
}
err := b.Websocket.Conn.SendJSONMessage(req)
if err != nil {
errs = append(errs, err)
continue
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
}
return b.WebsocketConn.SendJSONMessage(req)
if errs != nil {
return errs
}
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitstamp) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
req := websocketEventRequest{
Event: "bts:unsubscribe",
Data: websocketData{
Channel: channelToSubscribe.Channel,
},
func (b *Bitstamp) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var errs common.Errors
for i := range channelsToUnsubscribe {
req := websocketEventRequest{
Event: "bts:unsubscribe",
Data: websocketData{
Channel: channelsToUnsubscribe[i].Channel,
},
}
err := b.Websocket.Conn.SendJSONMessage(req)
if err != nil {
errs = append(errs, err)
continue
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
}
return b.WebsocketConn.SendJSONMessage(req)
if errs != nil {
return errs
}
return nil
}
func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, assetType asset.Item) error {
@@ -207,29 +238,22 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair,
bids = append(bids, orderbook.Item{Price: target, Amount: amount})
}
err := b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{
return b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{
Bids: bids,
Asks: asks,
Pair: p,
LastUpdated: time.Unix(update.Timestamp, 0),
AssetType: asset.Spot,
AssetType: assetType,
ExchangeName: b.Name,
})
}
func (b *Bitstamp) seedOrderBook() error {
p, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p,
Asset: assetType,
Exchange: b.Name,
}
return nil
}
func (b *Bitstamp) seedOrderBook() error {
p := b.GetEnabledPairs(asset.Spot)
for x := range p {
orderbookSeed, err := b.GetOrderbook(p[x].String())
if err != nil {
@@ -257,12 +281,6 @@ func (b *Bitstamp) seedOrderBook() error {
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p[x],
Asset: asset.Spot,
Exchange: b.Name,
}
}
return nil
}

View File

@@ -18,8 +18,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -56,17 +56,11 @@ func (b *Bitstamp) SetDefaults() {
b.API.CredentialsValidator.RequiresSecret = true
b.API.CredentialsValidator.RequiresClientID = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
},
requestFmt := &currency.PairFormat{Uppercase: true}
configFmt := &currency.PairFormat{Uppercase: true}
err := b.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
@@ -137,7 +131,7 @@ func (b *Bitstamp) SetDefaults() {
b.API.Endpoints.URLDefault = bitstampAPIURL
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
b.API.Endpoints.WebsocketURL = bitstampWSURL
b.Websocket = wshandler.New()
b.Websocket = stream.New()
b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
@@ -155,34 +149,30 @@ func (b *Bitstamp) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: bitstampWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
Features: &b.Features.Supports.WebsocketCapabilities,
})
err = b.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: bitstampWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
GenerateSubscriptions: b.generateDefaultSubscriptions,
Features: &b.Features.Supports.WebsocketCapabilities,
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
})
if err != nil {
return err
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
URL: b.Websocket.GetWebsocketURL(),
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
return nil
})
}
// Start starts the Bitstamp go routine
@@ -245,32 +235,35 @@ func (b *Bitstamp) UpdateTradablePairs(forceUpdate bool) error {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate)
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
return b.UpdatePairs(p, asset.Spot, false, forceUpdate)
}
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Bitstamp) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
tick, err := b.GetTicker(p.String(), false)
if err != nil {
return tickerPrice, err
return nil, err
}
tickerPrice = &ticker.Price{
Last: tick.Last,
High: tick.High,
Low: tick.Low,
Bid: tick.Bid,
Ask: tick.Ask,
Volume: tick.Volume,
Open: tick.Open,
Pair: p,
LastUpdated: time.Unix(tick.Timestamp, 0),
}
err = ticker.ProcessTicker(b.Name, tickerPrice, assetType)
err = ticker.ProcessTicker(&ticker.Price{
Last: tick.Last,
High: tick.High,
Low: tick.Low,
Bid: tick.Bid,
Ask: tick.Ask,
Volume: tick.Volume,
Open: tick.Open,
Pair: p,
LastUpdated: time.Unix(tick.Timestamp, 0),
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return tickerPrice, err
return nil, err
}
return ticker.GetTicker(b.Name, p, assetType)
@@ -548,11 +541,6 @@ func (b *Bitstamp) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdra
}, nil
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Bitstamp) GetWebsocket() (*wshandler.Websocket, error) {
return b.Websocket, nil
}
// GetActiveOrders retrieves any orders that are active/open
func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) {
var currPair string
@@ -580,6 +568,11 @@ func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
"%s GetActiveOrders unable to parse time: %s\n", b.Name, err)
}
pair, err := currency.NewPairFromString(resp[i].Currency)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
Amount: resp[i].Amount,
ID: strconv.FormatInt(resp[i].ID, 10),
@@ -587,7 +580,7 @@ func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
Type: order.Limit,
Side: orderSide,
Date: tm,
Pair: currency.NewPairFromString(resp[i].Currency),
Pair: pair,
Exchange: b.Name,
})
}
@@ -604,6 +597,12 @@ func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
if len(req.Pairs) == 1 {
currPair = req.Pairs[0].String()
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
resp, err := b.GetUserTransactions(currPair)
if err != nil {
return nil, err
@@ -644,7 +643,7 @@ func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
if quoteCurrency.String() != "" && baseCurrency.String() != "" {
currPair = currency.NewPairWithDelimiter(baseCurrency.String(),
quoteCurrency.String(),
b.GetPairFormat(asset.Spot, false).Delimiter)
format.Delimiter)
}
tm, err := parseTime(resp[i].Date)
@@ -666,30 +665,6 @@ func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Bitstamp) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
b.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Bitstamp) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
b.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitstamp) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitstamp) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *Bitstamp) ValidateCredentials() error {
@@ -712,8 +687,13 @@ func (b *Bitstamp) GetHistoricCandles(pair currency.Pair, a asset.Item, start, e
Interval: interval,
}
formattedPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
candles, err := b.OHLC(
b.FormatExchangeCurrency(pair, a).Lower().String(),
formattedPair.Lower().String(),
start,
end,
b.FormatExchangeKlineInterval(interval),
@@ -759,9 +739,14 @@ func (b *Bitstamp) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item,
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
formattedPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
for x := range dates {
candles, err := b.OHLC(
b.FormatExchangeCurrency(pair, a).Lower().String(),
formattedPair.Lower().String(),
dates[x].Start,
dates[x].End,
b.FormatExchangeKlineInterval(interval),

View File

@@ -343,14 +343,19 @@ func TestFormatWithdrawPermissions(t *testing.T) {
}
func TestGetActiveOrders(t *testing.T) {
p, err := currency.NewPairFromString(currPair)
if err != nil {
t.Fatal(err)
}
var getOrdersRequest = order.GetOrdersRequest{
Type: order.AnyType,
Pairs: []currency.Pair{currency.NewPairFromString(currPair)},
Pairs: []currency.Pair{p},
}
getOrdersRequest.Pairs[0].Delimiter = "-"
_, err := b.GetActiveOrders(&getOrdersRequest)
_, err = b.GetActiveOrders(&getOrdersRequest)
if areTestAPIKeysSet() && err != nil {
t.Errorf("Could not get open orders: %s", err)
} else if !areTestAPIKeysSet() && err == nil {

View File

@@ -18,7 +18,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -54,19 +53,11 @@ func (b *Bittrex) SetDefaults() {
b.API.CredentialsValidator.RequiresKey = true
b.API.CredentialsValidator.RequiresSecret = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Delimiter: "-",
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Delimiter: "-",
Uppercase: true,
},
requestFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
err := b.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
@@ -113,7 +104,6 @@ func (b *Bittrex) Setup(exch *config.ExchangeConfig) error {
b.SetEnabled(false)
return nil
}
return b.SetupDefaults(exch)
}
@@ -133,24 +123,53 @@ func (b *Bittrex) Run() {
}
forceUpdate := false
delim := b.GetPairFormat(asset.Spot, false).Delimiter
if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), delim) ||
!common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), delim) {
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
return
}
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
return
}
avail, err := b.GetAvailablePairs(asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
return
}
if !common.StringDataContains(pairs.Strings(), format.Delimiter) ||
!common.StringDataContains(avail.Strings(), format.Delimiter) {
forceUpdate = true
log.Warn(log.ExchangeSys, "Available pairs for Bittrex reset due to config upgrade, please enable the ones you would like again")
err := b.UpdatePairs(currency.NewPairsFromStrings(
[]string{currency.USDT.String() + delim + currency.BTC.String()},
),
asset.Spot,
true,
true,
)
pairs, err = currency.NewPairsFromStrings([]string{currency.USDT.String() +
format.Delimiter +
currency.BTC.String()})
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
} else {
err = b.UpdatePairs(pairs, asset.Spot, true, true)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
}
}
}
@@ -158,7 +177,7 @@ func (b *Bittrex) Run() {
return
}
err := b.UpdateTradablePairs(forceUpdate)
err = b.UpdateTradablePairs(forceUpdate)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
@@ -192,8 +211,12 @@ func (b *Bittrex) UpdateTradablePairs(forceUpdate bool) error {
if err != nil {
return err
}
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate)
return b.UpdatePairs(p, asset.Spot, false, forceUpdate)
}
// UpdateAccountInfo Retrieves balances for all enabled currencies for the
@@ -239,41 +262,46 @@ func (b *Bittrex) FetchAccountInfo() (account.Holdings, error) {
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Bittrex) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
ticks, err := b.GetMarketSummaries()
if err != nil {
return tickerPrice, err
}
pairs := b.GetEnabledPairs(assetType)
for i := range pairs {
for j := range ticks.Result {
if !strings.EqualFold(ticks.Result[j].MarketName, pairs[i].String()) {
continue
}
tickerTime, err := parseTime(ticks.Result[j].TimeStamp)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s UpdateTicker unable to parse time: %s\n", b.Name, err)
}
tickerPrice = &ticker.Price{
Last: ticks.Result[j].Last,
High: ticks.Result[j].High,
Low: ticks.Result[j].Low,
Bid: ticks.Result[j].Bid,
Ask: ticks.Result[j].Ask,
Volume: ticks.Result[j].BaseVolume,
QuoteVolume: ticks.Result[j].Volume,
Close: ticks.Result[j].PrevDay,
Pair: pairs[i],
LastUpdated: tickerTime,
}
err = ticker.ProcessTicker(b.Name, tickerPrice, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
}
return nil, err
}
pairs, err := b.GetEnabledPairs(assetType)
if err != nil {
return nil, err
}
for j := range ticks.Result {
cp, err := currency.NewPairFromString(ticks.Result[j].MarketName)
if err != nil {
return nil, err
}
if !pairs.Contains(cp, true) {
continue
}
tickerTime, err := parseTime(ticks.Result[j].TimeStamp)
if err != nil {
return nil, err
}
err = ticker.ProcessTicker(&ticker.Price{
Last: ticks.Result[j].Last,
High: ticks.Result[j].High,
Low: ticks.Result[j].Low,
Bid: ticks.Result[j].Bid,
Ask: ticks.Result[j].Ask,
Volume: ticks.Result[j].BaseVolume,
QuoteVolume: ticks.Result[j].Volume,
Close: ticks.Result[j].PrevDay,
Pair: cp,
LastUpdated: tickerTime,
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return nil, err
}
}
return ticker.GetTicker(b.Name, p, assetType)
}
@@ -297,12 +325,17 @@ func (b *Bittrex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
orderbookNew, err := b.GetOrderbook(b.FormatExchangeCurrency(p, assetType).String())
fpair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return orderBook, err
return nil, err
}
orderbookNew, err := b.GetOrderbook(fpair.String())
if err != nil {
return nil, err
}
orderBook := new(orderbook.Base)
for x := range orderbookNew.Result.Buy {
orderBook.Bids = append(orderBook.Bids,
orderbook.Item{
@@ -453,11 +486,6 @@ func (b *Bittrex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *Bittrex) GetWebsocket() (*wshandler.Websocket, error) {
return nil, common.ErrNotYetImplemented
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (b *Bittrex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
if !b.AllowAuthenticatedRequest() && // Todo check connection status
@@ -474,6 +502,11 @@ func (b *Bittrex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
currPair = req.Pairs[0].String()
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
resp, err := b.GetOpenOrders(currPair)
if err != nil {
return nil, err
@@ -491,8 +524,16 @@ func (b *Bittrex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail,
resp.Result[i].Opened)
}
pair := currency.NewPairDelimiter(resp.Result[i].Exchange,
b.GetPairFormat(asset.Spot, false).Delimiter)
pair, err := currency.NewPairDelimiter(resp.Result[i].Exchange,
format.Delimiter)
if err != nil {
log.Errorf(log.ExchangeSys,
"Exchange %v Func %v Order %v Could not parse currency pair %v",
b.Name,
"GetActiveOrders",
resp.Result[i].OrderUUID,
err)
}
orderType := order.Type(strings.ToUpper(resp.Result[i].Type))
orders = append(orders, order.Detail{
@@ -521,6 +562,11 @@ func (b *Bittrex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
currPair = req.Pairs[0].String()
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
resp, err := b.GetOrderHistoryForCurrency(currPair)
if err != nil {
return nil, err
@@ -533,13 +579,21 @@ func (b *Bittrex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
log.Errorf(log.ExchangeSys,
"Exchange %v Func %v Order %v Could not parse date to unix with value of %v",
b.Name,
"GetActiveOrders",
"GetOrderHistory",
resp.Result[i].OrderUUID,
resp.Result[i].Opened)
}
pair := currency.NewPairDelimiter(resp.Result[i].Exchange,
b.GetPairFormat(asset.Spot, false).Delimiter)
pair, err := currency.NewPairDelimiter(resp.Result[i].Exchange,
format.Delimiter)
if err != nil {
log.Errorf(log.ExchangeSys,
"Exchange %v Func %v Order %v Could not parse currency pair %v",
b.Name,
"GetOrderHistory",
resp.Result[i].OrderUUID,
err)
}
orderType := order.Type(strings.ToUpper(resp.Result[i].Type))
orders = append(orders, order.Detail{
@@ -561,28 +615,6 @@ func (b *Bittrex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail,
return orders, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Bittrex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Bittrex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bittrex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bittrex) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *Bittrex) ValidateCredentials() error {

View File

@@ -18,7 +18,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
const (
@@ -72,13 +71,12 @@ const (
heartbeat = "heartbeat"
tick = "tick"
wsOB = "orderbookUpdate"
tradeEndPoint = "tradeEndPoint"
tradeEndPoint = "trade"
)
// BTCMarkets is the overarching type across the BTCMarkets package
type BTCMarkets struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
}
// GetMarkets returns the BTCMarkets instruments
@@ -191,7 +189,7 @@ func (b *BTCMarkets) GetMarketCandles(marketID, timeWindow string, from, to time
}
// GetTickers gets multiple tickers
func (b *BTCMarkets) GetTickers(marketIDs []currency.Pair) ([]Ticker, error) {
func (b *BTCMarkets) GetTickers(marketIDs currency.Pairs) ([]Ticker, error) {
var tickers []Ticker
params := url.Values{}
for x := range marketIDs {
@@ -761,7 +759,11 @@ func (b *BTCMarkets) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
return fee, err
}
for x := range temp.FeeByMarkets {
if currency.NewPairFromString(temp.FeeByMarkets[x].MarketID) == feeBuilder.Pair {
p, err := currency.NewPairFromString(temp.FeeByMarkets[x].MarketID)
if err != nil {
return 0, err
}
if p == feeBuilder.Pair {
fee = temp.FeeByMarkets[x].MakerFeeRate
if !feeBuilder.IsMaker {
fee = temp.FeeByMarkets[x].TakerFeeRate

View File

@@ -44,21 +44,17 @@ func TestMain(m *testing.M) {
bConfig.API.Credentials.Key = apiKey
bConfig.API.Credentials.Secret = apiSecret
bConfig.API.AuthenticatedSupport = true
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(bConfig)
if err != nil {
log.Fatal(err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
err = b.ValidateCredentials()
if err != nil {
fmt.Println("API credentials are invalid:", err)
b.API.AuthenticatedSupport = false
b.API.AuthenticatedWebsocketSupport = false
}
os.Exit(m.Run())
}
@@ -108,8 +104,11 @@ func TestGetMarketCandles(t *testing.T) {
func TestGetTickers(t *testing.T) {
t.Parallel()
temp := currency.NewPairsFromStrings([]string{LTCAUD, BTCAUD})
_, err := b.GetTickers(temp)
temp, err := currency.NewPairsFromStrings([]string{LTCAUD, BTCAUD})
if err != nil {
t.Fatal(err)
}
_, err = b.GetTickers(temp)
if err != nil {
t.Error(err)
}
@@ -720,8 +719,11 @@ func TestWsOrders(t *testing.T) {
}
func TestBTCMarkets_GetHistoricCandles(t *testing.T) {
p := currency.NewPairFromString(BTCAUD)
_, err := b.GetHistoricCandles(p, asset.Spot, time.Now().Add(-time.Hour*24).UTC(), time.Now().UTC(), kline.OneHour)
p, err := currency.NewPairFromString(BTCAUD)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandles(p, asset.Spot, time.Now().Add(-time.Hour*24).UTC(), time.Now().UTC(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
@@ -736,8 +738,11 @@ func TestBTCMarkets_GetHistoricCandles(t *testing.T) {
func TestBTCMarkets_GetHistoricCandlesExtended(t *testing.T) {
start := time.Now().AddDate(0, 0, -1001)
end := time.Now()
p := currency.NewPairFromString(BTCAUD)
_, err := b.GetHistoricCandlesExtended(p, asset.Spot, start, end, kline.OneDay)
p, err := currency.NewPairFromString(BTCAUD)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandlesExtended(p, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}

View File

@@ -337,21 +337,14 @@ type TradingFeeResponse struct {
FeeByMarkets []TradingFeeData `json:"FeeByMarkets"`
}
// WsSubscribe message sent via ws to subscribe
// WsSubscribe defines a subscription message used in the Subscribe function
type WsSubscribe struct {
MarketIDs []string `json:"marketIds,omitempty"`
Channels []string `json:"channels"`
MessageType string `json:"messageType"`
}
// WsAuthSubscribe message sent via login to subscribe
type WsAuthSubscribe struct {
MarketIDs []string `json:"marketIds,omitempty"`
Channels []string `json:"channels"`
Key string `json:"key"`
Signature string `json:"signature"`
Timestamp string `json:"timestamp"`
MessageType string `json:"messageType"`
Channels []string `json:"channels,omitempty"`
Key string `json:"key,omitempty"`
Signature string `json:"signature,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
MessageType string `json:"messageType,omitempty"`
}
// WsMessageType message sent via ws to determine type
@@ -380,6 +373,7 @@ type WsTrade struct {
TradeID int64 `json:"tradeId"`
Price float64 `json:"price,string"`
Volume float64 `json:"volume,string"`
Side string `json:"side"`
MessageType string `json:"messageType"`
}

View File

@@ -12,13 +12,12 @@ import (
"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/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -29,10 +28,10 @@ const (
// WsConnect connects to a websocket feed
func (b *BTCMarkets) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.WebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
@@ -40,35 +39,26 @@ func (b *BTCMarkets) WsConnect() error {
log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name)
}
go b.wsReadData()
if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
b.createChannels()
subs, err := b.generateDefaultSubscriptions()
if err != nil {
return err
}
b.generateDefaultSubscriptions()
return nil
return b.Websocket.SubscribeToChannels(subs)
}
// wsReadData receives and passes on websocket messages for processing
func (b *BTCMarkets) wsReadData() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
default:
resp, err := b.WebsocketConn.ReadMessage()
if err != nil {
b.Websocket.ReadMessageErrors <- err
return
}
b.Websocket.TrafficAlert <- struct{}{}
err = b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
@@ -91,7 +81,11 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
return err
}
p := currency.NewPairFromString(ob.Currency)
p, err := currency.NewPairFromString(ob.Currency)
if err != nil {
return err
}
var bids, asks []orderbook.Item
for x := range ob.Bids {
var price, amount float64
@@ -135,7 +129,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
ExchangeName: b.Name,
})
} else {
err = b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
err = b.Websocket.Orderbook.Update(&buffer.Update{
UpdateTime: ob.Timestamp,
Asset: asset.Spot,
Bids: bids,
@@ -147,26 +141,31 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p,
Asset: asset.Spot,
Exchange: b.Name,
}
case tradeEndPoint:
var trade WsTrade
err := json.Unmarshal(respRaw, &trade)
if err != nil {
return err
}
p := currency.NewPairFromString(trade.Currency)
b.Websocket.DataHandler <- wshandler.TradeData{
p, err := currency.NewPairFromString(trade.Currency)
if err != nil {
return err
}
side := order.Buy
if trade.Side == "Ask" {
side = order.Sell
}
b.Websocket.DataHandler <- stream.TradeData{
Timestamp: trade.Timestamp,
CurrencyPair: p,
AssetType: asset.Spot,
Exchange: b.Name,
Price: trade.Price,
Amount: trade.Volume,
Side: order.UnknownSide,
Side: side,
EventType: order.UnknownType,
}
case tick:
@@ -176,7 +175,10 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
return err
}
p := currency.NewPairFromString(tick.Currency)
p, err := currency.NewPairFromString(tick.Currency)
if err != nil {
return err
}
b.Websocket.DataHandler <- &ticker.Price{
ExchangeName: b.Name,
@@ -247,12 +249,16 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
Err: err,
}
}
p := currency.NewPairFromString(orderData.MarketID)
var a asset.Item
a, err = b.GetPairAssetType(p)
p, err := currency.NewPairFromString(orderData.MarketID)
if err != nil {
return err
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
b.Websocket.DataHandler <- &order.Detail{
Price: price,
Amount: originalAmount,
@@ -263,7 +269,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
AssetType: asset.Spot,
Date: orderData.Timestamp,
Trades: trades,
Pair: p,
@@ -276,91 +282,80 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
}
return fmt.Errorf("%v websocket error. Code: %v Message: %v", b.Name, wsErr.Code, wsErr.Message)
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
return nil
}
return nil
}
func (b *BTCMarkets) generateDefaultSubscriptions() {
var channels = []string{tick, tradeEndPoint, wsOB}
enabledCurrencies := b.GetEnabledPairs(asset.Spot)
var subscriptions []wshandler.WebsocketChannelSubscription
func (b *BTCMarkets) generateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{wsOB, tick, tradeEndPoint}
enabledCurrencies, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
Asset: asset.Spot,
})
}
}
b.Websocket.SubscribeToChannels(subscriptions)
var authChannels = []string{fundChange, heartbeat, orderChange}
if b.Websocket.CanUseAuthenticatedEndpoints() {
for i := range authChannels {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: authChannels[i],
})
}
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *BTCMarkets) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
unauthChannels := []string{tick, tradeEndPoint, wsOB}
authChannels := []string{fundChange, heartbeat, orderChange}
switch {
case common.StringDataCompare(unauthChannels, channelToSubscribe.Channel):
req := WsSubscribe{
MarketIDs: []string{b.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String()},
Channels: []string{channelToSubscribe.Channel},
MessageType: subscribe,
}
err := b.WebsocketConn.SendJSONMessage(req)
if err != nil {
return err
}
case common.StringDataCompare(authChannels, channelToSubscribe.Channel):
message, ok := channelToSubscribe.Params["AuthSub"].(WsAuthSubscribe)
if !ok {
return errors.New("invalid params data")
}
tempAuthData := b.generateAuthSubscriptions()
message.Channels = append(message.Channels, channelToSubscribe.Channel, heartbeat)
message.Key = tempAuthData.Key
message.Signature = tempAuthData.Signature
message.Timestamp = tempAuthData.Timestamp
err := b.WebsocketConn.SendJSONMessage(message)
if err != nil {
return err
func (b *BTCMarkets) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var authChannels = []string{fundChange, heartbeat, orderChange}
var payload WsSubscribe
payload.MessageType = subscribe
for i := range channelsToSubscribe {
payload.Channels = append(payload.Channels,
channelsToSubscribe[i].Channel)
if channelsToSubscribe[i].Currency.String() != "" {
if !common.StringDataCompare(payload.MarketIDs,
channelsToSubscribe[i].Currency.String()) {
payload.MarketIDs = append(payload.MarketIDs,
channelsToSubscribe[i].Currency.String())
}
}
}
for i := range authChannels {
if !common.StringDataCompare(payload.Channels, authChannels[i]) {
continue
}
signTime := strconv.FormatInt(time.Now().UTC().UnixNano()/1000000, 10)
strToSign := "/users/self/subscribe" + "\n" + signTime
tempSign := crypto.GetHMAC(crypto.HashSHA512,
[]byte(strToSign),
[]byte(b.API.Credentials.Secret))
sign := crypto.Base64Encode(tempSign)
payload.Key = b.API.Credentials.Key
payload.Signature = sign
payload.Timestamp = signTime
break
}
err := b.Websocket.Conn.SendJSONMessage(payload)
if err != nil {
return err
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
return nil
}
// Login logs in allowing private ws events
func (b *BTCMarkets) generateAuthSubscriptions() WsAuthSubscribe {
var authSubInfo WsAuthSubscribe
signTime := strconv.FormatInt(time.Now().UTC().UnixNano()/1000000, 10)
strToSign := "/users/self/subscribe" + "\n" + signTime
tempSign := crypto.GetHMAC(crypto.HashSHA512,
[]byte(strToSign),
[]byte(b.API.Credentials.Secret))
sign := crypto.Base64Encode(tempSign)
authSubInfo.Key = b.API.Credentials.Key
authSubInfo.Signature = sign
authSubInfo.Timestamp = signTime
return authSubInfo
}
// createChannels creates channels that need to be
func (b *BTCMarkets) createChannels() {
tempChannels := []string{orderChange, fundChange}
var channels []wshandler.WebsocketChannelSubscription
pairArray := b.GetEnabledPairs(asset.Spot)
for y := range tempChannels {
for x := range pairArray {
var authSub WsAuthSubscribe
var channel wshandler.WebsocketChannelSubscription
channel.Params = make(map[string]interface{})
channel.Channel = tempChannels[y]
authSub.MarketIDs = append(authSub.MarketIDs, b.FormatExchangeCurrency(pairArray[x], asset.Spot).String())
authSub.MessageType = subscribe
channel.Params["AuthSub"] = authSub
channels = append(channels, channel)
}
}
b.Websocket.SubscribeToChannels(channels)
}

View File

@@ -19,8 +19,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -59,19 +59,11 @@ func (b *BTCMarkets) SetDefaults() {
b.API.Endpoints.URLDefault = btcMarketsAPIURL
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Delimiter: "-",
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Delimiter: "-",
Uppercase: true,
},
requestFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
err := b.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
@@ -131,7 +123,7 @@ func (b *BTCMarkets) SetDefaults() {
request.WithLimiter(SetRateLimit()))
b.API.Endpoints.WebsocketURL = btcMarketsWSURL
b.Websocket = wshandler.New()
b.Websocket = stream.New()
b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
@@ -149,41 +141,30 @@ func (b *BTCMarkets) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: btcMarketsWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
Features: &b.Features.Supports.WebsocketCapabilities,
})
err = b.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: btcMarketsWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
GenerateSubscriptions: b.generateDefaultSubscriptions,
Features: &b.Features.Supports.WebsocketCapabilities,
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
BufferEnabled: true,
SortBuffer: true,
})
if err != nil {
return err
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: b.Websocket.GetWebsocketURL(),
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
true,
true,
false,
false,
exch.Name)
return nil
})
}
// Start starts the BTC Markets go routine
@@ -205,10 +186,34 @@ func (b *BTCMarkets) Run() {
btcMarketsWSURL)
b.PrintEnabledPairs()
}
forceUpdate := false
delim := b.GetPairFormat(asset.Spot, false).Delimiter
if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), delim) ||
!common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), delim) {
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s Failed to update enabled currencies Err:%s\n",
b.Name,
err)
return
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s Failed to update enabled currencies.\n",
b.Name)
return
}
avail, err := b.GetAvailablePairs(asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s Failed to update enabled currencies.\n",
b.Name)
return
}
if !common.StringDataContains(pairs.Strings(), format.Delimiter) ||
!common.StringDataContains(avail.Strings(), format.Delimiter) {
log.Warnln(log.ExchangeSys, "Available pairs for BTC Markets reset due to config upgrade, please enable the pairs you would like again.")
forceUpdate = true
}
@@ -216,10 +221,10 @@ func (b *BTCMarkets) Run() {
enabledPairs := currency.Pairs{currency.Pair{
Base: currency.BTC.Lower(),
Quote: currency.AUD.Lower(),
Delimiter: delim,
Delimiter: format.Delimiter,
},
}
err := b.UpdatePairs(enabledPairs, asset.Spot, true, true)
err = b.UpdatePairs(enabledPairs, asset.Spot, true, true)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s Failed to update enabled currencies.\n",
@@ -231,7 +236,7 @@ func (b *BTCMarkets) Run() {
return
}
err := b.UpdateTradablePairs(forceUpdate)
err = b.UpdateTradablePairs(forceUpdate)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
@@ -264,28 +269,49 @@ func (b *BTCMarkets) UpdateTradablePairs(forceUpdate bool) error {
if err != nil {
return err
}
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate)
return b.UpdatePairs(p, asset.Spot, false, forceUpdate)
}
// UpdateTicker updates and returns the ticker for a currency pair
func (b *BTCMarkets) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
allPairs := b.GetEnabledPairs(assetType)
tickers, err := b.GetTickers(allPairs.Slice())
allPairs, err := b.GetEnabledPairs(assetType)
if err != nil {
return nil, err
}
tickers, err := b.GetTickers(allPairs)
if err != nil {
return nil, err
}
if len(allPairs) != len(tickers) {
return nil, errors.New("enabled pairs differ from returned tickers")
}
for x := range tickers {
var resp ticker.Price
resp.Pair = currency.NewPairFromString(tickers[x].MarketID)
resp.Last = tickers[x].LastPrice
resp.High = tickers[x].High24h
resp.Low = tickers[x].Low24h
resp.Bid = tickers[x].BestBID
resp.Ask = tickers[x].BestAsk
resp.Volume = tickers[x].Volume
resp.LastUpdated = time.Now()
err = ticker.ProcessTicker(b.Name, &resp, assetType)
var newP currency.Pair
newP, err = currency.NewPairFromString(tickers[x].MarketID)
if err != nil {
return nil, err
}
err = ticker.ProcessTicker(&ticker.Price{
Pair: newP,
Last: tickers[x].LastPrice,
High: tickers[x].High24h,
Low: tickers[x].Low24h,
Bid: tickers[x].BestBID,
Ask: tickers[x].BestAsk,
Volume: tickers[x].Volume,
LastUpdated: time.Now(),
ExchangeName: b.Name,
AssetType: assetType,
})
if err != nil {
return nil, err
}
@@ -313,11 +339,17 @@ func (b *BTCMarkets) FetchOrderbook(p currency.Pair, assetType asset.Item) (*ord
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *BTCMarkets) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
tempResp, err := b.GetOrderbook(b.FormatExchangeCurrency(p, assetType).String(), 2)
fpair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return orderBook, err
return nil, err
}
tempResp, err := b.GetOrderbook(fpair.String(), 2)
if err != nil {
return nil, err
}
orderBook := new(orderbook.Base)
for x := range tempResp.Bids {
orderBook.Bids = append(orderBook.Bids, orderbook.Item{
Amount: tempResp.Bids[x].Volume,
@@ -401,7 +433,12 @@ func (b *BTCMarkets) SubmitOrder(s *order.Submit) (order.SubmitResponse, error)
s.Side = order.Bid
}
tempResp, err := b.NewOrder(b.FormatExchangeCurrency(s.Pair, asset.Spot).String(),
fpair, err := b.FormatExchangeCurrency(s.Pair, asset.Spot)
if err != nil {
return resp, err
}
tempResp, err := b.NewOrder(fpair.String(),
s.Price,
s.Amount,
s.Type.String(),
@@ -468,9 +505,15 @@ func (b *BTCMarkets) GetOrderInfo(orderID string) (order.Detail, error) {
if err != nil {
return resp, err
}
p, err := currency.NewPairFromString(o.MarketID)
if err != nil {
return order.Detail{}, err
}
resp.Exchange = b.Name
resp.ID = orderID
resp.Pair = currency.NewPairFromString(o.MarketID)
resp.Pair = p
resp.Price = o.Price
resp.Date = o.CreationTime
resp.ExecutedAmount = o.Amount - o.OpenAmount
@@ -569,11 +612,6 @@ func (b *BTCMarkets) WithdrawFiatFundsToInternationalBank(withdrawRequest *withd
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *BTCMarkets) GetWebsocket() (*wshandler.Websocket, error) {
return b.Websocket, nil
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (b *BTCMarkets) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
if !b.AllowAuthenticatedRequest() && // Todo check connection status
@@ -586,7 +624,10 @@ func (b *BTCMarkets) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, err
// GetActiveOrders retrieves any orders that are active/open
func (b *BTCMarkets) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) {
if len(req.Pairs) == 0 {
allPairs := b.GetEnabledPairs(asset.Spot)
allPairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
for a := range allPairs {
req.Pairs = append(req.Pairs,
allPairs[a])
@@ -595,7 +636,11 @@ func (b *BTCMarkets) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detai
var resp []order.Detail
for x := range req.Pairs {
tempData, err := b.GetOrders(b.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String(), -1, -1, -1, true)
fpair, err := b.FormatExchangeCurrency(req.Pairs[x], asset.Spot)
if err != nil {
return nil, err
}
tempData, err := b.GetOrders(fpair.String(), -1, -1, -1, true)
if err != nil {
return resp, err
}
@@ -666,7 +711,12 @@ func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai
}
}
for y := range req.Pairs {
orders, err := b.GetOrders(b.FormatExchangeCurrency(req.Pairs[y], asset.Spot).String(), -1, -1, -1, false)
fpair, err := b.FormatExchangeCurrency(req.Pairs[y], asset.Spot)
if err != nil {
return nil, err
}
orders, err := b.GetOrders(fpair.String(), -1, -1, -1, false)
if err != nil {
return resp, err
}
@@ -697,8 +747,14 @@ func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai
case orderAccepted:
continue
}
p, err := currency.NewPairFromString(tempData.Orders[c].MarketID)
if err != nil {
return nil, err
}
tempResp.Exchange = b.Name
tempResp.Pair = currency.NewPairFromString(tempData.Orders[c].MarketID)
tempResp.Pair = p
tempResp.Side = order.Bid
if tempData.Orders[c].Side == ask {
tempResp.Side = order.Ask
@@ -713,28 +769,6 @@ func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai
return resp, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *BTCMarkets) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *BTCMarkets) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *BTCMarkets) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *BTCMarkets) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *BTCMarkets) ValidateCredentials() error {
@@ -776,7 +810,12 @@ func (b *BTCMarkets) GetHistoricCandles(pair currency.Pair, a asset.Item, start,
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
candles, err := b.GetMarketCandles(b.FormatExchangeCurrency(pair, a).String(),
formattedPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
candles, err := b.GetMarketCandles(formattedPair.String(),
b.FormatExchangeKlineInterval(interval),
start,
end,
@@ -789,7 +828,7 @@ func (b *BTCMarkets) GetHistoricCandles(pair currency.Pair, a asset.Item, start,
}
ret := kline.Item{
Exchange: b.Name,
Pair: b.FormatExchangeCurrency(pair, a),
Pair: formattedPair,
Asset: asset.Spot,
Interval: interval,
}

View File

@@ -15,19 +15,18 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
)
// BTSE is the overarching type across this package
type BTSE struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
}
const (
btseAPIURL = "https://api.btse.com"
btseAPIPath = "/spot/v2/"
btseAPIURL = "https://api.btse.com"
btseSPOTAPIPath = "/spot/v2/"
btseFuturesAPIPath = "/futures/api/v2.1/"
// Public endpoints
btseMarketOverview = "market_summary"
@@ -50,34 +49,40 @@ const (
// GetMarketsSummary stores market summary data
func (b *BTSE) GetMarketsSummary() (*HighLevelMarketData, error) {
var m HighLevelMarketData
return &m, b.SendHTTPRequest(http.MethodGet, btseMarketOverview, &m)
return &m, b.SendHTTPRequest(http.MethodGet, btseMarketOverview, &m, true)
}
// GetMarkets returns a list of markets available on BTSE
func (b *BTSE) GetMarkets() ([]Market, error) {
var m []Market
return m, b.SendHTTPRequest(http.MethodGet, btseMarkets, &m)
// GetSpotMarkets returns a list of spot markets available on BTSE
func (b *BTSE) GetSpotMarkets() ([]SpotMarket, error) {
var m []SpotMarket
return m, b.SendHTTPRequest(http.MethodGet, btseMarkets, &m, true)
}
// GetFuturesMarkets returns a list of futures markets available on BTSE
func (b *BTSE) GetFuturesMarkets() ([]FuturesMarket, error) {
var m []FuturesMarket
return m, b.SendHTTPRequest(http.MethodGet, btseMarketOverview, &m, false)
}
// FetchOrderBook gets orderbook data for a given pair
func (b *BTSE) FetchOrderBook(symbol string) (*Orderbook, error) {
var o Orderbook
endpoint := fmt.Sprintf("%s/%s", btseOrderbook, symbol)
return &o, b.SendHTTPRequest(http.MethodGet, endpoint, &o)
return &o, b.SendHTTPRequest(http.MethodGet, endpoint, &o, true)
}
// GetTrades returns a list of trades for the specified symbol
func (b *BTSE) GetTrades(symbol string) ([]Trade, error) {
var t []Trade
endpoint := fmt.Sprintf("%s/%s", btseTrades, symbol)
return t, b.SendHTTPRequest(http.MethodGet, endpoint, &t)
return t, b.SendHTTPRequest(http.MethodGet, endpoint, &t, true)
}
// GetTicker returns the ticker for a specified symbol
func (b *BTSE) GetTicker(symbol string) (*Ticker, error) {
var t Ticker
endpoint := fmt.Sprintf("%s/%s", btseTicker, symbol)
err := b.SendHTTPRequest(http.MethodGet, endpoint, &t)
err := b.SendHTTPRequest(http.MethodGet, endpoint, &t, true)
if err != nil {
return nil, err
}
@@ -88,19 +93,19 @@ func (b *BTSE) GetTicker(symbol string) (*Ticker, error) {
func (b *BTSE) GetMarketStatistics(symbol string) (*MarketStatistics, error) {
var m MarketStatistics
endpoint := fmt.Sprintf("%s/%s", btseStats, symbol)
return &m, b.SendHTTPRequest(http.MethodGet, endpoint, &m)
return &m, b.SendHTTPRequest(http.MethodGet, endpoint, &m, true)
}
// GetServerTime returns the exchanges server time
func (b *BTSE) GetServerTime() (*ServerTime, error) {
var s ServerTime
return &s, b.SendHTTPRequest(http.MethodGet, btseTime, &s)
return &s, b.SendHTTPRequest(http.MethodGet, btseTime, &s, true)
}
// GetAccountBalance returns the users account balance
func (b *BTSE) GetAccountBalance() ([]CurrencyBalance, error) {
var a []CurrencyBalance
return a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseAccount, nil, &a)
return a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseAccount, nil, &a, true)
}
// CreateOrder creates an order
@@ -129,7 +134,7 @@ func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeI
}
var r orderResp
return &r.ID, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseOrder, req, &r)
return &r.ID, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseOrder, req, &r, true)
}
// GetOrders returns all pending orders
@@ -139,7 +144,7 @@ func (b *BTSE) GetOrders(symbol string) ([]OpenOrder, error) {
req["symbol"] = symbol
}
var o []OpenOrder
return o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, req, &o)
return o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, req, &o, true)
}
// CancelExistingOrder cancels an order
@@ -148,7 +153,7 @@ func (b *BTSE) CancelExistingOrder(orderID, symbol string) (*CancelOrder, error)
req := make(map[string]interface{})
req["order_id"] = orderID
req["symbol"] = symbol
return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrder, req, &c)
return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrder, req, &c, true)
}
// GetFills gets all filled orders
@@ -184,14 +189,18 @@ func (b *BTSE) GetFills(orderID, symbol, before, after, limit, username string)
}
var o []FilledOrder
return o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseFills, req, &o)
return o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseFills, req, &o, true)
}
// SendHTTPRequest sends an HTTP request to the desired endpoint
func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}) error {
func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}, spotEndpoint bool) error {
p := btseSPOTAPIPath
if !spotEndpoint {
p = btseFuturesAPIPath
}
return b.SendPayload(context.Background(), &request.Item{
Method: method,
Path: b.API.Endpoints.URL + btseAPIPath + endpoint,
Path: b.API.Endpoints.URL + p + endpoint,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
@@ -200,12 +209,18 @@ func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}) erro
}
// SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the desired endpoint
func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[string]interface{}, result interface{}) error {
func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[string]interface{}, result interface{}, spotEndpoint bool) error {
if !b.AllowAuthenticatedRequest() {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet,
b.Name)
}
path := btseAPIPath + endpoint
p := btseSPOTAPIPath
if !spotEndpoint {
p = btseFuturesAPIPath
}
path := p + endpoint
headers := make(map[string]string)
headers["btse-api"] = b.API.Credentials.Key
nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)

View File

@@ -39,13 +39,11 @@ func TestMain(m *testing.M) {
btseConfig.API.AuthenticatedSupport = true
btseConfig.API.Credentials.Key = apiKey
btseConfig.API.Credentials.Secret = apiSecret
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(btseConfig)
if err != nil {
log.Fatal(err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
os.Exit(m.Run())
}
@@ -61,9 +59,17 @@ func TestGetMarketsSummary(t *testing.T) {
}
}
func TestGetMarkets(t *testing.T) {
func TestGetSpotMarkets(t *testing.T) {
t.Parallel()
_, err := b.GetMarkets()
_, err := b.GetSpotMarkets()
if err != nil {
t.Error(err)
}
}
func TestGetFuturesMarkets(t *testing.T) {
t.Parallel()
_, err := b.GetFuturesMarkets()
if err != nil {
t.Error(err)
}
@@ -411,3 +417,16 @@ func TestStatusToStandardStatus(t *testing.T) {
}
}
}
func TestFetchTradablePairs(t *testing.T) {
assets := b.GetAssetTypes()
for i := range assets {
data, err := b.FetchTradablePairs(assets[i])
if err != nil {
t.Fatal(err)
}
if len(data) == 0 {
t.Fatal("data cannot be zero")
}
}
}

View File

@@ -21,8 +21,8 @@ type OverviewData struct {
// HighLevelMarketData stores market overview data
type HighLevelMarketData map[string]OverviewData
// Market stores market data
type Market struct {
// SpotMarket stores market data
type SpotMarket struct {
Symbol string `json:"symbol"`
ID string `json:"id"`
BaseCurrency string `json:"base_currency"`
@@ -35,6 +35,41 @@ type Market struct {
Status string `json:"status"`
}
// FuturesMarket stores market data
type FuturesMarket struct {
Symbol string `json:"symbol"`
Last float64 `json:"last"`
LowestAsk float64 `json:"lowestAsk"`
HighestBid float64 `json:"highestBid"`
OpenInterest float64 `json:"openInterest"`
OpenInterestUSD float64 `json:"openInterestUSD"`
PercentageChange float64 `json:"percentageChange"`
Volume float64 `json:"volume"`
High24Hr float64 `json:"high24Hr"`
Low24Hr float64 `json:"low24Hr"`
Base string `json:"base"`
Quote string `json:"quote"`
ContractStart int64 `json:"contractStart"`
ContractEnd int64 `json:"contractEnd"`
Active bool `json:"active"`
TimeBasedContract bool `json:"timeBasedContract"`
OpenTime int64 `json:"openTime"`
CloseTime int64 `json:"closeTime"`
StartMatching int64 `json:"startMatching"`
InactiveTime int64 `json:"inactiveTime"`
FundingRate float64 `json:"fundingRate"`
ContractSize float64 `json:"contractSize"`
MaxPosition int64 `json:"maxPosition"`
MinValidPrice float64 `json:"minValidPrice"`
MinPriceIncrement float64 `json:"minPriceIncrement"`
MinOrderSize int32 `json:"minOrderSize"`
MaxOrderSize int32 `json:"maxOrderSize"`
MinRiskLimit int32 `json:"minRiskLimit"`
MaxRiskLimit int32 `json:"maxRiskLimit"`
MinSizeIncrement float64 `json:"minSizeIncrement"`
AvailableSettlement []string `json:"availableSettlement"`
}
// Trade stores trade data
type Trade struct {
SerialID string `json:"serial_id"`

View File

@@ -16,25 +16,25 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
)
const (
btseWebsocket = "wss://ws.btse.com/spotWS"
btseWebsocketTimer = 57 * time.Second
btseWebsocketTimer = time.Second * 57
)
// WsConnect connects the websocket client
func (b *BTSE) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.WebsocketConn.Dial(&dialer, http.Header{})
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
b.WebsocketConn.SetupPingHandler(wshandler.WebsocketPingHandler{
b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
MessageType: websocket.PingMessage,
Delay: btseWebsocketTimer,
})
@@ -48,17 +48,19 @@ func (b *BTSE) WsConnect() error {
}
}
b.GenerateDefaultSubscriptions()
return nil
subs, err := b.GenerateDefaultSubscriptions()
if err != nil {
return err
}
return b.Websocket.SubscribeToChannels(subs)
}
// WsAuthenticate Send an authentication message to receive auth data
func (b *BTSE) WsAuthenticate() error {
nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
path := "/spotWS" + nonce
hmac := crypto.GetHMAC(
crypto.HashSHA512_384,
[]byte((path + nonce)),
hmac := crypto.GetHMAC(crypto.HashSHA512_384,
[]byte((path)),
[]byte(b.API.Credentials.Secret),
)
sign := crypto.HexEncodeToString(hmac)
@@ -66,7 +68,7 @@ func (b *BTSE) WsAuthenticate() error {
Operation: "authKeyExpires",
Arguments: []string{b.API.Credentials.Key, nonce, sign},
}
return b.WebsocketConn.SendJSONMessage(req)
return b.Websocket.Conn.SendJSONMessage(req)
}
func stringToOrderStatus(status string) (order.Status, error) {
@@ -93,27 +95,16 @@ func stringToOrderStatus(status string) (order.Status, error) {
// wsReadData receives and passes on websocket messages for processing
func (b *BTSE) wsReadData() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
defer b.Websocket.Wg.Done()
for {
select {
case <-b.Websocket.ShutdownC:
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
default:
resp, err := b.WebsocketConn.ReadMessage()
if err != nil {
b.Websocket.ReadMessageErrors <- err
return
}
b.Websocket.TrafficAlert <- struct{}{}
err = b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
@@ -123,6 +114,13 @@ func (b *BTSE) wsHandleData(respRaw []byte) error {
var result Result
err := json.Unmarshal(respRaw, &result)
if err != nil {
if strings.Contains(string(respRaw), "UNLOGIN_USER connect success") ||
strings.Contains(string(respRaw), "authenticated successfully") {
return nil
} else if strings.Contains(string(respRaw), "AUTHENTICATE ERROR") {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return errors.New("authentication failure")
}
return err
}
switch {
@@ -160,12 +158,19 @@ func (b *BTSE) wsHandleData(respRaw []byte) error {
Err: err,
}
}
p := currency.NewPairFromString(notification.Data[i].Symbol)
var p currency.Pair
p, err = currency.NewPairFromString(notification.Data[i].Symbol)
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
return err
}
b.Websocket.DataHandler <- &order.Detail{
Price: notification.Data[i].Price,
Amount: notification.Data[i].Size,
@@ -192,13 +197,21 @@ func (b *BTSE) wsHandleData(respRaw []byte) error {
if tradeHistory.Data[x].Gain == -1 {
side = order.Sell
}
p := currency.NewPairFromString(strings.Replace(tradeHistory.Topic, "tradeHistory:", "", 1))
var p currency.Pair
p, err = currency.NewPairFromString(strings.Replace(tradeHistory.Topic,
"tradeHistory:",
"",
1))
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.TradeData{
b.Websocket.DataHandler <- stream.TradeData{
Timestamp: time.Unix(0, tradeHistory.Data[x].TransactionTime*int64(time.Millisecond)),
CurrencyPair: p,
AssetType: a,
@@ -248,7 +261,10 @@ func (b *BTSE) wsHandleData(respRaw []byte) error {
Amount: amount,
})
}
p := currency.NewPairFromString(t.Topic[strings.Index(t.Topic, ":")+1 : strings.Index(t.Topic, "_")])
p, err := currency.NewPairFromString(t.Topic[strings.Index(t.Topic, ":")+1 : strings.Index(t.Topic, "_")])
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
@@ -261,50 +277,65 @@ func (b *BTSE) wsHandleData(respRaw []byte) error {
if err != nil {
return err
}
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair,
Asset: a,
Exchange: b.Name}
default:
b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)}
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
return nil
}
return nil
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *BTSE) GenerateDefaultSubscriptions() {
func (b *BTSE) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{"orderBookApi:%s_0", "tradeHistory:%s"}
pairs := b.GetEnabledPairs(asset.Spot)
var subscriptions []wshandler.WebsocketChannelSubscription
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
if b.Websocket.CanUseAuthenticatedEndpoints() {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: "notificationApi",
})
}
for i := range channels {
for j := range pairs {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: fmt.Sprintf(channels[i], pairs[j]),
Currency: pairs[j],
Asset: asset.Spot,
})
}
}
b.Websocket.SubscribeToChannels(subscriptions)
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *BTSE) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
func (b *BTSE) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var sub wsSub
sub.Operation = "subscribe"
sub.Arguments = []string{channelToSubscribe.Channel}
return b.WebsocketConn.SendJSONMessage(sub)
for i := range channelsToSubscribe {
sub.Arguments = append(sub.Arguments, channelsToSubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(sub)
if err != nil {
return err
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *BTSE) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
func (b *BTSE) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var unSub wsSub
unSub.Operation = "unsubscribe"
unSub.Arguments = []string{channelToSubscribe.Channel}
return b.WebsocketConn.SendJSONMessage(unSub)
for i := range channelsToUnsubscribe {
unSub.Arguments = append(unSub.Arguments,
channelsToUnsubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(unSub)
if err != nil {
return err
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...)
return nil
}

View File

@@ -19,8 +19,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -56,11 +56,7 @@ func (b *BTSE) SetDefaults() {
b.API.CredentialsValidator.RequiresKey = true
b.API.CredentialsValidator.RequiresSecret = true
b.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
fmt1 := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
@@ -70,6 +66,23 @@ func (b *BTSE) SetDefaults() {
Delimiter: "-",
},
}
err := b.StoreAssetPairFormat(asset.Spot, fmt1)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
fmt2 := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
},
}
err = b.StoreAssetPairFormat(asset.Futures, fmt2)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
b.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
@@ -112,7 +125,7 @@ func (b *BTSE) SetDefaults() {
b.API.Endpoints.URLDefault = btseAPIURL
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
b.Websocket = wshandler.New()
b.Websocket = stream.New()
b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
@@ -130,41 +143,29 @@ func (b *BTSE) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: btseWebsocket,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
Features: &b.Features.Supports.WebsocketCapabilities,
})
err = b.Websocket.Setup(&stream.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: btseWebsocket,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: b.WsConnect,
Subscriber: b.Subscribe,
UnSubscriber: b.Unsubscribe,
GenerateSubscriptions: b.GenerateDefaultSubscriptions,
Features: &b.Features.Supports.WebsocketCapabilities,
OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit,
})
if err != nil {
return err
}
b.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: b.Name,
URL: b.Websocket.GetWebsocketURL(),
ProxyURL: b.Websocket.GetProxyAddress(),
Verbose: b.Verbose,
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
false,
false,
false,
false,
exch.Name)
return nil
})
}
// Start starts the BTSE go routine
@@ -194,61 +195,96 @@ func (b *BTSE) Run() {
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (b *BTSE) FetchTradablePairs(asset asset.Item) ([]string, error) {
m, err := b.GetMarkets()
if err != nil {
return nil, err
func (b *BTSE) FetchTradablePairs(a asset.Item) ([]string, error) {
var currencies []string
if a == asset.Spot {
m, err := b.GetSpotMarkets()
if err != nil {
return nil, err
}
for x := range m {
if m[x].Status != "active" {
continue
}
currencies = append(currencies, m[x].Symbol)
}
} else if a == asset.Futures {
m, err := b.GetFuturesMarkets()
if err != nil {
return nil, err
}
for x := range m {
if !m[x].Active {
continue
}
currencies = append(currencies, m[x].Symbol)
}
}
var currencies []string
for x := range m {
if m[x].Status != "active" {
continue
}
currencies = append(currencies, m[x].Symbol)
}
return currencies, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (b *BTSE) UpdateTradablePairs(forceUpdate bool) error {
pairs, err := b.FetchTradablePairs(asset.Spot)
if err != nil {
return err
}
a := b.GetAssetTypes()
for i := range a {
pairs, err := b.FetchTradablePairs(a[i])
if err != nil {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate)
p, err := currency.NewPairsFromStrings(pairs)
if err != nil {
return err
}
err = b.UpdatePairs(p, a[i], false, forceUpdate)
if err != nil {
return err
}
}
return nil
}
// UpdateTicker updates and returns the ticker for a currency pair
func (b *BTSE) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerPrice := new(ticker.Price)
t, err := b.GetTicker(b.FormatExchangeCurrency(p,
assetType).String())
if err != nil {
return tickerPrice, err
if assetType == asset.Futures {
// Futures REST implementation needs to be done before this can be
// removed
return nil, common.ErrNotYetImplemented
}
s, err := b.GetMarketStatistics(b.FormatExchangeCurrency(p,
assetType).String())
fpair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return tickerPrice, err
return nil, err
}
tickerPrice.Pair = p
tickerPrice.Ask = t.Ask
tickerPrice.Bid = t.Bid
tickerPrice.Low = s.Low
tickerPrice.Last = t.Price
tickerPrice.Volume = s.Volume
tickerPrice.High = s.High
tickerPrice.LastUpdated = s.Time
err = ticker.ProcessTicker(b.Name, tickerPrice, assetType)
t, err := b.GetTicker(fpair.String())
if err != nil {
return tickerPrice, err
return nil, err
}
s, err := b.GetMarketStatistics(fpair.String())
if err != nil {
return nil, err
}
err = ticker.ProcessTicker(&ticker.Price{
Pair: p,
Ask: t.Ask,
Bid: t.Bid,
Low: s.Low,
Last: t.Price,
Volume: s.Volume,
High: s.High,
LastUpdated: s.Time,
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return nil, err
}
return ticker.GetTicker(b.Name, p, assetType)
}
@@ -273,11 +309,22 @@ func (b *BTSE) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
a, err := b.FetchOrderBook(b.FormatExchangeCurrency(p, assetType).String())
if err != nil {
return orderBook, err
if assetType == asset.Futures {
// Futures REST implementation needs to be done before this can be
// removed
return nil, common.ErrNotYetImplemented
}
fpair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
a, err := b.FetchOrderBook(fpair.String())
if err != nil {
return nil, err
}
orderBook := new(orderbook.Base)
for x := range a.BuyQuote {
orderBook.Bids = append(orderBook.Bids, orderbook.Item{
Price: a.BuyQuote[x].Price,
@@ -360,11 +407,16 @@ func (b *BTSE) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) {
return resp, err
}
fpair, err := b.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return resp, err
}
r, err := b.CreateOrder(s.Amount,
s.Price,
s.Side.String(),
s.Type.String(),
b.FormatExchangeCurrency(s.Pair, asset.Spot).String(),
fpair.String(),
goodTillCancel,
s.ClientID)
if err != nil {
@@ -389,9 +441,13 @@ func (b *BTSE) ModifyOrder(action *order.Modify) (string, error) {
// CancelOrder cancels an order by its corresponding ID number
func (b *BTSE) CancelOrder(order *order.Cancel) error {
r, err := b.CancelExistingOrder(order.ID,
b.FormatExchangeCurrency(order.Pair,
asset.Spot).String())
fpair, err := b.FormatExchangeCurrency(order.Pair,
order.AssetType)
if err != nil {
return err
}
r, err := b.CancelExistingOrder(order.ID, fpair.String())
if err != nil {
return err
}
@@ -411,19 +467,28 @@ func (b *BTSE) CancelOrder(order *order.Cancel) error {
// If not specified, all orders of all markets will be cancelled
func (b *BTSE) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
var resp order.CancelAllResponse
markets, err := b.GetMarkets()
markets, err := b.GetSpotMarkets()
if err != nil {
return resp, err
}
format, err := b.GetPairFormat(orderCancellation.AssetType, false)
if err != nil {
return resp, err
}
resp.Status = make(map[string]string)
for x := range markets {
strPair := b.FormatExchangeCurrency(orderCancellation.Pair,
orderCancellation.AssetType).String()
fair, err := b.FormatExchangeCurrency(orderCancellation.Pair,
orderCancellation.AssetType)
if err != nil {
return resp, err
}
checkPair := currency.NewPairWithDelimiter(markets[x].BaseCurrency,
markets[x].QuoteCurrency,
b.GetPairFormat(asset.Spot, false).Delimiter).String()
if strPair != "" && strPair != checkPair {
format.Delimiter).String()
if fair.String() != checkPair {
continue
} else {
orders, err := b.GetOrders(checkPair)
@@ -455,6 +520,11 @@ func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) {
return od, errors.New("no orders found")
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return order.Detail{}, err
}
for i := range o {
if o[i].ID != orderID {
continue
@@ -465,8 +535,14 @@ func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) {
side = order.Sell
}
od.Pair = currency.NewPairDelimiter(o[i].Symbol,
b.GetPairFormat(asset.Spot, false).Delimiter)
od.Pair, err = currency.NewPairDelimiter(o[i].Symbol,
format.Delimiter)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetOrderInfo unable to parse currency pair: %s\n",
b.Name,
err)
}
od.Exchange = b.Name
od.Amount = o[i].Amount
od.ID = o[i].ID
@@ -530,11 +606,6 @@ func (b *BTSE) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Re
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (b *BTSE) GetWebsocket() (*wshandler.Websocket, error) {
return b.Websocket, nil
}
// GetActiveOrders retrieves any orders that are active/open
func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) {
resp, err := b.GetOrders("")
@@ -542,6 +613,11 @@ func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, err
return nil, err
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
var orders []order.Detail
for i := range resp {
var side = order.Buy
@@ -557,9 +633,17 @@ func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, err
err)
}
p, err := currency.NewPairDelimiter(resp[i].Symbol,
format.Delimiter)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetActiveOrders unable to parse currency pair: %s\n",
b.Name,
err)
}
openOrder := order.Detail{
Pair: currency.NewPairDelimiter(resp[i].Symbol,
b.GetPairFormat(asset.Spot, false).Delimiter),
Pair: p,
Exchange: b.Name,
Amount: resp[i].Amount,
ID: resp[i].ID,
@@ -621,30 +705,6 @@ func (b *BTSE) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
return b.GetFee(feeBuilder)
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *BTSE) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
b.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *BTSE) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
b.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *BTSE) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *BTSE) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (b *BTSE) ValidateCredentials() error {

Some files were not shown because too many files have changed in this diff Show More