Improvement: Subsystem separation (#664)

* Initial codes for a trade tracker

* Moving everything in a broken fashion

* Removes tradetracker. Removes some errors for subsystems

* Cleans up some subsystems, renames stuttering types. Removes some global Bot usage

* More basic subsystem renaming and file moving

* Removes engine dependency from events,ntpserver,ordermanager,comms manager

* Exports eventManager, fixes rpcserver. puts rpcserver back for now

* Removes redundant error message, further removes engine dependencies

* experimental end of day interface usage

* adds ability to build the application

* Withdraw and event manager handling

* cleans up apiserver and communications manager

* Cleans up some start/setup processes. Though should separate

* More consistency with Setup Start Stop IsRunning funcs

* Final consistency pass before testing phase

* Fixes engine tests. Fixes stop nil issue

* api server tests

* Communications manager testing

* Connection manager tests and nilsubsystem error

* End of day currencypairsyncer tests

* Adds databaseconnection/databaseconnection_test.go

* Adds withdrawal manager tests

* Deposit address testing. Moved orderbook sync first as its more important

* Adds test for event manager

* More full eventmanager testing

* Adds testfile. Enables skipped test.

* ntp manager tests

* Adds ordermanager tests, Extracts a whole new subsystem from engine and fanangles import cycles

* Adds websocket routine manager tests

* Basic portfolio manager testing

* Fixes issue with currency pair sync startup

* Fixes issue with event manager startup

* Starts the order manager before backtester starts

* Fixes fee tests. Expands testing. Doesnt fix races

* Fixes most test races

* Resolves data races

* Fixes subsystem test issues

* currency pair syncer coverage tests

* Refactors portfolio. Fixes tests. Withdraw validation

Portfolio didn't need to exist with a portfolio manager. Now the porfolio manager
is in charge how the portfolio is handled and all portfolio functions are attached
to the base instead of just exported at the package level

Withdrawal validation occurred at the exchange level when it can just be run at the
withdrawal manager level. All withdrawal requests go through that endpoint

* lint -fix

* golang lint fixes

* lints and comments everything

* Updates GCT logo, adds documentation for some subsystems

* More documentation and more logo updates

* Fixes backtesting and apiserver errors encountered

* Fixes errors and typos from reviewing

* More minor fixes

* Changes %h verb to %w

* reverbs to %s

* Humbly begins reverting to more flat engine package

The main reasoning for this is that the subsystem split doesn't make sense
in a golang environment. The subsystems are only meant to be used with engine
and so by placing them in a non-engine area, it does not work and is
inconsistent with the rest of the application's package layout.

This will begin salvaging the changes made by reverting to a flat
engine package, but maintaining the consistent designs introduced.
Further, I will look to remove any TestMains and decrease the scope
of testing to be more local and decrease the issues that have been
caused from our style of testing.

* Manages to re-flatten things. Everything is within its own file

* mini fixes

* Fixes tests and data races and lints

* Updates docs tool for engine to create filename readmes

* os -> ioutil

* remove err

* Appveyor version increase test

* Removes tCleanup as its unsupported on appveyor

* Adds stuff that I thought was in previous merge master commit

* Removes cancel from test

* Fixes really fun test-exclusive data race

* minor nit fixes

* niterinos

* docs gen

* rm;rf test

* Remove typoline. expands startstop helper. Splits apiserver

* Removes accidental folder

* Uses update instead of replace for order upsert

* addresses nits. Renames files. Regenerates documentation.

* lint and removal of comments

* Add new test for default scenario

* Fixes typo

* regen docs
This commit is contained in:
Scott
2021-05-31 10:17:12 +10:00
committed by GitHub
parent 0e7d530c71
commit 5ea5245afb
325 changed files with 11868 additions and 8068 deletions

View File

@@ -1,100 +0,0 @@
package engine
import (
"errors"
"strings"
"sync"
"github.com/thrasher-corp/gocryptotrader/currency"
)
// DepositAddressStore stores a list of exchange deposit addresses
type DepositAddressStore struct {
m sync.Mutex
Store map[string]map[string]string
}
// DepositAddressManager manages the exchange deposit address store
type DepositAddressManager struct {
Store DepositAddressStore
}
// vars related to the deposit address helpers
var (
ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil")
ErrDepositAddressNotFound = errors.New("deposit address does not exist")
)
// Seed seeds the deposit address store
func (d *DepositAddressStore) Seed(coinData map[string]map[string]string) {
d.m.Lock()
defer d.m.Unlock()
if d.Store == nil {
d.Store = make(map[string]map[string]string)
}
for k, v := range coinData {
r := make(map[string]string)
for w, x := range v {
r[strings.ToUpper(w)] = x
}
d.Store[strings.ToUpper(k)] = r
}
}
// GetDepositAddress returns a deposit address based on the specified item
func (d *DepositAddressStore) GetDepositAddress(exchName string, item currency.Code) (string, error) {
d.m.Lock()
defer d.m.Unlock()
if len(d.Store) == 0 {
return "", ErrDepositAddressStoreIsNil
}
r, ok := d.Store[strings.ToUpper(exchName)]
if !ok {
return "", ErrExchangeNotFound
}
addr, ok := r[strings.ToUpper(item.String())]
if !ok {
return "", ErrDepositAddressNotFound
}
return addr, nil
}
// GetDepositAddresses returns a list of stored deposit addresses
func (d *DepositAddressStore) GetDepositAddresses(exchName string) (map[string]string, error) {
d.m.Lock()
defer d.m.Unlock()
if len(d.Store) == 0 {
return nil, ErrDepositAddressStoreIsNil
}
r, ok := d.Store[strings.ToUpper(exchName)]
if !ok {
return nil, ErrDepositAddressNotFound
}
return r, nil
}
// GetDepositAddressByExchange returns a deposit address for the specified exchange and cryptocurrency
// if it exists
func (d *DepositAddressManager) GetDepositAddressByExchange(exchName string, currencyItem currency.Code) (string, error) {
return d.Store.GetDepositAddress(exchName, currencyItem)
}
// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified
// exchange if they exist
func (d *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string]string, error) {
return d.Store.GetDepositAddresses(exchName)
}
// Sync synchronises all deposit addresses
func (d *DepositAddressManager) Sync() {
result := Bot.GetExchangeCryptocurrencyDepositAddresses()
d.Store.Seed(result)
}

View File

@@ -1,64 +0,0 @@
package engine
import (
"testing"
"github.com/thrasher-corp/gocryptotrader/currency"
)
const (
testBTCAddress = "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX"
)
func TestSeed(t *testing.T) {
var d DepositAddressStore
u := map[string]map[string]string{
"BITSTAMP": {
"BTC": testBTCAddress,
},
}
d.Seed(u)
r, err := d.GetDepositAddress("BITSTAMP", currency.BTC)
if err != nil {
t.Error("unexpected result")
}
if r != testBTCAddress {
t.Error("unexpected result")
}
}
func TestGetDepositAddress(t *testing.T) {
var d DepositAddressStore
_, err := d.GetDepositAddress("", currency.BTC)
if err != ErrDepositAddressStoreIsNil {
t.Error("non-error on non-existent exchange")
}
d.Store = map[string]map[string]string{
"BITSTAMP": {
"BTC": testBTCAddress,
},
}
_, err = d.GetDepositAddress("", currency.BTC)
if err != ErrExchangeNotFound {
t.Error("non-error on non-existent exchange")
}
var r string
r, err = d.GetDepositAddress("BiTStAmP", currency.NewCode("bTC"))
if err != nil {
t.Error("unexpected err: ", err)
}
if r != testBTCAddress {
t.Error("unexpected BTC address: ", r)
}
_, err = d.GetDepositAddress("BiTStAmP", currency.LTC)
if err != ErrDepositAddressNotFound {
t.Error("unexpected err: ", err)
}
}

906
engine/apiserver.go Normal file
View File

@@ -0,0 +1,906 @@
package engine
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/pprof"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database/repository/exchange"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
// setupAPIServerManager checks and creates an api server manager
func setupAPIServerManager(remoteConfig *config.RemoteControlConfig, pprofConfig *config.Profiler, exchangeManager iExchangeManager, bot iBot, portfolioManager iPortfolioManager, configPath string) (*apiServerManager, error) {
if remoteConfig == nil {
return nil, errNilRemoteConfig
}
if pprofConfig == nil {
return nil, errNilPProfConfig
}
if exchangeManager == nil {
return nil, errNilExchangeManager
}
if bot == nil {
return nil, errNilBot
}
if configPath == "" {
return nil, errEmptyConfigPath
}
return &apiServerManager{
remoteConfig: remoteConfig,
pprofConfig: pprofConfig,
restListenAddress: remoteConfig.DeprecatedRPC.ListenAddress,
websocketListenAddress: remoteConfig.WebsocketRPC.ListenAddress,
exchangeManager: exchangeManager,
bot: bot,
gctConfigPath: configPath,
portfolioManager: portfolioManager,
}, nil
}
// IsRESTServerRunning safely checks whether the subsystem is running
func (m *apiServerManager) IsRESTServerRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.restStarted) == 1
}
// IsWebsocketServerRunning safely checks whether the subsystem is running
func (m *apiServerManager) IsWebsocketServerRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.websocketStarted) == 1
}
// StopRESTServer attempts to shutdown the subsystem
func (m *apiServerManager) StopRESTServer() error {
if m == nil {
return fmt.Errorf("api server %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.restStarted, 1, 0) {
return fmt.Errorf("apiserver deprecated server %w", ErrSubSystemNotStarted)
}
err := m.restHTTPServer.Shutdown(context.Background())
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
m.wgRest.Wait()
m.restRouter = nil
return nil
}
func (m *apiServerManager) StopWebsocketServer() error {
if m == nil {
return fmt.Errorf("api server %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.websocketStarted, 1, 0) {
return fmt.Errorf("apiserver websocket server %w", ErrSubSystemNotStarted)
}
err := m.websocketHTTPServer.Shutdown(context.Background())
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
m.websocketRouter = nil
m.websocketHub = nil
m.wgWebsocket.Wait()
m.websocketHTTPServer = nil
return nil
}
// newRouter takes in the exchange interfaces and returns a new multiplexer
// router
func (m *apiServerManager) newRouter(isREST bool) *mux.Router {
router := mux.NewRouter().StrictSlash(true)
var routes []Route
if common.ExtractPort(m.websocketListenAddress) == 80 {
m.websocketListenAddress = common.ExtractHost(m.websocketListenAddress)
} else {
m.websocketListenAddress = strings.Join([]string{common.ExtractHost(m.websocketListenAddress),
strconv.Itoa(common.ExtractPort(m.websocketListenAddress))}, ":")
}
if isREST {
routes = []Route{
{"", http.MethodGet, "/", m.getIndex},
{"GetAllSettings", http.MethodGet, "/config/all", m.restGetAllSettings},
{"SaveAllSettings", http.MethodPost, "/config/all/save", m.restSaveAllSettings},
{"AllEnabledAccountInfo", http.MethodGet, "/exchanges/enabled/accounts/all", m.restGetAllEnabledAccountInfo},
{"AllActiveExchangesAndCurrencies", http.MethodGet, "/exchanges/enabled/latest/all", m.restGetAllActiveTickers},
{"GetPortfolio", http.MethodGet, "/portfolio/all", m.restGetPortfolio},
{"AllActiveExchangesAndOrderbooks", http.MethodGet, "/exchanges/orderbook/latest/all", m.restGetAllActiveOrderbooks},
}
if m.pprofConfig.Enabled {
if m.pprofConfig.MutexProfileFraction > 0 {
runtime.SetMutexProfileFraction(m.pprofConfig.MutexProfileFraction)
}
log.Debugf(log.RESTSys,
"HTTP Go performance profiler (pprof) endpoint enabled: http://%s:%d/debug/pprof/\n",
common.ExtractHost(m.websocketListenAddress),
common.ExtractPort(m.websocketListenAddress))
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
}
} else {
routes = []Route{
{"ws", http.MethodGet, "/ws", m.WebsocketClientHandler},
}
}
for _, route := range routes {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(restLogger(route.HandlerFunc, route.Name)).
Host(m.websocketListenAddress)
}
return router
}
// StartRESTServer starts a REST handler
func (m *apiServerManager) StartRESTServer() error {
if !atomic.CompareAndSwapInt32(&m.restStarted, 0, 1) {
return fmt.Errorf("rest server %w", errAlreadyRunning)
}
if !m.remoteConfig.DeprecatedRPC.Enabled {
atomic.StoreInt32(&m.restStarted, 0)
return fmt.Errorf("rest %w", errServerDisabled)
}
log.Debugf(log.RESTSys,
"Deprecated RPC handler support enabled. Listen URL: http://%s:%d\n",
common.ExtractHost(m.restListenAddress), common.ExtractPort(m.restListenAddress))
m.restRouter = m.newRouter(true)
if m.restHTTPServer == nil {
m.restHTTPServer = &http.Server{
Addr: m.restListenAddress,
Handler: m.restRouter,
}
}
m.wgRest.Add(1)
go func() {
defer m.wgRest.Done()
err := m.restHTTPServer.ListenAndServe()
if err != nil {
atomic.StoreInt32(&m.restStarted, 0)
if !errors.Is(err, http.ErrServerClosed) {
log.Error(log.APIServerMgr, err)
}
}
}()
return nil
}
// restLogger logs the requests internally
func restLogger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
log.Debugf(log.RESTSys,
"%s\t%s\t%s\t%s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}
// writeResponse outputs a JSON response of the response interface
func writeResponse(w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
return json.NewEncoder(w).Encode(response)
}
// handleError prints the REST method and error
func handleError(method string, err error) {
log.Errorf(log.APIServerMgr, "RESTful %s: handler failed to send JSON response. Error %s\n",
method, err)
}
// restGetAllSettings replies to a request with an encoded JSON response about the
// trading Bots configuration.
func (m *apiServerManager) restGetAllSettings(w http.ResponseWriter, r *http.Request) {
err := writeResponse(w, config.GetConfig())
if err != nil {
handleError(r.Method, err)
}
}
// restSaveAllSettings saves all current settings from request body as a JSON
// document then reloads state and returns the settings
func (m *apiServerManager) restSaveAllSettings(w http.ResponseWriter, r *http.Request) {
// Get the data from the request
decoder := json.NewDecoder(r.Body)
var responseData config.Post
err := decoder.Decode(&responseData)
if err != nil {
handleError(r.Method, err)
}
// Save change the settings
cfg := config.GetConfig()
err = cfg.UpdateConfig(m.gctConfigPath, &responseData.Data, false)
if err != nil {
handleError(r.Method, err)
}
err = writeResponse(w, cfg)
if err != nil {
handleError(r.Method, err)
}
err = m.bot.SetupExchanges()
if err != nil {
handleError(r.Method, err)
}
}
// restGetAllActiveOrderbooks returns all enabled exchange orderbooks
func (m *apiServerManager) restGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) {
var response AllEnabledExchangeOrderbooks
response.Data = getAllActiveOrderbooks(m.exchangeManager)
err := writeResponse(w, response)
if err != nil {
handleError(r.Method, err)
}
}
// restGetPortfolio returns the Bot portfolio manager
func (m *apiServerManager) restGetPortfolio(w http.ResponseWriter, r *http.Request) {
result := m.portfolioManager.GetPortfolioSummary()
err := writeResponse(w, result)
if err != nil {
handleError(r.Method, err)
}
}
// restGetAllActiveTickers returns all active tickers
func (m *apiServerManager) restGetAllActiveTickers(w http.ResponseWriter, r *http.Request) {
var response AllEnabledExchangeCurrencies
response.Data = getAllActiveTickers(m.exchangeManager)
err := writeResponse(w, response)
if err != nil {
handleError(r.Method, err)
}
}
// restGetAllEnabledAccountInfo via get request returns JSON response of account
// info
func (m *apiServerManager) restGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) {
response := getAllActiveAccounts(m.exchangeManager)
err := writeResponse(w, response)
if err != nil {
handleError(r.Method, err)
}
}
// getIndex returns an HTML snippet for when a user requests the index URL
func (m *apiServerManager) getIndex(w http.ResponseWriter, _ *http.Request) {
_, err := fmt.Fprint(w, restIndexResponse)
if err != nil {
log.Error(log.APIServerMgr, err)
}
w.WriteHeader(http.StatusOK)
}
// getAllActiveOrderbooks returns all enabled exchanges orderbooks
func getAllActiveOrderbooks(m iExchangeManager) []EnabledExchangeOrderbooks {
var orderbookData []EnabledExchangeOrderbooks
exchanges := m.GetExchanges()
for x := range exchanges {
assets := exchanges[x].GetAssetTypes()
exchName := exchanges[x].GetName()
var exchangeOB EnabledExchangeOrderbooks
exchangeOB.ExchangeName = exchName
for y := range assets {
currencies, err := exchanges[x].GetEnabledPairs(assets[y])
if err != nil {
log.Errorf(log.APIServerMgr,
"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 {
log.Errorf(log.APIServerMgr,
"Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName,
currencies[z].String(),
err)
continue
}
exchangeOB.ExchangeValues = append(exchangeOB.ExchangeValues, *ob)
}
orderbookData = append(orderbookData, exchangeOB)
}
orderbookData = append(orderbookData, exchangeOB)
}
return orderbookData
}
// getAllActiveTickers returns all enabled exchanges tickers
func getAllActiveTickers(m iExchangeManager) []EnabledExchangeCurrencies {
var tickers []EnabledExchangeCurrencies
exchanges := m.GetExchanges()
for x := range exchanges {
assets := exchanges[x].GetAssetTypes()
exchName := exchanges[x].GetName()
var exchangeTickers EnabledExchangeCurrencies
exchangeTickers.ExchangeName = exchName
for y := range assets {
currencies, err := exchanges[x].GetEnabledPairs(assets[y])
if err != nil {
log.Errorf(log.APIServerMgr,
"Exchange %s could not retrieve enabled currencies. Err: %s\n",
exchName,
err)
continue
}
for z := range currencies {
t, err := exchanges[x].FetchTicker(currencies[z], assets[y])
if err != nil {
log.Errorf(log.APIServerMgr,
"Exchange %s failed to retrieve %s ticker. Err: %s\n", exchName,
currencies[z].String(),
err)
continue
}
exchangeTickers.ExchangeValues = append(exchangeTickers.ExchangeValues, *t)
}
tickers = append(tickers, exchangeTickers)
}
tickers = append(tickers, exchangeTickers)
}
return tickers
}
// getAllActiveAccounts returns all enabled exchanges accounts
func getAllActiveAccounts(m iExchangeManager) []AllEnabledExchangeAccounts {
var accounts []AllEnabledExchangeAccounts
exchanges := m.GetExchanges()
for x := range exchanges {
assets := exchanges[x].GetAssetTypes()
exchName := exchanges[x].GetName()
var exchangeAccounts AllEnabledExchangeAccounts
for y := range assets {
a, err := exchanges[x].FetchAccountInfo(assets[y])
if err != nil {
log.Errorf(log.APIServerMgr,
"Exchange %s failed to retrieve %s ticker. Err: %s\n",
exchName,
assets[y],
err)
continue
}
exchangeAccounts.Data = append(exchangeAccounts.Data, a)
}
accounts = append(accounts, exchangeAccounts)
}
return accounts
}
// StartWebsocketServer starts a Websocket handler
func (m *apiServerManager) StartWebsocketServer() error {
if !atomic.CompareAndSwapInt32(&m.websocketStarted, 0, 1) {
return fmt.Errorf("websocket server %w", errAlreadyRunning)
}
if !m.remoteConfig.WebsocketRPC.Enabled {
atomic.StoreInt32(&m.websocketStarted, 0)
return fmt.Errorf("websocket %w", errServerDisabled)
}
log.Debugf(log.APIServerMgr,
"Websocket RPC support enabled. Listen URL: ws://%s:%d/ws\n",
common.ExtractHost(m.websocketListenAddress), common.ExtractPort(m.websocketListenAddress))
m.websocketRouter = m.newRouter(false)
if m.websocketHTTPServer == nil {
m.websocketHTTPServer = &http.Server{
Addr: m.websocketListenAddress,
Handler: m.websocketRouter,
}
}
m.wgWebsocket.Add(1)
go func() {
defer m.wgWebsocket.Done()
err := m.websocketHTTPServer.ListenAndServe()
if err != nil {
atomic.StoreInt32(&m.websocketStarted, 0)
if !errors.Is(err, http.ErrServerClosed) {
log.Error(log.APIServerMgr, err)
}
}
}()
return nil
}
// newWebsocketHub Creates a new websocket hub
func newWebsocketHub() *websocketHub {
return &websocketHub{
Broadcast: make(chan []byte),
Register: make(chan *websocketClient),
Unregister: make(chan *websocketClient),
Clients: make(map[*websocketClient]bool),
}
}
func (h *websocketHub) run() {
for {
select {
case client := <-h.Register:
h.Clients[client] = true
case client := <-h.Unregister:
if _, ok := h.Clients[client]; ok {
log.Debugln(log.APIServerMgr, "websocket: disconnected client")
delete(h.Clients, client)
close(client.Send)
}
case message := <-h.Broadcast:
for client := range h.Clients {
select {
case client.Send <- message:
default:
log.Debugln(log.APIServerMgr, "websocket: disconnected client")
close(client.Send)
delete(h.Clients, client)
}
}
}
}
}
// SendWebsocketMessage sends a websocket event to the client
func (c *websocketClient) SendWebsocketMessage(evt interface{}) error {
data, err := json.Marshal(evt)
if err != nil {
log.Errorf(log.APIServerMgr, "websocket: failed to send message: %s\n", err)
return err
}
c.Send <- data
return nil
}
func (c *websocketClient) read() {
defer func() {
c.Hub.Unregister <- c
conErr := c.Conn.Close()
if conErr != nil {
log.Error(log.APIServerMgr, conErr)
}
}()
for {
msgType, message, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Errorf(log.APIServerMgr, "websocket: client disconnected, err: %s\n", err)
}
break
}
if msgType == websocket.TextMessage {
var evt WebsocketEvent
err := json.Unmarshal(message, &evt)
if err != nil {
log.Errorf(log.APIServerMgr, "websocket: failed to decode JSON sent from client %s\n", err)
continue
}
if evt.Event == "" {
log.Warnln(log.APIServerMgr, "websocket: client sent a blank event, disconnecting")
continue
}
dataJSON, err := json.Marshal(evt.Data)
if err != nil {
log.Errorln(log.APIServerMgr, "websocket: client sent data we couldn't JSON decode")
break
}
req := strings.ToLower(evt.Event)
log.Debugf(log.APIServerMgr, "websocket: request received: %s\n", req)
result, ok := wsHandlers[req]
if !ok {
log.Debugln(log.APIServerMgr, "websocket: unsupported event")
continue
}
if result.authRequired && !c.Authenticated {
log.Warnf(log.APIServerMgr, "Websocket: request %s failed due to unauthenticated request on an authenticated API\n", evt.Event)
err = c.SendWebsocketMessage(WebsocketEventResponse{Event: evt.Event, Error: "unauthorised request on authenticated API"})
if err != nil {
log.Error(log.APIServerMgr, err)
}
continue
}
err = result.handler(c, dataJSON)
if err != nil {
log.Errorf(log.APIServerMgr, "websocket: request %s failed. Error %s\n", evt.Event, err)
continue
}
}
}
}
func (c *websocketClient) write() {
defer func() {
err := c.Conn.Close()
if err != nil {
log.Error(log.APIServerMgr, err)
}
}()
for {
message, ok := <-c.Send
if !ok {
err := c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
log.Error(log.APIServerMgr, err)
}
log.Debugln(log.APIServerMgr, "websocket: hub closed the channel")
return
}
w, err := c.Conn.NextWriter(websocket.TextMessage)
if err != nil {
log.Errorf(log.APIServerMgr, "websocket: failed to create new io.writeCloser: %s\n", err)
return
}
_, err = w.Write(message)
if err != nil {
log.Error(log.APIServerMgr, err)
}
// Add queued chat messages to the current websocket message
n := len(c.Send)
for i := 0; i < n; i++ {
_, err = w.Write(<-c.Send)
if err != nil {
log.Error(log.APIServerMgr, err)
}
}
if err := w.Close(); err != nil {
log.Errorf(log.APIServerMgr, "websocket: failed to close io.WriteCloser: %s\n", err)
return
}
}
}
// StartWebsocketHandler starts the websocket hub and routine which
// handles clients
func StartWebsocketHandler() {
if !wsHubStarted {
wsHubStarted = true
wsHub = newWebsocketHub()
go wsHub.run()
}
}
// BroadcastWebsocketMessage meow
func BroadcastWebsocketMessage(evt WebsocketEvent) error {
if !wsHubStarted {
return ErrWebsocketServiceNotRunning
}
data, err := json.Marshal(evt)
if err != nil {
return err
}
wsHub.Broadcast <- data
return nil
}
// WebsocketClientHandler upgrades the HTTP connection to a websocket
// compatible one
func (m *apiServerManager) WebsocketClientHandler(w http.ResponseWriter, r *http.Request) {
if !wsHubStarted {
StartWebsocketHandler()
}
connectionLimit := m.remoteConfig.WebsocketRPC.ConnectionLimit
numClients := len(wsHub.Clients)
if numClients >= connectionLimit {
log.Warnf(log.APIServerMgr,
"websocket: client rejected due to websocket client limit reached. Number of clients %d. Limit %d.\n",
numClients, connectionLimit)
w.WriteHeader(http.StatusForbidden)
return
}
upgrader := websocket.Upgrader{
WriteBufferSize: 1024,
ReadBufferSize: 1024,
}
// Allow insecure origin if the Origin request header is present and not
// equal to the Host request header. Default to false
if m.remoteConfig.WebsocketRPC.AllowInsecureOrigin {
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error(log.APIServerMgr, err)
return
}
client := &websocketClient{
Hub: wsHub,
Conn: conn,
Send: make(chan []byte, 1024),
maxAuthFailures: m.remoteConfig.WebsocketRPC.MaxAuthFailures,
username: m.remoteConfig.Username,
password: m.remoteConfig.Password,
configPath: m.gctConfigPath,
exchangeManager: m.exchangeManager,
bot: m.bot,
portfolioManager: m.portfolioManager,
}
client.Hub.Register <- client
log.Debugf(log.APIServerMgr,
"websocket: client connected. Connected clients: %d. Limit %d.\n",
numClients+1, connectionLimit)
go client.read()
go client.write()
}
func wsAuth(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "auth",
}
var auth WebsocketAuth
err := json.Unmarshal(data.([]byte), &auth)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
hashPW := crypto.HexEncodeToString(crypto.GetSHA256([]byte(client.password)))
if auth.Username == client.username && auth.Password == hashPW {
client.Authenticated = true
wsResp.Data = WebsocketResponseSuccess
log.Debugln(log.APIServerMgr,
"websocket: client authenticated successfully")
return client.SendWebsocketMessage(wsResp)
}
wsResp.Error = "invalid username/password"
client.authFailures++
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
if client.authFailures >= client.maxAuthFailures {
log.Debugf(log.APIServerMgr,
"websocket: disconnecting client, maximum auth failures threshold reached (failures: %d limit: %d)\n",
client.authFailures, client.maxAuthFailures)
wsHub.Unregister <- client
return nil
}
log.Debugf(log.APIServerMgr,
"websocket: client sent wrong username/password (failures: %d limit: %d)\n",
client.authFailures, client.maxAuthFailures)
return nil
}
func wsGetConfig(client *websocketClient, _ interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetConfig",
Data: config.GetConfig(),
}
return client.SendWebsocketMessage(wsResp)
}
func wsSaveConfig(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "SaveConfig",
}
var respCfg config.Config
err := json.Unmarshal(data.([]byte), &respCfg)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
cfg := config.GetConfig()
err = cfg.UpdateConfig(client.configPath, &respCfg, false)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
err = client.bot.SetupExchanges()
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
wsResp.Data = WebsocketResponseSuccess
return client.SendWebsocketMessage(wsResp)
}
func wsGetAccountInfo(client *websocketClient, data interface{}) error {
accountInfo := getAllActiveAccounts(client.exchangeManager)
wsResp := WebsocketEventResponse{
Event: "GetAccountInfo",
Data: accountInfo,
}
return client.SendWebsocketMessage(wsResp)
}
func wsGetTickers(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetTickers",
}
wsResp.Data = getAllActiveTickers(client.exchangeManager)
return client.SendWebsocketMessage(wsResp)
}
func wsGetTicker(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetTicker",
}
var tickerReq WebsocketOrderbookTickerRequest
err := json.Unmarshal(data.([]byte), &tickerReq)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
p, err := currency.NewPairFromString(tickerReq.Currency)
if err != nil {
return err
}
a, err := asset.New(tickerReq.AssetType)
if err != nil {
return err
}
exch := client.exchangeManager.GetExchangeByName(tickerReq.Exchange)
if exch == nil {
wsResp.Error = exchange.ErrNoExchangeFound.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
tick, err := exch.FetchTicker(p, a)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
wsResp.Data = tick
return client.SendWebsocketMessage(wsResp)
}
func wsGetOrderbooks(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetOrderbooks",
}
wsResp.Data = getAllActiveOrderbooks(client.exchangeManager)
return client.SendWebsocketMessage(wsResp)
}
func wsGetOrderbook(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetOrderbook",
}
var orderbookReq WebsocketOrderbookTickerRequest
err := json.Unmarshal(data.([]byte), &orderbookReq)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
p, err := currency.NewPairFromString(orderbookReq.Currency)
if err != nil {
return err
}
a, err := asset.New(orderbookReq.AssetType)
if err != nil {
return err
}
exch := client.exchangeManager.GetExchangeByName(orderbookReq.Exchange)
if exch == nil {
wsResp.Error = exchange.ErrNoExchangeFound.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
ob, err := exch.FetchOrderbook(p, a)
if err != nil {
wsResp.Error = err.Error()
sendErr := client.SendWebsocketMessage(wsResp)
if sendErr != nil {
log.Error(log.APIServerMgr, sendErr)
}
return err
}
wsResp.Data = ob
return nil
}
func wsGetExchangeRates(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetExchangeRates",
}
var err error
wsResp.Data, err = currency.GetExchangeRates()
if err != nil {
return err
}
return client.SendWebsocketMessage(wsResp)
}
func wsGetPortfolio(client *websocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetPortfolio",
}
wsResp.Data = client.portfolioManager.GetPortfolioSummary()
return client.SendWebsocketMessage(wsResp)
}

62
engine/apiserver.md Normal file
View File

@@ -0,0 +1,62 @@
# GoCryptoTrader package Apiserver
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/apiserver)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This apiserver package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Apiserver
+ The API server subsystem is a deprecated service used to host a REST or websocket server to interact with some functions of GoCryptoTrader
+ This subsystem is no longer maintained and it is highly encouraged to interact with GRPC endpoints directly where possible
+ In order to modify the behaviour of the API server subsystem, you can edit the following inside your config file:
### deprecatedRPC
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | If enabled will create a REST server which will listen to commands on the listen address | `true` |
| listenAddress | If enabled will listen for REST requests on this address and return a JSON response | `localhost:9050` |
### websocketRPC
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | If enabled will create a REST server which will listen to commands on the listen address | `true` |
| listenAddress | If enabled will listen for requests on this address and return a JSON response | `localhost:9051` |
| connectionLimit | Defines how many connections the websocket RPC server can handle simultanesoly | `1` |
| maxAuthFailures | For authenticated endpoints, the amount of failed attempts allowed before disconnection | `3` |
| allowInsecureOrigin | Allows use of insecure connections | `true` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

288
engine/apiserver_test.go Normal file
View File

@@ -0,0 +1,288 @@
package engine
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"reflect"
"sync"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
)
func TestSetupAPIServerManager(t *testing.T) {
t.Parallel()
_, err := setupAPIServerManager(nil, nil, nil, nil, nil, "")
if !errors.Is(err, errNilRemoteConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilRemoteConfig)
}
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, nil, nil, nil, nil, "")
if !errors.Is(err, errNilPProfConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilPProfConfig)
}
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, nil, nil, nil, "")
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, nil, nil, "")
if !errors.Is(err, errNilBot) {
t.Errorf("error '%v', expected '%v'", err, errNilBot)
}
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, "")
if !errors.Is(err, errEmptyConfigPath) {
t.Errorf("error '%v', expected '%v'", err, errEmptyConfigPath)
}
wd, _ := os.Getwd()
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestStartRESTServer(t *testing.T) {
t.Parallel()
wd, _ := os.Getwd()
m, err := setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StartRESTServer()
if !errors.Is(err, errServerDisabled) {
t.Errorf("error '%v', expected '%v'", err, errServerDisabled)
}
m.remoteConfig.DeprecatedRPC.Enabled = true
var wg sync.WaitGroup
wg.Add(1)
// this is difficult to test as a webserver actually starts, so quit if an immediate error is not received
err = m.StartRESTServer()
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
wg.Done()
}
func TestStartWebsocketServer(t *testing.T) {
t.Parallel()
wd, _ := os.Getwd()
m, err := setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StartWebsocketServer()
if !errors.Is(err, errServerDisabled) {
t.Errorf("error '%v', expected '%v'", err, errServerDisabled)
}
m.remoteConfig.WebsocketRPC.Enabled = true
err = m.StartWebsocketServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestStopRESTServer(t *testing.T) {
t.Parallel()
wd, _ := os.Getwd()
m, err := setupAPIServerManager(&config.RemoteControlConfig{
DeprecatedRPC: config.DepcrecatedRPCConfig{
Enabled: true,
ListenAddress: "localhost:9051",
},
}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StopRESTServer()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.StartRESTServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StopRESTServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
// do it again to ensure things have reset appropriately and no errors occur starting
err = m.StartRESTServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StopRESTServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestWebsocketStop(t *testing.T) {
t.Parallel()
wd, _ := os.Getwd()
m, err := setupAPIServerManager(&config.RemoteControlConfig{
WebsocketRPC: config.WebsocketRPCConfig{
Enabled: true,
ListenAddress: "localhost:9052",
},
}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StopWebsocketServer()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.StartWebsocketServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StopWebsocketServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
// do it again to ensure things have reset appropriately and no errors occur starting
err = m.StartWebsocketServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.StopWebsocketServer()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestIsRESTServerRunning(t *testing.T) {
t.Parallel()
m := &apiServerManager{}
if m.IsRESTServerRunning() {
t.Error("expected false")
}
m.restStarted = 1
if !m.IsRESTServerRunning() {
t.Error("expected true")
}
m = nil
if m.IsRESTServerRunning() {
t.Error("expected false")
}
}
func TestIsWebsocketServerRunning(t *testing.T) {
t.Parallel()
m := &apiServerManager{}
if m.IsWebsocketServerRunning() {
t.Error("expected false")
}
m.websocketStarted = 1
if !m.IsWebsocketServerRunning() {
t.Error("expected true")
}
m = nil
if m.IsWebsocketServerRunning() {
t.Error("expected false")
}
}
func TestGetAllActiveOrderbooks(t *testing.T) {
man := SetupExchangeManager()
bs, err := man.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
bs.SetDefaults()
man.Add(bs)
resp := getAllActiveOrderbooks(man)
if resp == nil {
t.Error("expected not nil")
}
}
func TestGetAllActiveTickers(t *testing.T) {
t.Parallel()
man := SetupExchangeManager()
bs, err := man.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
bs.SetDefaults()
man.Add(bs)
resp := getAllActiveTickers(man)
if resp == nil {
t.Error("expected not nil")
}
}
func TestGetAllActiveAccounts(t *testing.T) {
t.Parallel()
man := SetupExchangeManager()
bs, err := man.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
bs.SetDefaults()
man.Add(bs)
resp := getAllActiveAccounts(man)
if resp == nil {
t.Error("expected not nil")
}
}
func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response {
w := httptest.NewRecorder()
err := writeResponse(w, response)
if err != nil {
t.Error("Failed to make response.", err)
}
return w.Result()
}
// TestConfigAllJsonResponse test if config/all restful json response is valid
func TestConfigAllJsonResponse(t *testing.T) {
t.Parallel()
var c config.Config
err := c.LoadConfig(config.TestFile, true)
if err != nil {
t.Error(err)
}
resp := makeHTTPGetRequest(t, c)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error("Body not readable", err)
}
err = resp.Body.Close()
if err != nil {
t.Error("Body not closable", err)
}
var responseConfig config.Config
jsonErr := json.Unmarshal(body, &responseConfig)
if jsonErr != nil {
t.Error("Response not parse-able as json", err)
}
if !reflect.DeepEqual(responseConfig, c) {
t.Error("Json not equal to config")
}
}
// fakeBot is a basic implementation of the iBot interface used for testing
type fakeBot struct{}
// SetupExchanges is a basic implementation of the iBot interface used for testing
func (f *fakeBot) SetupExchanges() error {
return nil
}

169
engine/apiserver_types.go Normal file
View File

@@ -0,0 +1,169 @@
package engine
import (
"errors"
"net/http"
"sync"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
// Const vars for websocket
const (
WebsocketResponseSuccess = "OK"
restIndexResponse = "<html>GoCryptoTrader RESTful interface. For the web GUI, please visit the <a href=https://github.com/thrasher-corp/gocryptotrader/blob/master/web/README.md>web GUI readme.</a></html>"
DeprecatedName = "deprecated_rpc"
WebsocketName = "websocket_rpc"
)
var (
wsHub *websocketHub
wsHubStarted bool
errNilRemoteConfig = errors.New("received nil remote config")
errNilPProfConfig = errors.New("received nil pprof config")
errNilBot = errors.New("received nil engine bot")
errEmptyConfigPath = errors.New("received empty config path")
errServerDisabled = errors.New("server disabled")
errInvalidListenAddress = errors.New("invalid listen address")
errAlreadyRunning = errors.New("already running")
// ErrWebsocketServiceNotRunning occurs when a message is sent to be broadcast via websocket
// and its not running
ErrWebsocketServiceNotRunning = errors.New("websocket service not started")
)
// apiServerManager holds all relevant fields to manage both REST and websocket
// api servers
type apiServerManager struct {
restStarted int32
websocketStarted int32
restListenAddress string
websocketListenAddress string
gctConfigPath string
restHTTPServer *http.Server
websocketHTTPServer *http.Server
wgRest sync.WaitGroup
wgWebsocket sync.WaitGroup
restRouter *mux.Router
websocketRouter *mux.Router
websocketHub *websocketHub
remoteConfig *config.RemoteControlConfig
pprofConfig *config.Profiler
exchangeManager iExchangeManager
bot iBot
portfolioManager iPortfolioManager
}
// websocketClient stores information related to the websocket client
type websocketClient struct {
Hub *websocketHub
Conn *websocket.Conn
Authenticated bool
authFailures int
Send chan []byte
username string
password string
maxAuthFailures int
exchangeManager iExchangeManager
bot iBot
portfolioManager iPortfolioManager
configPath string
}
// websocketHub stores the data for managing websocket clients
type websocketHub struct {
Clients map[*websocketClient]bool
Broadcast chan []byte
Register chan *websocketClient
Unregister chan *websocketClient
}
// WebsocketEvent is the struct used for websocket events
type WebsocketEvent struct {
Exchange string `json:"exchange,omitempty"`
AssetType string `json:"assetType,omitempty"`
Event string
Data interface{}
}
// WebsocketEventResponse is the struct used for websocket event responses
type WebsocketEventResponse struct {
Event string `json:"event"`
Data interface{} `json:"data"`
Error string `json:"error"`
}
// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook
// requests
type WebsocketOrderbookTickerRequest struct {
Exchange string `json:"exchangeName"`
Currency string `json:"currency"`
AssetType string `json:"assetType"`
}
// WebsocketAuth is a struct used for
type WebsocketAuth struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Route is a sub type that holds the request routes
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks
type AllEnabledExchangeOrderbooks struct {
Data []EnabledExchangeOrderbooks `json:"data"`
}
// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective
// orderbooks
type EnabledExchangeOrderbooks struct {
ExchangeName string `json:"exchangeName"`
ExchangeValues []orderbook.Base `json:"exchangeValues"`
}
// AllEnabledExchangeCurrencies holds the enabled exchange currencies
type AllEnabledExchangeCurrencies struct {
Data []EnabledExchangeCurrencies `json:"data"`
}
// EnabledExchangeCurrencies is a sub type for singular exchanges and respective
// currencies
type EnabledExchangeCurrencies struct {
ExchangeName string `json:"exchangeName"`
ExchangeValues []ticker.Price `json:"exchangeValues"`
}
// AllEnabledExchangeAccounts holds all enabled accounts info
type AllEnabledExchangeAccounts struct {
Data []account.Holdings `json:"data"`
}
var wsHandlers = map[string]wsCommandHandler{
"auth": {authRequired: false, handler: wsAuth},
"getconfig": {authRequired: true, handler: wsGetConfig},
"saveconfig": {authRequired: true, handler: wsSaveConfig},
"getaccountinfo": {authRequired: true, handler: wsGetAccountInfo},
"gettickers": {authRequired: false, handler: wsGetTickers},
"getticker": {authRequired: false, handler: wsGetTicker},
"getorderbooks": {authRequired: false, handler: wsGetOrderbooks},
"getorderbook": {authRequired: false, handler: wsGetOrderbook},
"getexchangerates": {authRequired: false, handler: wsGetExchangeRates},
"getportfolio": {authRequired: true, handler: wsGetPortfolio},
}
type wsCommandHandler struct {
authRequired bool
handler func(client *websocketClient, data interface{}) error
}

View File

@@ -1,95 +0,0 @@
package engine
import (
"errors"
"fmt"
"sync/atomic"
"github.com/thrasher-corp/gocryptotrader/communications"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"github.com/thrasher-corp/gocryptotrader/engine/subsystem"
"github.com/thrasher-corp/gocryptotrader/log"
)
// commsManager starts the NTP manager
type commsManager struct {
started int32
shutdown chan struct{}
relayMsg chan base.Event
comms *communications.Communications
}
func (c *commsManager) Started() bool {
return atomic.LoadInt32(&c.started) == 1
}
func (c *commsManager) Start() (err error) {
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return fmt.Errorf("communications manager %w", subsystem.ErrSubSystemAlreadyStarted)
}
defer func() {
if err != nil {
atomic.CompareAndSwapInt32(&c.started, 1, 0)
}
}()
log.Debugln(log.CommunicationMgr, "Communications manager starting...")
commsCfg := Bot.Config.GetCommunicationsConfig()
c.comms, err = communications.NewComm(&commsCfg)
if err != nil {
return err
}
c.shutdown = make(chan struct{})
c.relayMsg = make(chan base.Event)
go c.run()
log.Debugln(log.CommunicationMgr, "Communications manager started.")
return nil
}
func (c *commsManager) GetStatus() (map[string]base.CommsStatus, error) {
if !c.Started() {
return nil, errors.New("communications manager not started")
}
return c.comms.GetStatus(), nil
}
func (c *commsManager) Stop() error {
if atomic.LoadInt32(&c.started) == 0 {
return fmt.Errorf("communications manager %w", subsystem.ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&c.started, 1, 0)
}()
close(c.shutdown)
log.Debugln(log.CommunicationMgr, "Communications manager shutting down...")
return nil
}
func (c *commsManager) PushEvent(evt base.Event) {
if !c.Started() {
return
}
select {
case c.relayMsg <- evt:
default:
log.Errorf(log.CommunicationMgr, "Failed to send, no receiver when pushing event [%v]", evt)
}
}
func (c *commsManager) run() {
defer func() {
// TO-DO shutdown comms connections for connected services (Slack etc)
log.Debugln(log.CommunicationMgr, "Communications manager shutdown.")
}()
for {
select {
case msg := <-c.relayMsg:
c.comms.PushEvent(msg)
case <-c.shutdown:
return
}
}
}

View File

@@ -0,0 +1,117 @@
package engine
import (
"errors"
"fmt"
"sync/atomic"
"github.com/thrasher-corp/gocryptotrader/communications"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"github.com/thrasher-corp/gocryptotrader/log"
)
// CommunicationsManagerName is an exported subsystem name
const CommunicationsManagerName = "communications"
// CommunicationManager ensures operations of communications
type CommunicationManager struct {
started int32
shutdown chan struct{}
relayMsg chan base.Event
comms *communications.Communications
}
var errNilConfig = errors.New("received nil communications config")
// SetupCommunicationManager creates a communications manager
func SetupCommunicationManager(cfg *base.CommunicationsConfig) (*CommunicationManager, error) {
if cfg == nil {
return nil, errNilConfig
}
manager := &CommunicationManager{
shutdown: make(chan struct{}),
relayMsg: make(chan base.Event),
}
var err error
manager.comms, err = communications.NewComm(cfg)
if err != nil {
return nil, err
}
return manager, nil
}
// IsRunning safely checks whether the subsystem is running
func (m *CommunicationManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Start runs the subsystem
func (m *CommunicationManager) Start() error {
if m == nil {
return fmt.Errorf("communications manager server %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("communications manager %w", ErrSubSystemAlreadyStarted)
}
log.Debugf(log.CommunicationMgr, "Communications manager %s", MsgSubSystemStarting)
m.shutdown = make(chan struct{})
go m.run()
return nil
}
// GetStatus returns the status of communications
func (m *CommunicationManager) GetStatus() (map[string]base.CommsStatus, error) {
if !m.IsRunning() {
return nil, fmt.Errorf("communications manager %w", ErrSubSystemNotStarted)
}
return m.comms.GetStatus(), nil
}
// Stop attempts to shutdown the subsystem
func (m *CommunicationManager) Stop() error {
if m == nil {
return fmt.Errorf("communications manager server %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("communications manager %w", ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}()
close(m.shutdown)
log.Debugf(log.CommunicationMgr, "Communications manager %s", MsgSubSystemShuttingDown)
return nil
}
// PushEvent pushes an event to the communications relay
func (m *CommunicationManager) PushEvent(evt base.Event) {
if !m.IsRunning() {
return
}
select {
case m.relayMsg <- evt:
default:
log.Errorf(log.CommunicationMgr, "Failed to send, no receiver when pushing event [%v]", evt)
}
}
// run takes awaiting messages and pushes them to be handled by communications
func (m *CommunicationManager) run() {
log.Debugf(log.Global, "Communications manager %s", MsgSubSystemStarted)
defer func() {
// TO-DO shutdown comms connections for connected services (Slack etc)
log.Debugf(log.CommunicationMgr, "Communications manager %s", MsgSubSystemShutdown)
}()
for {
select {
case msg := <-m.relayMsg:
m.comms.PushEvent(msg)
case <-m.shutdown:
return
}
}
}

View File

@@ -0,0 +1,90 @@
# GoCryptoTrader package Communication_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/communication_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This communication_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Communication_manager
+ The communication manager subsystem is used to push events raised in GoCryptoTrader to any enabled communication system such as a Slack server
+ In order to modify the behaviour of the communication manager subsystem, you can edit the following inside your config file under `communications`:
### slack
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | Determines whether the push communications to a Slack server | `true` |
| verbose | If enabled will log more details to your logger output | `false` |
| targetChannel | The channel to send communications to | `announcements` |
| verificationToken | The token generated by Slack to allow interactions with the server and channel | `iamafaketoken` |
### smsGlobal
| Config | Description | Example |
| ------ | ----------- | ------- |
| name | The name of the SMS sender | `SMSGlobal` |
| from | Who the text name is from | `Skynet` |
| enabled | Determines whether the push communications to the SMS service | `true` |
| verbose | If enabled will log more details to your logger output | `false` |
| username | The username to use with the SMS provider | `username` |
| password | The username to use with the SMS provider | `password` |
| contacts | The `name` `number` of the user people you wish to send SMS to and whether it is `enabled` | `"name": "StyleGherkin", "number": "1231424", "enabled": true` |
### smtp
| Config | Description | Example |
| ------ | ----------- | ------- |
| name | The name of the service | `SMTP` |
| enabled | Determines whether the push communications to a email server | `true` |
| verbose | If enabled will log more details to your logger output | `false` |
| host | The SMTP host | `smtp.google.com` |
| port | The port to use | `537` |
| accountName | Your username | `username` |
| accountPassword | Your password | `password` |
| from | The display name of the sender | `Jeff Bezos` |
| recipientList | A comma delimited list of addresses to send alerts to | `bill@gates.com` |
### telegram
| Config | Description | Example |
| ------ | ----------- | ------- |
| name | The name to be displayed | `Telegram` |
| enabled | Determines whether the push communications to a Telegram server | `true` |
| verbose | If enabled will log more details to your logger output | `false` |
| verificationToken | The token generated by Telegram to allow you to send messages | `iamafaketoken` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,158 @@
package engine
import (
"errors"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/communications"
"github.com/thrasher-corp/gocryptotrader/communications/base"
)
func TestSetup(t *testing.T) {
t.Parallel()
_, err := SetupCommunicationManager(nil)
if !errors.Is(err, errNilConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilConfig)
}
_, err = SetupCommunicationManager(&base.CommunicationsConfig{})
if !errors.Is(err, communications.ErrNoCommunicationRelayersEnabled) {
t.Errorf("error '%v', expected '%v'", err, communications.ErrNoCommunicationRelayersEnabled)
}
m, err := SetupCommunicationManager(&base.CommunicationsConfig{
SlackConfig: base.SlackConfig{
Enabled: true,
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expected manager")
}
}
func TestIsRunning(t *testing.T) {
t.Parallel()
m, err := SetupCommunicationManager(&base.CommunicationsConfig{
SMSGlobalConfig: base.SMSGlobalConfig{
Enabled: true,
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.IsRunning() {
t.Error("expected true")
}
m.started = 0
if m.IsRunning() {
t.Error("expected false")
}
m = nil
if m.IsRunning() {
t.Error("expected false")
}
}
func TestStart(t *testing.T) {
t.Parallel()
m, err := SetupCommunicationManager(&base.CommunicationsConfig{
SMTPConfig: base.SMTPConfig{
Enabled: true,
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.started = 1
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
}
func TestGetStatus(t *testing.T) {
t.Parallel()
m, err := SetupCommunicationManager(&base.CommunicationsConfig{
TelegramConfig: base.TelegramConfig{
Enabled: true,
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
_, err = m.GetStatus()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.started = 0
_, err = m.GetStatus()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
}
func TestStop(t *testing.T) {
t.Parallel()
m, err := SetupCommunicationManager(&base.CommunicationsConfig{
SlackConfig: base.SlackConfig{
Enabled: true,
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
m = nil
err = m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestPushEvent(t *testing.T) {
t.Parallel()
m, err := SetupCommunicationManager(&base.CommunicationsConfig{
SlackConfig: base.SlackConfig{
Enabled: true,
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.PushEvent(base.Event{})
time.Sleep(time.Second)
m.PushEvent(base.Event{})
m = nil
m.PushEvent(base.Event{})
}

View File

@@ -1,67 +0,0 @@
package engine
import (
"fmt"
"sync/atomic"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/connchecker"
"github.com/thrasher-corp/gocryptotrader/engine/subsystem"
"github.com/thrasher-corp/gocryptotrader/log"
)
// connectionManager manages the connchecker
type connectionManager struct {
started int32
conn *connchecker.Checker
}
// Started returns if the connection manager has started
func (c *connectionManager) Started() bool {
return atomic.LoadInt32(&c.started) == 1
}
// Start starts an instance of the connection manager
func (c *connectionManager) Start(conf *config.ConnectionMonitorConfig) error {
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return fmt.Errorf("connection manager %w", subsystem.ErrSubSystemAlreadyStarted)
}
log.Debugln(log.ConnectionMgr, "Connection manager starting...")
var err error
c.conn, err = connchecker.New(conf.DNSList,
conf.PublicDomainList,
conf.CheckInterval)
if err != nil {
atomic.CompareAndSwapInt32(&c.started, 1, 0)
return err
}
log.Debugln(log.ConnectionMgr, "Connection manager started.")
return nil
}
// Stop stops the connection manager
func (c *connectionManager) Stop() error {
if atomic.LoadInt32(&c.started) == 0 {
return fmt.Errorf("connection manager %w", subsystem.ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&c.started, 1, 0)
}()
log.Debugln(log.ConnectionMgr, "Connection manager shutting down...")
c.conn.Shutdown()
log.Debugln(log.ConnectionMgr, "Connection manager stopped.")
return nil
}
// IsOnline returns if the connection manager is online
func (c *connectionManager) IsOnline() bool {
if c.conn == nil {
log.Warnln(log.ConnectionMgr, "Connection manager: IsOnline called but conn is nil")
return false
}
return c.conn.IsConnected()
}

View File

@@ -0,0 +1,100 @@
package engine
import (
"fmt"
"sync/atomic"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/connchecker"
"github.com/thrasher-corp/gocryptotrader/log"
)
// ConnectionManagerName is an exported subsystem name
const ConnectionManagerName = "internet_monitor"
// connectionManager manages the connchecker
type connectionManager struct {
started int32
conn *connchecker.Checker
cfg *config.ConnectionMonitorConfig
}
// IsRunning safely checks whether the subsystem is running
func (m *connectionManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// setupConnectionManager creates a connection manager
func setupConnectionManager(cfg *config.ConnectionMonitorConfig) (*connectionManager, error) {
if cfg == nil {
return nil, errNilConfig
}
if cfg.DNSList == nil {
cfg.DNSList = connchecker.DefaultDNSList
}
if cfg.PublicDomainList == nil {
cfg.PublicDomainList = connchecker.DefaultDomainList
}
if cfg.CheckInterval == 0 {
cfg.CheckInterval = connchecker.DefaultCheckInterval
}
return &connectionManager{
cfg: cfg,
}, nil
}
// Start runs the subsystem
func (m *connectionManager) Start() error {
if m == nil {
return fmt.Errorf("connection manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("connection manager %w", ErrSubSystemAlreadyStarted)
}
log.Debugln(log.ConnectionMgr, "Connection manager starting...")
var err error
m.conn, err = connchecker.New(m.cfg.DNSList,
m.cfg.PublicDomainList,
m.cfg.CheckInterval)
if err != nil {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
return err
}
log.Debugln(log.ConnectionMgr, "Connection manager started.")
return nil
}
// Stop stops the connection manager
func (m *connectionManager) Stop() error {
if m == nil {
return fmt.Errorf("connection manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("connection manager %w", ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}()
log.Debugln(log.ConnectionMgr, "Connection manager shutting down...")
m.conn.Shutdown()
log.Debugln(log.ConnectionMgr, "Connection manager stopped.")
return nil
}
// IsOnline returns if the connection manager is online
func (m *connectionManager) IsOnline() bool {
if m == nil {
return false
}
if m.conn == nil {
log.Warnln(log.ConnectionMgr, "Connection manager: IsOnline called but conn is nil")
return false
}
return m.conn.IsConnected()
}

View File

@@ -0,0 +1,53 @@
# GoCryptoTrader package Connection_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/connection_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This connection_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Connection_manager
+ The connection manager subsystem is used to periodically check whether the application is connected to the internet and will provide alerts of any changes
+ In order to modify the behaviour of the connection manager subsystem, you can edit the following inside your config file under `connectionMonitor`:
### connectionMonitor
| Config | Description | Example |
| ------ | ----------- | ------- |
| perferredDNSList | Is a string array of DNS servers to periodically verify whether GoCryptoTrader is connected to the internet | `["8.8.8.8","8.8.4.4","1.1.1.1","1.0.0.1"]` |
| preferredDomainList | Is a string array of domains to periodically verify whether GoCryptoTrader is connected to the internet | `["www.google.com","www.cloudflare.com","www.facebook.com"]` |
| checkInterval | A time period in golang `time.Duration` format to check whether GoCryptoTrader is connected to the internet | `1000000000` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,122 @@
package engine
import (
"errors"
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
)
func TestSetupConnectionManager(t *testing.T) {
t.Parallel()
_, err := setupConnectionManager(nil)
if !errors.Is(err, errNilConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilConfig)
}
m, err := setupConnectionManager(&config.ConnectionMonitorConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expected manager")
}
}
func TestConnectionMonitorIsRunning(t *testing.T) {
t.Parallel()
m, err := setupConnectionManager(&config.ConnectionMonitorConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.IsRunning() {
t.Error("expected true")
}
m.started = 0
if m.IsRunning() {
t.Error("expected false")
}
m = nil
if m.IsRunning() {
t.Error("expected false")
}
}
func TestConnectionMonitorStart(t *testing.T) {
t.Parallel()
m, err := setupConnectionManager(&config.ConnectionMonitorConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
m = nil
err = m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestConnectionMonitorStop(t *testing.T) {
t.Parallel()
m, err := setupConnectionManager(&config.ConnectionMonitorConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
m = nil
err = m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestConnectionMonitorIsOnline(t *testing.T) {
t.Parallel()
m, err := setupConnectionManager(&config.ConnectionMonitorConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
// If someone runs this offline, who are we to fail them?
m.IsOnline()
err = m.Stop()
if err != nil {
t.Fatal(err)
}
if m.IsOnline() {
t.Error("expected false")
}
m.conn = nil
if m.IsOnline() {
t.Error("expected false")
}
m = nil
if m.IsOnline() {
t.Error("expected false")
}
}

View File

@@ -1,132 +0,0 @@
package engine
import (
"errors"
"fmt"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/database"
dbpsql "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres"
dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3"
"github.com/thrasher-corp/gocryptotrader/engine/subsystem"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/sqlboiler/boil"
)
var (
dbConn *database.Instance
)
type databaseManager struct {
started int32
shutdown chan struct{}
}
func (a *databaseManager) Started() bool {
return atomic.LoadInt32(&a.started) == 1
}
func (a *databaseManager) Start(bot *Engine) (err error) {
if !atomic.CompareAndSwapInt32(&a.started, 0, 1) {
return fmt.Errorf("database manager %w", subsystem.ErrSubSystemAlreadyStarted)
}
defer func() {
if err != nil {
atomic.CompareAndSwapInt32(&a.started, 1, 0)
}
}()
log.Debugln(log.DatabaseMgr, "Database manager starting...")
a.shutdown = make(chan struct{})
if bot.Config.Database.Enabled {
if bot.Config.Database.Driver == database.DBPostgreSQL {
log.Debugf(log.DatabaseMgr,
"Attempting to establish database connection to host %s/%s utilising %s driver\n",
bot.Config.Database.Host,
bot.Config.Database.Database,
bot.Config.Database.Driver)
dbConn, err = dbpsql.Connect()
} else if bot.Config.Database.Driver == database.DBSQLite ||
bot.Config.Database.Driver == database.DBSQLite3 {
log.Debugf(log.DatabaseMgr,
"Attempting to establish database connection to %s utilising %s driver\n",
bot.Config.Database.Database,
bot.Config.Database.Driver)
dbConn, err = dbsqlite3.Connect()
}
if err != nil {
return fmt.Errorf("database failed to connect: %v Some features that utilise a database will be unavailable", err)
}
dbConn.Connected = true
DBLogger := database.Logger{}
if bot.Config.Database.Verbose {
boil.DebugMode = true
boil.DebugWriter = DBLogger
}
go a.run(bot)
return nil
}
return errors.New("database support disabled")
}
func (a *databaseManager) Stop() error {
if atomic.LoadInt32(&a.started) == 0 {
return fmt.Errorf("database manager %w", subsystem.ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&a.started, 1, 0)
}()
err := dbConn.SQL.Close()
if err != nil {
log.Errorf(log.DatabaseMgr, "Failed to close database: %v", err)
}
close(a.shutdown)
return nil
}
func (a *databaseManager) run(bot *Engine) {
log.Debugln(log.DatabaseMgr, "Database manager started.")
bot.ServicesWG.Add(1)
t := time.NewTicker(time.Second * 2)
defer func() {
t.Stop()
bot.ServicesWG.Done()
log.Debugln(log.DatabaseMgr, "Database manager shutdown.")
}()
for {
select {
case <-a.shutdown:
return
case <-t.C:
go a.checkConnection()
}
}
}
func (a *databaseManager) checkConnection() {
dbConn.Mu.Lock()
defer dbConn.Mu.Unlock()
err := dbConn.SQL.Ping()
if err != nil {
log.Errorf(log.DatabaseMgr, "Database connection error: %v\n", err)
dbConn.Connected = false
return
}
if !dbConn.Connected {
log.Info(log.DatabaseMgr, "Database connection reestablished")
dbConn.Connected = true
}
}

View File

@@ -0,0 +1,191 @@
package engine
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/database"
dbpsql "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres"
dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3"
"github.com/thrasher-corp/gocryptotrader/log"
)
// DatabaseConnectionManagerName is an exported subsystem name
const DatabaseConnectionManagerName = "database"
var (
errDatabaseDisabled = errors.New("database support disabled")
)
// DatabaseConnectionManager holds the database connection and its status
type DatabaseConnectionManager struct {
started int32
shutdown chan struct{}
enabled bool
verbose bool
host string
username string
password string
database string
driver string
wg sync.WaitGroup
dbConn *database.Instance
}
// IsRunning safely checks whether the subsystem is running
func (m *DatabaseConnectionManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// SetupDatabaseConnectionManager creates a new database manager
func SetupDatabaseConnectionManager(cfg *database.Config) (*DatabaseConnectionManager, error) {
if cfg == nil {
return nil, errNilConfig
}
m := &DatabaseConnectionManager{
shutdown: make(chan struct{}),
enabled: cfg.Enabled,
verbose: cfg.Verbose,
host: cfg.Host,
username: cfg.Username,
password: cfg.Password,
database: cfg.Database,
driver: cfg.Driver,
dbConn: database.DB,
}
err := m.dbConn.SetConfig(cfg)
if err != nil {
return nil, err
}
return m, nil
}
// Start sets up the database connection manager to maintain a SQL connection
func (m *DatabaseConnectionManager) Start(wg *sync.WaitGroup) (err error) {
if m == nil {
return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("database manager %w", ErrSubSystemAlreadyStarted)
}
defer func() {
if err != nil {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}
}()
log.Debugln(log.DatabaseMgr, "Database manager starting...")
if m.enabled {
m.shutdown = make(chan struct{})
switch m.driver {
case database.DBPostgreSQL:
log.Debugf(log.DatabaseMgr,
"Attempting to establish database connection to host %s/%s utilising %s driver\n",
m.host,
m.database,
m.driver)
m.dbConn, err = dbpsql.Connect()
case database.DBSQLite,
database.DBSQLite3:
log.Debugf(log.DatabaseMgr,
"Attempting to establish database connection to %s utilising %s driver\n",
m.database,
m.driver)
m.dbConn, err = dbsqlite3.Connect()
default:
return database.ErrNoDatabaseProvided
}
if err != nil {
return fmt.Errorf("%w: %v Some features that utilise a database will be unavailable", database.ErrFailedToConnect, err)
}
m.dbConn.SetConnected(true)
wg.Add(1)
m.wg.Add(1)
go m.run(wg)
return nil
}
return errDatabaseDisabled
}
// Stop stops the database manager and closes the connection
// Stop attempts to shutdown the subsystem
func (m *DatabaseConnectionManager) Stop() error {
if m == nil {
return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}()
err := m.dbConn.CloseConnection()
if err != nil {
log.Errorf(log.DatabaseMgr, "Failed to close database: %v", err)
}
close(m.shutdown)
m.wg.Wait()
return nil
}
func (m *DatabaseConnectionManager) run(wg *sync.WaitGroup) {
log.Debugln(log.DatabaseMgr, "Database manager started.")
t := time.NewTicker(time.Second * 2)
defer func() {
t.Stop()
m.wg.Done()
wg.Done()
log.Debugln(log.DatabaseMgr, "Database manager shutdown.")
}()
for {
select {
case <-m.shutdown:
return
case <-t.C:
err := m.checkConnection()
if err != nil {
log.Error(log.DatabaseMgr, "Database connection error:", err)
}
}
}
}
func (m *DatabaseConnectionManager) checkConnection() error {
if m == nil {
return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrSubSystemNotStarted)
}
if !m.enabled {
return database.ErrDatabaseSupportDisabled
}
if m.dbConn == nil {
return database.ErrNoDatabaseProvided
}
err := m.dbConn.Ping()
if err != nil {
m.dbConn.SetConnected(false)
return err
}
if !m.dbConn.IsConnected() {
log.Info(log.DatabaseMgr, "Database connection reestablished")
m.dbConn.SetConnected(true)
}
return nil
}

View File

@@ -0,0 +1,64 @@
# GoCryptoTrader package Database_connection
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/database_connection)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This database_connection package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Database_connection
+ The database connection manager subsystem is used to periodically check whether the application is connected to the database and will provide alerts of any changes
+ In order to modify the behaviour of the database connection manager subsystem, you can edit the following inside your config file under `database`:
### database
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | Enabled or disables the database connection subsystem | `true` |
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
| driver | The SQL driver to use. Can be `postgres` or `sqlite`. | `sqlite` |
| connectionDetails | See below | |
### connectionDetails
| Config | Description | Example |
| ------ | ----------- | ------- |
| host | The host address of the database | `localhost` |
| port | The port used to connect to the database | `5432` |
| username | An optional username to connect to the database | `username` |
| password | An optional password to connect to the database | `password` |
| database | The name of the database | `database.db` |
| sslmode | The connection type of the database for Postgres databases only | `disable` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,241 @@
package engine
import (
"errors"
"io/ioutil"
"log"
"os"
"sync"
"testing"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
)
func CreateDatabase(t *testing.T) string {
t.Helper()
// fun workarounds to globals ruining testing
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
log.Fatal(err)
}
database.DB.DataPath = tmpDir
return tmpDir
}
func Cleanup(t *testing.T, tmpDir string) {
if database.DB.IsConnected() {
err := database.DB.CloseConnection()
if err != nil {
log.Fatal(err)
}
err = os.RemoveAll(tmpDir)
if err != nil {
log.Fatal(err)
}
}
}
func TestSetupDatabaseConnectionManager(t *testing.T) {
_, err := SetupDatabaseConnectionManager(nil)
if !errors.Is(err, errNilConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilConfig)
}
m, err := SetupDatabaseConnectionManager(&database.Config{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expected manager")
}
}
func TestStartSQLite(t *testing.T) {
tmpDir := CreateDatabase(t)
defer Cleanup(t, tmpDir)
m, err := SetupDatabaseConnectionManager(&database.Config{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
var wg sync.WaitGroup
err = m.Start(&wg)
if !errors.Is(err, errDatabaseDisabled) {
t.Errorf("error '%v', expected '%v'", err, errDatabaseDisabled)
}
m, err = SetupDatabaseConnectionManager(&database.Config{Enabled: true})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start(&wg)
if !errors.Is(err, database.ErrNoDatabaseProvided) {
t.Errorf("error '%v', expected '%v'", err, database.ErrNoDatabaseProvided)
}
m.driver = database.DBSQLite
err = m.Start(&wg)
if !errors.Is(err, database.ErrFailedToConnect) {
t.Errorf("error '%v', expected '%v'", err, database.ErrFailedToConnect)
}
_, err = SetupDatabaseConnectionManager(&database.Config{
Enabled: true,
Driver: database.DBSQLite,
ConnectionDetails: drivers.ConnectionDetails{
Host: "localhost",
Database: "test.db",
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
// This test does not care for a successful connection
func TestStartPostgres(t *testing.T) {
tmpDir := CreateDatabase(t)
defer Cleanup(t, tmpDir)
m, err := SetupDatabaseConnectionManager(&database.Config{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
var wg sync.WaitGroup
err = m.Start(&wg)
if !errors.Is(err, errDatabaseDisabled) {
t.Errorf("error '%v', expected '%v'", err, errDatabaseDisabled)
}
m.enabled = true
err = m.Start(&wg)
if !errors.Is(err, database.ErrNoDatabaseProvided) {
t.Errorf("error '%v', expected '%v'", err, database.ErrNoDatabaseProvided)
}
m.driver = database.DBPostgreSQL
err = m.Start(&wg)
if !errors.Is(err, database.ErrFailedToConnect) {
t.Errorf("error '%v', expected '%v'", err, database.ErrFailedToConnect)
}
}
func TestDatabaseConnectionManagerIsRunning(t *testing.T) {
tmpDir := CreateDatabase(t)
defer Cleanup(t, tmpDir)
m, err := SetupDatabaseConnectionManager(&database.Config{
Enabled: true,
Driver: database.DBSQLite,
ConnectionDetails: drivers.ConnectionDetails{
Host: "localhost",
Database: "test.db",
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.IsRunning() {
t.Error("expected false")
}
var wg sync.WaitGroup
err = m.Start(&wg)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.IsRunning() {
t.Error("expected true")
}
m = nil
if m.IsRunning() {
t.Error("expected false")
}
}
func TestDatabaseConnectionManagerStop(t *testing.T) {
tmpDir := CreateDatabase(t)
defer Cleanup(t, tmpDir)
m, err := SetupDatabaseConnectionManager(&database.Config{
Enabled: true,
Driver: database.DBSQLite,
ConnectionDetails: drivers.ConnectionDetails{
Host: "localhost",
Database: "test.db",
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
var wg sync.WaitGroup
err = m.Start(&wg)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m = nil
err = m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestCheckConnection(t *testing.T) {
tmpDir := CreateDatabase(t)
defer Cleanup(t, tmpDir)
var m *DatabaseConnectionManager
err := m.checkConnection()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
m, err = SetupDatabaseConnectionManager(&database.Config{
Enabled: true,
Driver: database.DBSQLite,
ConnectionDetails: drivers.ConnectionDetails{
Host: "localhost",
Database: "test.db",
},
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.checkConnection()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
var wg sync.WaitGroup
err = m.Start(&wg)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.checkConnection()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.checkConnection()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start(&wg)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.checkConnection()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.dbConn.SetConnected(false)
err = m.checkConnection()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}

91
engine/depositaddress.go Normal file
View File

@@ -0,0 +1,91 @@
package engine
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/thrasher-corp/gocryptotrader/currency"
)
// vars related to the deposit address helpers
var (
ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil")
ErrDepositAddressNotFound = errors.New("deposit address does not exist")
)
// DepositAddressManager manages the exchange deposit address store
type DepositAddressManager struct {
m sync.Mutex
store map[string]map[string]string
}
// SetupDepositAddressManager returns a DepositAddressManager
func SetupDepositAddressManager() *DepositAddressManager {
return &DepositAddressManager{
store: make(map[string]map[string]string),
}
}
// GetDepositAddressByExchangeAndCurrency returns a deposit address for the specified exchange and cryptocurrency
// if it exists
func (m *DepositAddressManager) GetDepositAddressByExchangeAndCurrency(exchName string, currencyItem currency.Code) (string, error) {
m.m.Lock()
defer m.m.Unlock()
if len(m.store) == 0 {
return "", ErrDepositAddressStoreIsNil
}
r, ok := m.store[strings.ToUpper(exchName)]
if !ok {
return "", ErrExchangeNotFound
}
addr, ok := r[strings.ToUpper(currencyItem.String())]
if !ok {
return "", ErrDepositAddressNotFound
}
return addr, nil
}
// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified
// exchange if they exist
func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string]string, error) {
m.m.Lock()
defer m.m.Unlock()
if len(m.store) == 0 {
return nil, ErrDepositAddressStoreIsNil
}
r, ok := m.store[strings.ToUpper(exchName)]
if !ok {
return nil, ErrDepositAddressNotFound
}
return r, nil
}
// Sync synchronises all deposit addresses
func (m *DepositAddressManager) Sync(addresses map[string]map[string]string) error {
if m == nil {
return fmt.Errorf("deposit address manager %w", ErrNilSubsystem)
}
m.m.Lock()
defer m.m.Unlock()
if m.store == nil {
return ErrDepositAddressStoreIsNil
}
for k, v := range addresses {
r := make(map[string]string)
for w, x := range v {
r[strings.ToUpper(w)] = x
}
m.store[strings.ToUpper(k)] = r
}
return nil
}

45
engine/depositaddress.md Normal file
View File

@@ -0,0 +1,45 @@
# GoCryptoTrader package Depositaddress
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/depositaddress)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This depositaddress package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Depositaddress
+ The deposit address manager subsystem stores Exchange deposit addresses.
+ On start of the application the engine Bot will retrieve deposit addresses from exchanges if you have API keys set
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,96 @@
package engine
import (
"errors"
"testing"
"github.com/thrasher-corp/gocryptotrader/currency"
)
const (
address = "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX"
bitStamp = "BITSTAMP"
btc = "BTC"
)
func TestSetupDepositAddressManager(t *testing.T) {
m := SetupDepositAddressManager()
if m.store == nil {
t.Fatal("expected store")
}
}
func TestSync(t *testing.T) {
m := SetupDepositAddressManager()
err := m.Sync(map[string]map[string]string{
bitStamp: {
btc: address,
},
})
if err != nil {
t.Error(err)
}
r, err := m.GetDepositAddressByExchangeAndCurrency(bitStamp, currency.BTC)
if err != nil {
t.Error("unexpected result")
}
if r != address {
t.Error("unexpected result")
}
m.store = nil
err = m.Sync(map[string]map[string]string{
bitStamp: {
btc: address,
},
})
if !errors.Is(err, ErrDepositAddressStoreIsNil) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil)
}
m = nil
err = m.Sync(map[string]map[string]string{
bitStamp: {
btc: address,
},
})
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("received %v, expected %v", err, ErrNilSubsystem)
}
}
func TestGetDepositAddressByExchangeAndCurrency(t *testing.T) {
m := SetupDepositAddressManager()
_, err := m.GetDepositAddressByExchangeAndCurrency("", currency.BTC)
if !errors.Is(err, ErrDepositAddressStoreIsNil) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil)
}
m.store = map[string]map[string]string{
bitStamp: {
btc: address,
},
}
_, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, currency.BTC)
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)
}
}
func TestGetDepositAddressesByExchange(t *testing.T) {
m := SetupDepositAddressManager()
_, err := m.GetDepositAddressesByExchange("")
if !errors.Is(err, ErrDepositAddressStoreIsNil) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil)
}
m.store = map[string]map[string]string{
bitStamp: {
btc: address,
},
}
_, err = m.GetDepositAddressesByExchange(bitStamp)
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)
}
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"
@@ -15,39 +16,42 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap"
"github.com/thrasher-corp/gocryptotrader/dispatch"
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/trade"
gctscript "github.com/thrasher-corp/gocryptotrader/gctscript/vm"
gctlog "github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
"github.com/thrasher-corp/gocryptotrader/utils"
)
// Engine contains configuration, portfolio, exchange & ticker data and is the
// Engine contains configuration, portfolio manager, exchange & ticker data and is the
// overarching type across this code base.
type Engine struct {
Config *config.Config
Portfolio *portfolio.Base
ExchangeCurrencyPairManager *ExchangeCurrencyPairSyncer
NTPManager ntpManager
ConnectionManager connectionManager
DatabaseManager databaseManager
GctScriptManager *gctscript.GctScriptManager
OrderManager orderManager
PortfolioManager portfolioManager
CommsManager commsManager
exchangeManager exchangeManager
DepositAddressManager *DepositAddressManager
Settings Settings
Uptime time.Time
ServicesWG sync.WaitGroup
Config *config.Config
apiServer *apiServerManager
CommunicationsManager *CommunicationManager
connectionManager *connectionManager
currencyPairSyncer *syncManager
DatabaseManager *DatabaseConnectionManager
DepositAddressManager *DepositAddressManager
eventManager *eventManager
ExchangeManager *ExchangeManager
ntpManager *ntpManager
OrderManager *OrderManager
portfolioManager *portfolioManager
gctScriptManager *gctscript.GctScriptManager
websocketRoutineManager *websocketRoutineManager
WithdrawManager *WithdrawManager
Settings Settings
uptime time.Time
ServicesWG sync.WaitGroup
}
// Vars for engine
var (
Bot *Engine
)
// Bot is a happy global engine to allow various areas of the application
// to access its setup services and functions
var Bot *Engine
// New starts a new engine
func New() (*Engine, error) {
@@ -60,10 +64,6 @@ func New() (*Engine, error) {
if err != nil {
return nil, fmt.Errorf("failed to load config. Err: %s", err)
}
b.GctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript)
if err != nil {
return nil, fmt.Errorf("failed to create script manager. Err: %s", err)
}
return &b, nil
}
@@ -99,7 +99,7 @@ func NewFromSettings(settings *Settings, flagSet map[string]bool) (*Engine, erro
return nil, fmt.Errorf("unable to adjust runtime GOMAXPROCS value. Err: %s", err)
}
b.GctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript)
b.gctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript)
if err != nil {
return nil, fmt.Errorf("failed to create script manager. Err: %s", err)
}
@@ -182,7 +182,7 @@ func validateSettings(b *Engine, s *Settings, flagSet map[string]bool) {
if flagSet["maxvirtualmachines"] {
maxMachines := uint8(s.MaxVirtualMachines)
b.GctScriptManager.MaxVirtualMachines = &maxMachines
b.gctScriptManager.MaxVirtualMachines = &maxMachines
}
if flagSet["withdrawcachesize"] {
@@ -344,36 +344,57 @@ func (bot *Engine) Start() error {
if bot == nil {
return errors.New("engine instance is nil")
}
var err error
newEngineMutex.Lock()
defer newEngineMutex.Unlock()
if bot.Settings.EnableDatabaseManager {
if err := bot.DatabaseManager.Start(bot); err != nil {
gctlog.Errorf(gctlog.Global, "Database manager unable to start: %v", err)
bot.DatabaseManager, err = SetupDatabaseConnectionManager(&bot.Config.Database)
if err != nil {
gctlog.Errorf(gctlog.Global, "Database manager unable to setup: %v", err)
} else {
err = bot.DatabaseManager.Start(&bot.ServicesWG)
if err != nil {
gctlog.Errorf(gctlog.Global, "Database manager unable to start: %v", err)
}
}
}
if bot.Settings.EnableDispatcher {
if err := dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit); err != nil {
if err = dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit); err != nil {
gctlog.Errorf(gctlog.DispatchMgr, "Dispatcher unable to start: %v", err)
}
}
// Sets up internet connectivity monitor
if bot.Settings.EnableConnectivityMonitor {
if err := bot.ConnectionManager.Start(&bot.Config.ConnectionMonitor); err != nil {
gctlog.Errorf(gctlog.Global, "Connection manager unable to start: %v", err)
bot.connectionManager, err = setupConnectionManager(&bot.Config.ConnectionMonitor)
if err != nil {
gctlog.Errorf(gctlog.Global, "Connection manager unable to setup: %v", err)
} else {
err = bot.connectionManager.Start()
if err != nil {
gctlog.Errorf(gctlog.Global, "Connection manager unable to start: %v", err)
}
}
}
if bot.Settings.EnableNTPClient {
if err := bot.NTPManager.Start(); err != nil {
gctlog.Errorf(gctlog.Global, "NTP manager unable to start: %v", err)
if bot.Config.NTPClient.Level == 0 {
var responseMessage string
responseMessage, err = bot.Config.SetNTPCheck(os.Stdin)
if err != nil {
return fmt.Errorf("unable to set NTP check: %w", err)
}
gctlog.Info(gctlog.TimeMgr, responseMessage)
}
bot.ntpManager, err = setupNTPManager(&bot.Config.NTPClient, *bot.Config.Logging.Enabled)
if err != nil {
gctlog.Errorf(gctlog.Global, "NTP manager unable to start: %s", err)
}
}
bot.Uptime = time.Now()
bot.uptime = time.Now()
gctlog.Debugf(gctlog.Global, "Bot '%s' started.\n", bot.Config.Name)
gctlog.Debugf(gctlog.Global, "Using data dir: %s\n", bot.Settings.DataDir)
if *bot.Config.Logging.Enabled && strings.Contains(bot.Config.Logging.Output, "file") {
@@ -398,15 +419,22 @@ func (bot *Engine) Start() error {
bot.Config.PurgeExchangeAPICredentials()
}
bot.ExchangeManager = SetupExchangeManager()
gctlog.Debugln(gctlog.Global, "Setting up exchanges..")
err := bot.SetupExchanges()
err = bot.SetupExchanges()
if err != nil {
return err
}
if bot.Settings.EnableCommsRelayer {
if err = bot.CommsManager.Start(); err != nil {
gctlog.Errorf(gctlog.Global, "Communications manager unable to start: %v\n", err)
bot.CommunicationsManager, err = SetupCommunicationManager(&bot.Config.Communications)
if err != nil {
gctlog.Errorf(gctlog.Global, "Communications manager unable to setup: %s", err)
} else {
err = bot.CommunicationsManager.Start()
if err != nil {
gctlog.Errorf(gctlog.Global, "Communications manager unable to start: %s", err)
}
}
}
if bot.Settings.EnableCoinmarketcapAnalysis ||
@@ -433,7 +461,7 @@ func (bot *Engine) Start() error {
},
bot.Settings.DataDir)
if err != nil {
gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %v", err)
gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %s", err)
}
}
@@ -441,34 +469,79 @@ func (bot *Engine) Start() error {
go StartRPCServer(bot)
}
if bot.Settings.EnableDeprecatedRPC {
go StartRESTServer(bot)
}
if bot.Settings.EnableWebsocketRPC {
go StartWebsocketServer(bot)
StartWebsocketHandler()
}
if bot.Settings.EnablePortfolioManager {
if err = bot.PortfolioManager.Start(); err != nil {
gctlog.Errorf(gctlog.Global, "Fund manager unable to start: %v", err)
if bot.portfolioManager == nil {
bot.portfolioManager, err = setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, &bot.Config.Portfolio)
if err != nil {
gctlog.Errorf(gctlog.Global, "portfolio manager unable to setup: %s", err)
} else {
err = bot.portfolioManager.Start(&bot.ServicesWG)
if err != nil {
gctlog.Errorf(gctlog.Global, "portfolio manager unable to start: %s", err)
}
}
}
}
bot.WithdrawManager, err = SetupWithdrawManager(bot.ExchangeManager, bot.portfolioManager, bot.Settings.EnableDryRun)
if err != nil {
return err
}
if bot.Settings.EnableDeprecatedRPC ||
bot.Settings.EnableWebsocketRPC {
var filePath string
filePath, err = config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile)
if err != nil {
return err
}
bot.apiServer, err = setupAPIServerManager(&bot.Config.RemoteControl, &bot.Config.Profiler, bot.ExchangeManager, bot, bot.portfolioManager, filePath)
if err != nil {
gctlog.Errorf(gctlog.Global, "API Server unable to start: %s", err)
} else {
if bot.Settings.EnableDeprecatedRPC {
err = bot.apiServer.StartRESTServer()
if err != nil {
gctlog.Errorf(gctlog.Global, "could not start REST API server: %s", err)
}
}
if bot.Settings.EnableWebsocketRPC {
err = bot.apiServer.StartWebsocketServer()
if err != nil {
gctlog.Errorf(gctlog.Global, "could not start websocket API server: %s", err)
}
}
}
}
if bot.Settings.EnableDepositAddressManager {
bot.DepositAddressManager = new(DepositAddressManager)
go bot.DepositAddressManager.Sync()
bot.DepositAddressManager = SetupDepositAddressManager()
go func() {
err = bot.DepositAddressManager.Sync(bot.GetExchangeCryptocurrencyDepositAddresses())
if err != nil {
gctlog.Errorf(gctlog.Global, "Deposit address manager unable to setup: %s", err)
}
}()
}
if bot.Settings.EnableOrderManager {
if err = bot.OrderManager.Start(bot); err != nil {
gctlog.Errorf(gctlog.Global, "Order manager unable to start: %v", err)
bot.OrderManager, err = SetupOrderManager(
bot.ExchangeManager,
bot.CommunicationsManager,
&bot.ServicesWG,
bot.Settings.Verbose)
if err != nil {
gctlog.Errorf(gctlog.Global, "Order manager unable to setup: %s", err)
} else {
err = bot.OrderManager.Start()
if err != nil {
gctlog.Errorf(gctlog.Global, "Order manager unable to start: %s", err)
}
}
}
if bot.Settings.EnableExchangeSyncManager {
exchangeSyncCfg := CurrencyPairSyncerConfig{
exchangeSyncCfg := &Config{
SyncTicker: bot.Settings.EnableTickerSyncing,
SyncOrderbook: bot.Settings.EnableOrderbookSyncing,
SyncTrades: bot.Settings.EnableTradeSyncing,
@@ -479,25 +552,54 @@ func (bot *Engine) Start() error {
SyncTimeoutWebsocket: bot.Settings.SyncTimeoutWebsocket,
}
bot.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(exchangeSyncCfg)
bot.currencyPairSyncer, err = setupSyncManager(
exchangeSyncCfg,
bot.ExchangeManager,
bot.websocketRoutineManager,
&bot.Config.RemoteControl)
if err != nil {
gctlog.Warnf(gctlog.Global, "Unable to initialise exchange currency pair syncer. Err: %s", err)
gctlog.Errorf(gctlog.Global, "Unable to initialise exchange currency pair syncer. Err: %s", err)
} else {
go bot.ExchangeCurrencyPairManager.Start()
go func() {
err = bot.currencyPairSyncer.Start()
if err != nil {
gctlog.Errorf(gctlog.Global, "failed to start exchange currency pair manager. Err: %s", err)
}
}()
}
}
if bot.Settings.EnableEventManager {
go EventManger(bot.Settings.Verbose, &bot.CommsManager)
bot.eventManager, err = setupEventManager(bot.CommunicationsManager, bot.ExchangeManager, bot.Settings.EventManagerDelay, bot.Settings.EnableDryRun)
if err != nil {
gctlog.Errorf(gctlog.Global, "Unable to initialise event manager. Err: %s", err)
} else {
err = bot.eventManager.Start()
if err != nil {
gctlog.Errorf(gctlog.Global, "failed to start event manager. Err: %s", err)
}
}
}
if bot.Settings.EnableWebsocketRoutine {
go bot.WebsocketRoutine()
bot.websocketRoutineManager, err = setupWebsocketRoutineManager(bot.ExchangeManager, bot.OrderManager, bot.currencyPairSyncer, &bot.Config.Currency, bot.Settings.Verbose)
if err != nil {
gctlog.Errorf(gctlog.Global, "Unable to initialise websocket routine manager. Err: %s", err)
} else {
err = bot.websocketRoutineManager.Start()
if err != nil {
gctlog.Errorf(gctlog.Global, "failed to start websocket routine manager. Err: %s", err)
}
}
}
if bot.Settings.EnableGCTScriptManager {
if err := bot.GctScriptManager.Start(&bot.ServicesWG); err != nil {
gctlog.Errorf(gctlog.Global, "GCTScript manager unable to start: %v", err)
bot.gctScriptManager, err = gctscript.NewManager(&bot.Config.GCTScript)
if err != nil {
gctlog.Errorf(gctlog.Global, "failed to create script manager. Err: %s", err)
}
if err := bot.gctScriptManager.Start(&bot.ServicesWG); err != nil {
gctlog.Errorf(gctlog.Global, "GCTScript manager unable to start: %s", err)
}
}
@@ -511,46 +613,64 @@ func (bot *Engine) Stop() {
gctlog.Debugln(gctlog.Global, "Engine shutting down..")
if len(portfolio.Portfolio.Addresses) != 0 {
bot.Config.Portfolio = portfolio.Portfolio
if len(bot.portfolioManager.GetAddresses()) != 0 {
bot.Config.Portfolio = *bot.portfolioManager.GetPortfolio()
}
if bot.GctScriptManager.Started() {
if err := bot.GctScriptManager.Stop(); err != nil {
if bot.gctScriptManager.IsRunning() {
if err := bot.gctScriptManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "GCTScript manager unable to stop. Error: %v", err)
}
}
if bot.OrderManager.Started() {
if bot.OrderManager.IsRunning() {
if err := bot.OrderManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Order manager unable to stop. Error: %v", err)
}
}
if bot.NTPManager.Started() {
if err := bot.NTPManager.Stop(); err != nil {
if bot.eventManager.IsRunning() {
if err := bot.eventManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "event manager unable to stop. Error: %v", err)
}
}
if bot.ntpManager.IsRunning() {
if err := bot.ntpManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "NTP manager unable to stop. Error: %v", err)
}
}
if bot.CommsManager.Started() {
if err := bot.CommsManager.Stop(); err != nil {
if bot.CommunicationsManager.IsRunning() {
if err := bot.CommunicationsManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Communication manager unable to stop. Error: %v", err)
}
}
if bot.PortfolioManager.Started() {
if err := bot.PortfolioManager.Stop(); err != nil {
if bot.portfolioManager.IsRunning() {
if err := bot.portfolioManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Fund manager unable to stop. Error: %v", err)
}
}
if bot.ConnectionManager.Started() {
if err := bot.ConnectionManager.Stop(); err != nil {
if bot.connectionManager.IsRunning() {
if err := bot.connectionManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Connection manager unable to stop. Error: %v", err)
}
}
if bot.DatabaseManager.Started() {
if bot.apiServer.IsRESTServerRunning() {
if err := bot.apiServer.StopRESTServer(); err != nil {
gctlog.Errorf(gctlog.Global, "API Server unable to stop REST server. Error: %s", err)
}
}
if bot.apiServer.IsWebsocketServerRunning() {
if err := bot.apiServer.StopWebsocketServer(); err != nil {
gctlog.Errorf(gctlog.Global, "API Server unable to stop websocket server. Error: %s", err)
}
}
if bot.DatabaseManager.IsRunning() {
if err := bot.DatabaseManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Database manager unable to stop. Error: %v", err)
}
@@ -561,6 +681,11 @@ func (bot *Engine) Stop() {
gctlog.Errorf(gctlog.DispatchMgr, "Dispatch system unable to stop. Error: %v", err)
}
}
if bot.websocketRoutineManager.IsRunning() {
if err := bot.websocketRoutineManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "websocket routine manager unable to stop. Error: %v", err)
}
}
if bot.Settings.EnableCoinmarketcapAnalysis ||
bot.Settings.EnableCurrencyConverter ||
@@ -589,3 +714,233 @@ func (bot *Engine) Stop() {
log.Printf("Failed to close logger. Error: %v\n", err)
}
}
// GetExchangeByName returns an exchange given an exchange name
func (bot *Engine) GetExchangeByName(exchName string) exchange.IBotExchange {
return bot.ExchangeManager.GetExchangeByName(exchName)
}
// UnloadExchange unloads an exchange by name
func (bot *Engine) UnloadExchange(exchName string) error {
exchCfg, err := bot.Config.GetExchangeConfig(exchName)
if err != nil {
return err
}
err = bot.ExchangeManager.RemoveExchange(exchName)
if err != nil {
return err
}
exchCfg.Enabled = false
return nil
}
// GetExchanges retrieves the loaded exchanges
func (bot *Engine) GetExchanges() []exchange.IBotExchange {
return bot.ExchangeManager.GetExchanges()
}
// LoadExchange loads an exchange by name
func (bot *Engine) LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error {
exch, err := bot.ExchangeManager.NewExchangeByName(name)
if err != nil {
return err
}
if exch.GetBase() == nil {
return ErrExchangeFailedToLoad
}
var localWG sync.WaitGroup
localWG.Add(1)
go func() {
exch.SetDefaults()
localWG.Done()
}()
exchCfg, err := bot.Config.GetExchangeConfig(name)
if err != nil {
return err
}
if bot.Settings.EnableAllPairs &&
exchCfg.CurrencyPairs != nil {
assets := exchCfg.CurrencyPairs.GetAssetTypes()
for x := range assets {
var pairs currency.Pairs
pairs, err = exchCfg.CurrencyPairs.GetPairs(assets[x], false)
if err != nil {
return err
}
exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true)
}
}
if bot.Settings.EnableExchangeVerbose {
exchCfg.Verbose = true
}
if exchCfg.Features != nil {
if bot.Settings.EnableExchangeWebsocketSupport &&
exchCfg.Features.Supports.Websocket {
exchCfg.Features.Enabled.Websocket = true
}
if bot.Settings.EnableExchangeAutoPairUpdates &&
exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates {
exchCfg.Features.Enabled.AutoPairUpdates = true
}
if bot.Settings.DisableExchangeAutoPairUpdates {
if exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates {
exchCfg.Features.Enabled.AutoPairUpdates = false
}
}
}
if bot.Settings.HTTPUserAgent != "" {
exchCfg.HTTPUserAgent = bot.Settings.HTTPUserAgent
}
if bot.Settings.HTTPProxy != "" {
exchCfg.ProxyAddress = bot.Settings.HTTPProxy
}
if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout {
exchCfg.HTTPTimeout = bot.Settings.HTTPTimeout
}
if bot.Settings.EnableExchangeHTTPDebugging {
exchCfg.HTTPDebugging = bot.Settings.EnableExchangeHTTPDebugging
}
localWG.Wait()
if !bot.Settings.EnableExchangeHTTPRateLimiter {
gctlog.Warnf(gctlog.ExchangeSys,
"Loaded exchange %s rate limiting has been turned off.\n",
exch.GetName(),
)
err = exch.DisableRateLimiter()
if err != nil {
gctlog.Errorf(gctlog.ExchangeSys,
"Loaded exchange %s rate limiting cannot be turned off: %s.\n",
exch.GetName(),
err,
)
}
}
exchCfg.Enabled = true
err = exch.Setup(exchCfg)
if err != nil {
exchCfg.Enabled = false
return err
}
bot.ExchangeManager.Add(exch)
base := exch.GetBase()
if base.API.AuthenticatedSupport ||
base.API.AuthenticatedWebsocketSupport {
assetTypes := base.GetAssetTypes()
var useAsset asset.Item
for a := range assetTypes {
err = base.CurrencyPairs.IsAssetEnabled(assetTypes[a])
if err != nil {
continue
}
useAsset = assetTypes[a]
break
}
err = exch.ValidateCredentials(useAsset)
if err != nil {
gctlog.Warnf(gctlog.ExchangeSys,
"%s: Cannot validate credentials, authenticated support has been disabled, Error: %s\n",
base.Name,
err)
base.API.AuthenticatedSupport = false
base.API.AuthenticatedWebsocketSupport = false
exchCfg.API.AuthenticatedSupport = false
exchCfg.API.AuthenticatedWebsocketSupport = false
}
}
if useWG {
exch.Start(wg)
} else {
tempWG := sync.WaitGroup{}
exch.Start(&tempWG)
tempWG.Wait()
}
return nil
}
func (bot *Engine) dryRunParamInteraction(param string) {
if !bot.Settings.CheckParamInteraction {
return
}
if !bot.Settings.EnableDryRun {
gctlog.Warnf(gctlog.Global,
"Command line argument '-%s' induces dry run mode."+
" Set -dryrun=false if you wish to override this.",
param)
bot.Settings.EnableDryRun = true
}
}
// SetupExchanges sets up the exchanges used by the Bot
func (bot *Engine) SetupExchanges() error {
var wg sync.WaitGroup
configs := bot.Config.GetAllExchangeConfigs()
if bot.Settings.EnableAllPairs {
bot.dryRunParamInteraction("enableallpairs")
}
if bot.Settings.EnableAllExchanges {
bot.dryRunParamInteraction("enableallexchanges")
}
if bot.Settings.EnableExchangeVerbose {
bot.dryRunParamInteraction("exchangeverbose")
}
if bot.Settings.EnableExchangeWebsocketSupport {
bot.dryRunParamInteraction("exchangewebsocketsupport")
}
if bot.Settings.EnableExchangeAutoPairUpdates {
bot.dryRunParamInteraction("exchangeautopairupdates")
}
if bot.Settings.DisableExchangeAutoPairUpdates {
bot.dryRunParamInteraction("exchangedisableautopairupdates")
}
if bot.Settings.HTTPUserAgent != "" {
bot.dryRunParamInteraction("httpuseragent")
}
if bot.Settings.HTTPProxy != "" {
bot.dryRunParamInteraction("httpproxy")
}
if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout {
bot.dryRunParamInteraction("httptimeout")
}
if bot.Settings.EnableExchangeHTTPDebugging {
bot.dryRunParamInteraction("exchangehttpdebugging")
}
for x := range configs {
if !configs[x].Enabled && !bot.Settings.EnableAllExchanges {
gctlog.Debugf(gctlog.ExchangeSys, "%s: Exchange support: Disabled\n", configs[x].Name)
continue
}
wg.Add(1)
cfg := configs[x]
go func(currCfg config.ExchangeConfig) {
defer wg.Done()
err := bot.LoadExchange(currCfg.Name, true, &wg)
if err != nil {
gctlog.Errorf(gctlog.ExchangeSys, "LoadExchange %s failed: %s\n", currCfg.Name, err)
return
}
gctlog.Debugf(gctlog.ExchangeSys,
"%s: Exchange support: Enabled (Authenticated API support: %s - Verbose mode: %s).\n",
currCfg.Name,
common.IsEnabled(currCfg.API.AuthenticatedSupport),
common.IsEnabled(currCfg.Verbose),
)
}(cfg)
}
wg.Wait()
if len(bot.ExchangeManager.GetExchanges()) == 0 {
return ErrNoExchangesLoaded
}
return nil
}

View File

@@ -1,6 +1,8 @@
package engine
import (
"errors"
"io/ioutil"
"os"
"testing"
@@ -81,7 +83,7 @@ func TestStartStopDoesNotCausePanic(t *testing.T) {
if err != nil {
t.Error(err)
}
botOne.Settings.EnableGRPCProxy = false
if err = botOne.Start(); err != nil {
t.Error(err)
}
@@ -89,23 +91,51 @@ func TestStartStopDoesNotCausePanic(t *testing.T) {
botOne.Stop()
}
var enableExperimentalTest = false
func TestStartStopTwoDoesNotCausePanic(t *testing.T) {
t.Skip("Closing global currency.storage from two bots causes panic")
t.Parallel()
if !enableExperimentalTest {
t.Skip("test is functional, however does not need to be included in go test runs")
}
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err)
}
tempDir2, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err)
}
defer func() {
err = os.RemoveAll(tempDir)
if err != nil {
t.Error(err)
}
err = os.RemoveAll(tempDir2)
if err != nil {
t.Error(err)
}
}()
botOne, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
EnableDryRun: true,
DataDir: tempDir,
}, nil)
if err != nil {
t.Error(err)
}
botOne.Settings.EnableGRPCProxy = false
botTwo, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
EnableDryRun: true,
DataDir: tempDir2,
}, nil)
if err != nil {
t.Error(err)
}
botTwo.Settings.EnableGRPCProxy = false
if err = botOne.Start(); err != nil {
t.Error(err)
}
@@ -116,3 +146,113 @@ func TestStartStopTwoDoesNotCausePanic(t *testing.T) {
botOne.Stop()
botTwo.Stop()
}
func TestCheckExchangeExists(t *testing.T) {
e := CreateTestBot(t)
if e.GetExchangeByName(testExchange) == nil {
t.Errorf("TestGetExchangeExists: Unable to find exchange")
}
if e.GetExchangeByName("Asdsad") != nil {
t.Errorf("TestGetExchangeExists: Non-existent exchange found")
}
}
func TestGetExchangeByName(t *testing.T) {
e := CreateTestBot(t)
exch := e.GetExchangeByName(testExchange)
if exch == nil {
t.Errorf("TestGetExchangeByName: Failed to get exchange")
}
if !exch.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
exch.SetEnabled(false)
bfx := e.GetExchangeByName(testExchange)
if bfx.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
if exch.GetName() != testExchange {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
exch = e.GetExchangeByName("Asdasd")
if exch != nil {
t.Errorf("TestGetExchangeByName: Non-existent exchange found")
}
}
func TestUnloadExchange(t *testing.T) {
e := CreateTestBot(t)
err := e.UnloadExchange("asdf")
if !errors.Is(err, config.ErrExchangeNotFound) {
t.Errorf("error '%v', expected '%v'", err, config.ErrExchangeNotFound)
}
err = e.UnloadExchange(testExchange)
if err != nil {
t.Errorf("TestUnloadExchange: Failed to get exchange. %s",
err)
}
err = e.UnloadExchange(testExchange)
if !errors.Is(err, ErrNoExchangesLoaded) {
t.Errorf("error '%v', expected '%v'", err, ErrNoExchangesLoaded)
}
}
func TestDryRunParamInteraction(t *testing.T) {
bot := CreateTestBot(t)
// Simulate overiding default settings and ensure that enabling exchange
// verbose mode will be set on Bitfinex
var err error
if err = bot.UnloadExchange(testExchange); err != nil {
t.Error(err)
}
bot.Settings.CheckParamInteraction = false
bot.Settings.EnableExchangeVerbose = false
if err = bot.LoadExchange(testExchange, false, nil); err != nil {
t.Error(err)
}
exchCfg, err := bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if exchCfg.Verbose {
t.Error("verbose should have been disabled")
}
if err = bot.UnloadExchange(testExchange); err != nil {
t.Error(err)
}
// Now set dryrun mode to true,
// enable exchange verbose mode and verify that verbose mode
// will be set on Bitfinex
bot.Settings.EnableDryRun = true
bot.Settings.CheckParamInteraction = true
bot.Settings.EnableExchangeVerbose = true
if err = bot.LoadExchange(testExchange, false, nil); err != nil {
t.Error(err)
}
exchCfg, err = bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if !bot.Settings.EnableDryRun ||
!exchCfg.Verbose {
t.Error("dryrun should be true and verbose should be true")
}
}

View File

@@ -96,6 +96,8 @@ const (
MsgStatusSuccess string = "success"
// MsgStatusError message to display when failure occurs
MsgStatusError string = "error"
grpcName string = "grpc"
grpcProxyName string = "grpc_proxy"
)
// newConfigMutex only locks and unlocks on engine creation functions

334
engine/event_manager.go Normal file
View File

@@ -0,0 +1,334 @@
package engine
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/log"
)
// setupEventManager loads and validates the communications manager config
func setupEventManager(comManager iCommsManager, exchangeManager iExchangeManager, sleepDelay time.Duration, verbose bool) (*eventManager, error) {
if comManager == nil {
return nil, errNilComManager
}
if exchangeManager == nil {
return nil, errNilExchangeManager
}
if sleepDelay <= 0 {
sleepDelay = EventSleepDelay
}
return &eventManager{
comms: comManager,
exchangeManager: exchangeManager,
verbose: verbose,
sleepDelay: sleepDelay,
shutdown: make(chan struct{}),
}, nil
}
// Start runs the subsystem
func (m *eventManager) Start() error {
if m == nil {
return fmt.Errorf("event manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("event manager %w", ErrSubSystemAlreadyStarted)
}
log.Debugf(log.EventMgr, "Event Manager started. SleepDelay: %v\n", EventSleepDelay.String())
m.shutdown = make(chan struct{})
go m.run()
return nil
}
// IsRunning safely checks whether the subsystem is running
func (m *eventManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Stop attempts to shutdown the subsystem
func (m *eventManager) Stop() error {
if m == nil {
return fmt.Errorf("event manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 1, 0) {
return fmt.Errorf("event manager %w", ErrSubSystemNotStarted)
}
close(m.shutdown)
return nil
}
func (m *eventManager) run() {
t := time.NewTicker(m.sleepDelay)
select {
case <-m.shutdown:
return
case <-t.C:
total, executed := m.getEventCounter()
if total > 0 && executed != total {
m.m.Lock()
for i := range m.events {
m.executeEvent(i)
}
m.m.Unlock()
}
}
}
func (m *eventManager) executeEvent(i int) {
if !m.events[i].Executed {
if m.verbose {
log.Debugf(log.EventMgr, "Events: Processing event %s.\n", m.events[i].String())
}
err := m.checkEventCondition(&m.events[i])
if err != nil {
msg := fmt.Sprintf(
"Events: ID: %d triggered on %s successfully [%v]\n", m.events[i].ID,
m.events[i].Exchange, m.events[i].String(),
)
log.Infoln(log.EventMgr, msg)
m.comms.PushEvent(base.Event{Type: "event", Message: msg})
m.events[i].Executed = true
} else if m.verbose {
log.Debugf(log.EventMgr, "%v", err)
}
}
}
// Add adds an event to the Events chain and returns an index/eventID
// and an error
func (m *eventManager) Add(exchange, item string, condition EventConditionParams, p currency.Pair, a asset.Item, action string) (int64, error) {
if m == nil {
return 0, fmt.Errorf("event manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return 0, fmt.Errorf("event manager %w", ErrSubSystemNotStarted)
}
err := m.isValidEvent(exchange, item, condition, action)
if err != nil {
return 0, err
}
evt := Event{
Exchange: exchange,
Item: item,
Condition: condition,
Pair: p,
Asset: a,
Action: action,
Executed: false,
}
m.m.Lock()
if len(m.events) > 0 {
evt.ID = int64(len(m.events) + 1)
}
m.events = append(m.events, evt)
m.m.Unlock()
return evt.ID, nil
}
// Remove deletes an event by its ID
func (m *eventManager) Remove(eventID int64) bool {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return false
}
m.m.Lock()
defer m.m.Unlock()
for i := range m.events {
if m.events[i].ID == eventID {
m.events = append(m.events[:i], m.events[i+1:]...)
return true
}
}
return false
}
// getEventCounter displays the amount of total events on the chain and the
// events that have been executed.
func (m *eventManager) getEventCounter() (total, executed int) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return 0, 0
}
m.m.Lock()
defer m.m.Unlock()
total = len(m.events)
for i := range m.events {
if m.events[i].Executed {
executed++
}
}
return total, executed
}
// checkEventCondition will check the event structure to see if there is a condition
// met
func (m *eventManager) checkEventCondition(e *Event) error {
if m == nil {
return fmt.Errorf("event manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("event manager %w", ErrSubSystemNotStarted)
}
if e == nil {
return errNilEvent
}
if e.Item == ItemPrice {
return e.processTicker()
}
return e.processOrderbook()
}
// isValidEvent checks the actions to be taken and returns an error if incorrect
func (m *eventManager) isValidEvent(exchange, item string, condition EventConditionParams, action string) error {
exchange = strings.ToUpper(exchange)
item = strings.ToUpper(item)
action = strings.ToUpper(action)
if !m.isValidExchange(exchange) {
return errExchangeDisabled
}
if !isValidItem(item) {
return errInvalidItem
}
if !isValidCondition(condition.Condition) {
return errInvalidCondition
}
if item == ItemPrice {
if condition.Price <= 0 {
return errInvalidCondition
}
}
if item == ItemOrderbook {
if condition.OrderbookAmount <= 0 {
return errInvalidCondition
}
}
if strings.Contains(action, ",") {
a := strings.Split(action, ",")
if a[0] != ActionSMSNotify {
return errInvalidAction
}
} else if action != ActionConsolePrint && action != ActionTest {
return errInvalidAction
}
return nil
}
// isValidExchange validates the exchange
func (m *eventManager) isValidExchange(exchangeName string) bool {
return m.exchangeManager.GetExchangeByName(exchangeName) != nil
}
// isValidCondition validates passed in condition
func isValidCondition(condition string) bool {
switch condition {
case ConditionGreaterThan, ConditionGreaterThanOrEqual, ConditionLessThan, ConditionLessThanOrEqual, ConditionIsEqual:
return true
}
return false
}
// isValidItem validates passed in Item
func isValidItem(item string) bool {
item = strings.ToUpper(item)
switch item {
case ItemPrice, ItemOrderbook:
return true
}
return false
}
// String turns the structure event into a string
func (e *Event) String() string {
return fmt.Sprintf(
"If the %s [%s] %s on %s meets the following %v then %s.", e.Pair.String(),
strings.ToUpper(e.Asset.String()), e.Item, e.Exchange, e.Condition, e.Action,
)
}
func (e *Event) processTicker() error {
t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset)
if err != nil {
return fmt.Errorf("failed to get ticker. Err: %w", err)
}
if t.Last == 0 {
return errTickerLastPriceZero
}
return e.shouldProcessEvent(t.Last, e.Condition.Price)
}
func (e *Event) shouldProcessEvent(actual, threshold float64) error {
switch e.Condition.Condition {
case ConditionGreaterThan:
if actual > threshold {
return nil
}
case ConditionGreaterThanOrEqual:
if actual >= threshold {
return nil
}
case ConditionLessThan:
if actual < threshold {
return nil
}
case ConditionLessThanOrEqual:
if actual <= threshold {
return nil
}
case ConditionIsEqual:
if actual == threshold {
return nil
}
}
return errors.New("does not meet conditions")
}
func (e *Event) processOrderbook() error {
ob, err := orderbook.Get(e.Exchange, e.Pair, e.Asset)
if err != nil {
return fmt.Errorf("events: Failed to get orderbook. Err: %w", err)
}
if !e.Condition.CheckBids && !e.Condition.CheckAsks {
return nil
}
if e.Condition.CheckBids {
for x := range ob.Bids {
subtotal := ob.Bids[x].Amount * ob.Bids[x].Price
err = e.shouldProcessEvent(subtotal, e.Condition.OrderbookAmount)
if err == nil {
log.Debugf(log.EventMgr, "Events: Bid Amount: %f Price: %v Subtotal: %v\n", ob.Bids[x].Amount, ob.Bids[x].Price, subtotal)
}
}
}
if e.Condition.CheckAsks {
for x := range ob.Asks {
subtotal := ob.Asks[x].Amount * ob.Asks[x].Price
err = e.shouldProcessEvent(subtotal, e.Condition.OrderbookAmount)
if err == nil {
log.Debugf(log.EventMgr, "Events: Ask Amount: %f Price: %v Subtotal: %v\n", ob.Asks[x].Amount, ob.Asks[x].Price, subtotal)
}
}
}
return err
}

52
engine/event_manager.md Normal file
View File

@@ -0,0 +1,52 @@
# GoCryptoTrader package Event_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/event_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This event_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Event_manager
+ The event manager subsystem is used to push events to communication systems such as Slack
+ The only configurable aspects of the event manager are the delays between receiving an event and pushing it and enabling verbose:
### connectionMonitor
| Config | Description | Example |
| ------ | ----------- | ------- |
| eventmanagerdelay | Sets the event managers sleep delay between event checking by a Golang `time.Duration` | `0` |
| verbose | Outputs debug messaging allowing for greater transparency for what the event manager is doing | `false` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,321 @@
package engine
import (
"errors"
"strings"
"sync/atomic"
"testing"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
func TestSetupEventManager(t *testing.T) {
t.Parallel()
_, err := setupEventManager(nil, nil, 0, false)
if !errors.Is(err, errNilComManager) {
t.Errorf("error '%v', expected '%v'", err, errNilComManager)
}
_, err = setupEventManager(&CommunicationManager{}, nil, 0, false)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false)
if !errors.Is(err, nil) {
t.Fatalf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Fatal("expected manager")
}
if m.sleepDelay == 0 {
t.Error("expected default set")
}
}
func TestEventManagerStart(t *testing.T) {
m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
m = nil
err = m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestEventManagerIsRunning(t *testing.T) {
t.Parallel()
m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.IsRunning() {
t.Error("expected true")
}
atomic.StoreInt32(&m.started, 0)
if m.IsRunning() {
t.Error("expected false")
}
m = nil
if m.IsRunning() {
t.Error("expected false")
}
}
func TestEventManagerStop(t *testing.T) {
t.Parallel()
m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
m = nil
err = m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestEventManagerAdd(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
m, err := setupEventManager(&CommunicationManager{}, em, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
_, err = m.Add("", "", EventConditionParams{}, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "")
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
_, err = m.Add("", "", EventConditionParams{}, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "")
if !errors.Is(err, errExchangeDisabled) {
t.Errorf("error '%v', expected '%v'", err, errExchangeDisabled)
}
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Error(err)
}
exch.SetDefaults()
em.Add(exch)
_, err = m.Add(testExchange, "", EventConditionParams{}, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "")
if !errors.Is(err, errInvalidItem) {
t.Errorf("error '%v', expected '%v'", err, errInvalidItem)
}
cond := EventConditionParams{
Condition: ConditionGreaterThan,
Price: 1337,
OrderbookAmount: 1337,
}
_, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "")
if !errors.Is(err, errInvalidAction) {
t.Errorf("error '%v', expected '%v'", err, errInvalidAction)
}
_, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, ActionTest)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
action := ActionSMSNotify + "," + ActionTest
_, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, action)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestEventManagerRemove(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
m, err := setupEventManager(&CommunicationManager{}, em, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.Remove(0) {
t.Error("expected false")
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.Remove(0) {
t.Error("expected false")
}
action := ActionSMSNotify + "," + ActionTest
cond := EventConditionParams{
Condition: ConditionGreaterThan,
Price: 1337,
OrderbookAmount: 1337,
}
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Error(err)
}
exch.SetDefaults()
em.Add(exch)
id, err := m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, action)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.Remove(id) {
t.Error("expected true")
}
}
func TestGetEventCounter(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
m, err := setupEventManager(&CommunicationManager{}, em, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
total, executed := m.getEventCounter()
if total != 0 && executed != 0 {
t.Error("expected 0")
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
total, executed = m.getEventCounter()
if total != 0 && executed != 0 {
t.Error("expected 0")
}
action := ActionSMSNotify + "," + ActionTest
cond := EventConditionParams{
Condition: ConditionGreaterThan,
Price: 1337,
OrderbookAmount: 1337,
}
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Error(err)
}
exch.SetDefaults()
em.Add(exch)
_, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, action)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
total, _ = m.getEventCounter()
if total == 0 {
t.Error("expected 1")
}
}
func TestCheckEventCondition(t *testing.T) {
em := SetupExchangeManager()
m, err := setupEventManager(&CommunicationManager{}, em, 0, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Lock()
err = m.checkEventCondition(nil)
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
m.m.Unlock()
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Lock()
err = m.checkEventCondition(nil)
if !errors.Is(err, errNilEvent) {
t.Errorf("error '%v', expected '%v'", err, errNilEvent)
}
m.m.Unlock()
action := ActionSMSNotify + "," + ActionTest
cond := EventConditionParams{
Condition: ConditionGreaterThan,
Price: 1337,
OrderbookAmount: 1337,
}
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Error(err)
}
exch.SetDefaults()
em.Add(exch)
_, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USD), asset.Spot, action)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Lock()
err = m.checkEventCondition(&m.events[0])
if err != nil && !strings.Contains(err.Error(), "no tickers for") {
t.Error(err)
} else if err == nil {
t.Error("expected error")
}
m.m.Unlock()
_, err = exch.FetchTicker(currency.NewPair(currency.BTC, currency.USD), asset.Spot)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Lock()
err = m.checkEventCondition(&m.events[0])
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Unlock()
m.events[0].Item = ItemOrderbook
m.events[0].Executed = false
m.events[0].Condition.CheckAsks = true
m.events[0].Condition.CheckBids = true
m.m.Lock()
err = m.checkEventCondition(&m.events[0])
if err != nil && !strings.Contains(err.Error(), "cannot find orderbook") {
t.Error(err)
} else if err == nil {
t.Error("expected error")
}
m.m.Unlock()
_, err = exch.FetchOrderbook(currency.NewPair(currency.BTC, currency.USD), asset.Spot)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Lock()
err = m.checkEventCondition(&m.events[0])
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.m.Unlock()
}

View File

@@ -0,0 +1,74 @@
package engine
import (
"errors"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Event const vars
const (
ItemPrice = "PRICE"
ItemOrderbook = "ORDERBOOK"
ConditionGreaterThan = ">"
ConditionGreaterThanOrEqual = ">="
ConditionLessThan = "<"
ConditionLessThanOrEqual = "<="
ConditionIsEqual = "=="
ActionSMSNotify = "SMS"
ActionConsolePrint = "CONSOLE_PRINT"
ActionTest = "ACTION_TEST"
defaultSleepDelay = time.Millisecond * 500
)
// vars related to events package
var (
EventSleepDelay = defaultSleepDelay
errInvalidItem = errors.New("invalid item")
errInvalidCondition = errors.New("invalid conditional option")
errInvalidAction = errors.New("invalid action")
errExchangeDisabled = errors.New("desired exchange is disabled")
errNilEvent = errors.New("nil event received")
errNilComManager = errors.New("nil communications manager received")
errTickerLastPriceZero = errors.New("ticker last price is 0")
)
// EventConditionParams holds the event condition variables
type EventConditionParams struct {
Condition string
Price float64
CheckBids bool
CheckAsks bool
OrderbookAmount float64
}
// Event struct holds the event variables
type Event struct {
ID int64
Exchange string
Item string
Condition EventConditionParams
Pair currency.Pair
Asset asset.Item
Action string
Executed bool
}
// eventManager holds communication manager data
type eventManager struct {
started int32
comms iCommsManager
events []Event
verbose bool
sleepDelay time.Duration
exchangeManager iExchangeManager
shutdown chan struct{}
m sync.Mutex
}

View File

@@ -1,347 +0,0 @@
package engine
import (
"errors"
"fmt"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/log"
)
// TO-DO MAKE THIS A SERVICE SUBSYSTEM
// Event const vars
const (
ItemPrice = "PRICE"
ItemOrderbook = "ORDERBOOK"
ConditionGreaterThan = ">"
ConditionGreaterThanOrEqual = ">="
ConditionLessThan = "<"
ConditionLessThanOrEqual = "<="
ConditionIsEqual = "=="
ActionSMSNotify = "SMS"
ActionConsolePrint = "CONSOLE_PRINT"
ActionTest = "ACTION_TEST"
defaultSleepDelay = time.Millisecond * 500
)
// vars related to events package
var (
errInvalidItem = errors.New("invalid item")
errInvalidCondition = errors.New("invalid conditional option")
errInvalidAction = errors.New("invalid action")
errExchangeDisabled = errors.New("desired exchange is disabled")
EventSleepDelay = defaultSleepDelay
)
// EventConditionParams holds the event condition variables
type EventConditionParams struct {
Condition string
Price float64
CheckBids bool
CheckBidsAndAsks bool
OrderbookAmount float64
}
// Event struct holds the event variables
type Event struct {
ID int64
Exchange string
Item string
Condition EventConditionParams
Pair currency.Pair
Asset asset.Item
Action string
Executed bool
}
// Events variable is a pointer array to the event structures that will be
// appended
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, p currency.Pair, a asset.Item, action string) (int64, error) {
err := IsValidEvent(exchange, item, condition, action)
if err != nil {
return 0, err
}
evt := &Event{}
if len(Events) == 0 {
evt.ID = 0
} else {
evt.ID = int64(len(Events) + 1)
}
evt.Exchange = exchange
evt.Item = item
evt.Condition = condition
evt.Pair = p
evt.Asset = a
evt.Action = action
evt.Executed = false
Events = append(Events, evt)
return evt.ID, nil
}
// Remove deletes and event by its ID
func Remove(eventID int64) bool {
for i := range Events {
if Events[i].ID == eventID {
Events = append(Events[:i], Events[i+1:]...)
return true
}
}
return false
}
// GetEventCounter displays the emount of total events on the chain and the
// events that have been executed.
func GetEventCounter() (total, executed int) {
total = len(Events)
for i := range Events {
if Events[i].Executed {
executed++
}
}
return total, executed
}
// ExecuteAction will execute the action pending on the chain
func (e *Event) ExecuteAction() bool {
if strings.Contains(e.Action, ",") {
action := strings.Split(e.Action, ",")
if action[0] == ActionSMSNotify {
if action[1] == "ALL" {
Bot.CommsManager.PushEvent(base.Event{
Type: "event",
Message: "Event triggered: " + e.String(),
})
}
}
} else {
log.Debugf(log.EventMgr, "Event triggered: %s\n", e.String())
}
return true
}
// String turns the structure event into a string
func (e *Event) String() string {
return fmt.Sprintf(
"If the %s [%s] %s on %s meets the following %v then %s.", e.Pair.String(),
strings.ToUpper(e.Asset.String()), e.Item, e.Exchange, e.Condition, e.Action,
)
}
func (e *Event) processTicker(verbose bool) bool {
t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset)
if err != nil {
if verbose {
log.Debugf(log.EventMgr, "Events: failed to get ticker. Err: %s\n", err)
}
return false
}
if t.Last == 0 {
if verbose {
log.Debugln(log.EventMgr, "Events: ticker last price is 0")
}
return false
}
return e.processCondition(t.Last, e.Condition.Price)
}
func (e *Event) processCondition(actual, threshold float64) bool {
switch e.Condition.Condition {
case ConditionGreaterThan:
if actual > threshold {
return e.ExecuteAction()
}
case ConditionGreaterThanOrEqual:
if actual >= threshold {
return e.ExecuteAction()
}
case ConditionLessThan:
if actual < threshold {
return e.ExecuteAction()
}
case ConditionLessThanOrEqual:
if actual <= threshold {
return e.ExecuteAction()
}
case ConditionIsEqual:
if actual == threshold {
return e.ExecuteAction()
}
}
return false
}
func (e *Event) processOrderbook(verbose bool) bool {
ob, err := orderbook.Get(e.Exchange, e.Pair, e.Asset)
if err != nil {
if verbose {
log.Debugf(log.EventMgr, "Events: Failed to get orderbook. Err: %s\n", err)
}
return false
}
success := false
if e.Condition.CheckBids || e.Condition.CheckBidsAndAsks {
for x := range ob.Bids {
subtotal := ob.Bids[x].Amount * ob.Bids[x].Price
result := e.processCondition(subtotal, e.Condition.OrderbookAmount)
if result {
success = true
log.Debugf(log.EventMgr, "Events: Bid Amount: %f Price: %v Subtotal: %v\n", ob.Bids[x].Amount, ob.Bids[x].Price, subtotal)
}
}
}
if !e.Condition.CheckBids || e.Condition.CheckBidsAndAsks {
for x := range ob.Asks {
subtotal := ob.Asks[x].Amount * ob.Asks[x].Price
result := e.processCondition(subtotal, e.Condition.OrderbookAmount)
if result {
success = true
log.Debugf(log.EventMgr, "Events: Ask Amount: %f Price: %v Subtotal: %v\n", ob.Asks[x].Amount, ob.Asks[x].Price, subtotal)
}
}
}
return success
}
// CheckEventCondition will check the event structure to see if there is a condition
// met
func (e *Event) CheckEventCondition(verbose bool) bool {
if e.Item == ItemPrice {
return e.processTicker(verbose)
}
return e.processOrderbook(verbose)
}
// IsValidEvent checks the actions to be taken and returns an error if incorrect
func IsValidEvent(exchange, item string, condition EventConditionParams, action string) error {
exchange = strings.ToUpper(exchange)
item = strings.ToUpper(item)
action = strings.ToUpper(action)
if !IsValidExchange(exchange) {
return errExchangeDisabled
}
if !IsValidItem(item) {
return errInvalidItem
}
if !IsValidCondition(condition.Condition) {
return errInvalidCondition
}
if item == ItemPrice {
if condition.Price <= 0 {
return errInvalidCondition
}
}
if item == ItemOrderbook {
if condition.OrderbookAmount <= 0 {
return errInvalidCondition
}
}
if strings.Contains(action, ",") {
a := strings.Split(action, ",")
if a[0] != ActionSMSNotify {
return errInvalidAction
}
} else if action != ActionConsolePrint && action != ActionTest {
return errInvalidAction
}
return nil
}
// EventManger is the overarching routine that will iterate through the Events
// chain
func EventManger(verbose bool, comManager *commsManager) {
log.Debugf(log.EventMgr, "EventManager started. SleepDelay: %v\n", EventSleepDelay.String())
for {
total, executed := GetEventCounter()
if total > 0 && executed != total {
for _, event := range Events {
if !event.Executed {
if verbose {
log.Debugf(log.EventMgr, "Events: Processing event %s.\n", event.String())
}
success := event.CheckEventCondition(verbose)
if success {
msg := fmt.Sprintf(
"Events: ID: %d triggered on %s successfully [%v]\n", event.ID,
event.Exchange, event.String(),
)
log.Infoln(log.EventMgr, msg)
comManager.PushEvent(base.Event{Type: "event", Message: msg})
event.Executed = true
}
}
}
}
time.Sleep(EventSleepDelay)
}
}
// IsValidExchange validates the exchange
func IsValidExchange(exchangeName string) bool {
cfg := config.GetConfig()
for x := range cfg.Exchanges {
if strings.EqualFold(cfg.Exchanges[x].Name, exchangeName) && cfg.Exchanges[x].Enabled {
return true
}
}
return false
}
// IsValidCondition validates passed in condition
func IsValidCondition(condition string) bool {
switch condition {
case ConditionGreaterThan, ConditionGreaterThanOrEqual, ConditionLessThan, ConditionLessThanOrEqual, ConditionIsEqual:
return true
}
return false
}
// IsValidAction validates passed in action
func IsValidAction(action string) bool {
action = strings.ToUpper(action)
switch action {
case ActionSMSNotify, ActionConsolePrint, ActionTest:
return true
}
return false
}
// IsValidItem validates passed in Item
func IsValidItem(item string) bool {
item = strings.ToUpper(item)
switch item {
case ItemPrice, ItemOrderbook:
return true
}
return false
}

View File

@@ -1,336 +0,0 @@
package engine
import (
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
const (
testExchange = "Bitstamp"
)
func addValidEvent() (int64, error) {
return Add(testExchange,
ItemPrice,
EventConditionParams{Condition: ConditionGreaterThan, Price: 1},
currency.NewPair(currency.BTC, currency.USD),
asset.Spot,
"SMS,test")
}
func TestAdd(t *testing.T) {
bot := CreateTestBot(t)
if config.Cfg.Name == "" && bot != nil {
config.Cfg = *bot.Config
}
_, err := Add("", "", EventConditionParams{}, currency.Pair{}, "", "")
if err == nil {
t.Error("should err on invalid params")
}
_, err = addValidEvent()
if err != nil {
t.Error("unexpected result", err)
}
_, err = addValidEvent()
if err != nil {
t.Error("unexpected result", err)
}
if len(Events) != 2 {
t.Error("2 events should be stored")
}
}
func TestRemove(t *testing.T) {
bot := CreateTestBot(t)
if config.Cfg.Name == "" && bot != nil {
config.Cfg = *bot.Config
}
id, err := addValidEvent()
if err != nil {
t.Error("unexpected result", err)
}
if s := Remove(id); !s {
t.Error("unexpected result")
}
if s := Remove(id); s {
t.Error("unexpected result")
}
}
func TestGetEventCounter(t *testing.T) {
bot := CreateTestBot(t)
if config.Cfg.Name == "" && bot != nil {
config.Cfg = *bot.Config
}
_, err := addValidEvent()
if err != nil {
t.Error("unexpected result", err)
}
n, e := GetEventCounter()
if n == 0 || e > 0 {
t.Error("unexpected result")
}
Events[0].Executed = true
n, e = GetEventCounter()
if n == 0 || e == 0 {
t.Error("unexpected result")
}
}
func TestExecuteAction(t *testing.T) {
t.Parallel()
bot := CreateTestBot(t)
if Bot == nil {
Bot = bot
}
if config.Cfg.Name == "" && bot != nil {
config.Cfg = *bot.Config
}
var e Event
if r := e.ExecuteAction(); !r {
t.Error("unexpected result")
}
e.Action = "SMS,test"
if r := e.ExecuteAction(); !r {
t.Error("unexpected result")
}
e.Action = "SMS,ALL"
if r := e.ExecuteAction(); !r {
t.Error("unexpected result")
}
}
func TestString(t *testing.T) {
t.Parallel()
e := Event{
Exchange: testExchange,
Item: ItemPrice,
Condition: EventConditionParams{
Condition: ConditionGreaterThan,
Price: 1,
},
Pair: currency.NewPair(currency.BTC, currency.USD),
Asset: asset.Spot,
Action: "SMS,ALL",
}
if r := e.String(); r != "If the BTCUSD [SPOT] PRICE on Bitstamp meets the following {> 1 false false 0} then SMS,ALL." {
t.Error("unexpected result")
}
}
func TestProcessTicker(t *testing.T) {
e := Event{
Exchange: testExchange,
Pair: currency.NewPair(currency.BTC, currency.USD),
Asset: asset.Spot,
Condition: EventConditionParams{
Condition: ConditionGreaterThan,
Price: 1,
},
}
// now populate it with a 0 entry
tick := ticker.Price{
Pair: currency.NewPair(currency.BTC, currency.USD),
ExchangeName: e.Exchange,
AssetType: e.Asset,
}
if err := ticker.ProcessTicker(&tick); err != nil {
t.Fatal("unexpected result:", err)
}
if r := e.processTicker(false); r {
t.Error("unexpected result")
}
// now populate it with a number > 0
tick.Last = 1337
if err := ticker.ProcessTicker(&tick); err != nil {
t.Fatal("unexpected result:", err)
}
if r := e.processTicker(false); !r {
t.Error("unexpected result")
}
}
func TestProcessCondition(t *testing.T) {
t.Parallel()
var e Event
tester := []struct {
Condition string
Actual float64
Threshold float64
ExpectedResult bool
}{
{ConditionGreaterThan, 1, 2, false},
{ConditionGreaterThan, 2, 1, true},
{ConditionGreaterThanOrEqual, 1, 2, false},
{ConditionGreaterThanOrEqual, 2, 1, true},
{ConditionIsEqual, 1, 1, true},
{ConditionIsEqual, 1, 2, false},
{ConditionLessThan, 1, 2, true},
{ConditionLessThan, 2, 1, false},
{ConditionLessThanOrEqual, 1, 2, true},
{ConditionLessThanOrEqual, 2, 1, false},
}
for x := range tester {
e.Condition.Condition = tester[x].Condition
if r := e.processCondition(tester[x].Actual, tester[x].Threshold); r != tester[x].ExpectedResult {
t.Error("unexpected result")
}
}
}
func TestProcessOrderbook(t *testing.T) {
e := Event{
Exchange: testExchange,
Pair: currency.NewPair(currency.BTC, currency.USD),
Asset: asset.Spot,
Condition: EventConditionParams{
Condition: ConditionGreaterThan,
CheckBidsAndAsks: true,
OrderbookAmount: 100,
},
}
// now populate it with a 0 entry
o := orderbook.Base{
Pair: currency.NewPair(currency.BTC, currency.USD),
Bids: []orderbook.Item{{Amount: 24, Price: 23}},
Asks: []orderbook.Item{{Amount: 24, Price: 23}},
Exchange: e.Exchange,
Asset: e.Asset,
}
if err := o.Process(); err != nil {
t.Fatal("unexpected result:", err)
}
if r := e.processOrderbook(false); !r {
t.Error("unexpected result")
}
}
func TestCheckEventCondition(t *testing.T) {
t.Parallel()
if Bot == nil {
Bot = new(Engine)
}
e := Event{
Item: ItemPrice,
}
if r := e.CheckEventCondition(false); r {
t.Error("unexpected result")
}
e.Item = ItemOrderbook
if r := e.CheckEventCondition(false); r {
t.Error("unexpected result")
}
}
func TestIsValidEvent(t *testing.T) {
bot := CreateTestBot(t)
if config.Cfg.Name == "" && bot != nil {
config.Cfg = *bot.Config
}
// invalid exchange name
if err := IsValidEvent("meow", "", EventConditionParams{}, ""); err != errExchangeDisabled {
t.Error("unexpected result:", err)
}
// invalid item
if err := IsValidEvent(testExchange, "", EventConditionParams{}, ""); err != errInvalidItem {
t.Error("unexpected result:", err)
}
// invalid condition
if err := IsValidEvent(testExchange, ItemPrice, EventConditionParams{}, ""); err != errInvalidCondition {
t.Error("unexpected result:", err)
}
// valid condition but empty price which will still throw an errInvalidCondition
c := EventConditionParams{
Condition: ConditionGreaterThan,
}
if err := IsValidEvent(testExchange, ItemPrice, c, ""); err != errInvalidCondition {
t.Error("unexpected result:", err)
}
// valid condition but empty orderbook amount will still still throw an errInvalidCondition
if err := IsValidEvent(testExchange, ItemOrderbook, c, ""); err != errInvalidCondition {
t.Error("unexpected result:", err)
}
// test action splitting, but invalid
c.OrderbookAmount = 1337
if err := IsValidEvent(testExchange, ItemOrderbook, c, "a,meow"); err != errInvalidAction {
t.Error("unexpected result:", err)
}
// check for invalid action without splitting
if err := IsValidEvent(testExchange, ItemOrderbook, c, "hi"); err != errInvalidAction {
t.Error("unexpected result:", err)
}
// valid event
if err := IsValidEvent(testExchange, ItemOrderbook, c, "SMS,test"); err != nil {
t.Error("unexpected result:", err)
}
}
func TestIsValidExchange(t *testing.T) {
t.Parallel()
if s := IsValidExchange("invalidexchangerino"); s {
t.Error("unexpected result")
}
CreateTestBot(t)
if s := IsValidExchange(testExchange); !s {
t.Error("unexpected result")
}
}
func TestIsValidCondition(t *testing.T) {
t.Parallel()
if s := IsValidCondition("invalidconditionerino"); s {
t.Error("unexpected result")
}
if s := IsValidCondition(ConditionGreaterThan); !s {
t.Error("unexpected result")
}
}
func TestIsValidAction(t *testing.T) {
t.Parallel()
if s := IsValidAction("invalidactionerino"); s {
t.Error("unexpected result")
}
if s := IsValidAction(ActionSMSNotify); !s {
t.Error("unexpected result")
}
}
func TestIsValidItem(t *testing.T) {
t.Parallel()
if s := IsValidItem("invaliditemerino"); s {
t.Error("unexpected result")
}
if s := IsValidItem(ItemPrice); !s {
t.Error("unexpected result")
}
}

View File

@@ -1,406 +0,0 @@
package engine
import (
"errors"
"strings"
"sync"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitflyer"
"github.com/thrasher-corp/gocryptotrader/exchanges/bithumb"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitmex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp"
"github.com/thrasher-corp/gocryptotrader/exchanges/bittrex"
"github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets"
"github.com/thrasher-corp/gocryptotrader/exchanges/btse"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbene"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinut"
"github.com/thrasher-corp/gocryptotrader/exchanges/exmo"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
"github.com/thrasher-corp/gocryptotrader/exchanges/gateio"
"github.com/thrasher-corp/gocryptotrader/exchanges/gemini"
"github.com/thrasher-corp/gocryptotrader/exchanges/hitbtc"
"github.com/thrasher-corp/gocryptotrader/exchanges/huobi"
"github.com/thrasher-corp/gocryptotrader/exchanges/itbit"
"github.com/thrasher-corp/gocryptotrader/exchanges/kraken"
"github.com/thrasher-corp/gocryptotrader/exchanges/lakebtc"
"github.com/thrasher-corp/gocryptotrader/exchanges/lbank"
"github.com/thrasher-corp/gocryptotrader/exchanges/localbitcoins"
"github.com/thrasher-corp/gocryptotrader/exchanges/okcoin"
"github.com/thrasher-corp/gocryptotrader/exchanges/okex"
"github.com/thrasher-corp/gocryptotrader/exchanges/poloniex"
"github.com/thrasher-corp/gocryptotrader/exchanges/yobit"
"github.com/thrasher-corp/gocryptotrader/exchanges/zb"
"github.com/thrasher-corp/gocryptotrader/log"
)
// vars related to exchange functions
var (
ErrNoExchangesLoaded = errors.New("no exchanges have been loaded")
ErrExchangeNotFound = errors.New("exchange not found")
ErrExchangeAlreadyLoaded = errors.New("exchange already loaded")
ErrExchangeFailedToLoad = errors.New("exchange failed to load")
)
type exchangeManager struct {
m sync.Mutex
exchanges map[string]exchange.IBotExchange
}
func (bot *Engine) dryrunParamInteraction(param string) {
if !bot.Settings.CheckParamInteraction {
return
}
if !bot.Settings.EnableDryRun {
log.Warnf(log.Global,
"Command line argument '-%s' induces dry run mode."+
" Set -dryrun=false if you wish to override this.",
param)
bot.Settings.EnableDryRun = true
}
}
func (e *exchangeManager) add(exch exchange.IBotExchange) {
e.m.Lock()
if e.exchanges == nil {
e.exchanges = make(map[string]exchange.IBotExchange)
}
e.exchanges[strings.ToLower(exch.GetName())] = exch
e.m.Unlock()
}
func (e *exchangeManager) getExchanges() []exchange.IBotExchange {
if e.Len() == 0 {
return nil
}
e.m.Lock()
defer e.m.Unlock()
var exchs []exchange.IBotExchange
for x := range e.exchanges {
exchs = append(exchs, e.exchanges[x])
}
return exchs
}
func (e *exchangeManager) removeExchange(exchName string) error {
if e.Len() == 0 {
return ErrNoExchangesLoaded
}
exch := e.getExchangeByName(exchName)
if exch == nil {
return ErrExchangeNotFound
}
e.m.Lock()
defer e.m.Unlock()
delete(e.exchanges, strings.ToLower(exchName))
log.Infof(log.ExchangeSys, "%s exchange unloaded successfully.\n", exchName)
return nil
}
func (e *exchangeManager) getExchangeByName(exchangeName string) exchange.IBotExchange {
if e.Len() == 0 {
return nil
}
e.m.Lock()
defer e.m.Unlock()
exch, ok := e.exchanges[strings.ToLower(exchangeName)]
if !ok {
return nil
}
return exch
}
func (e *exchangeManager) Len() int {
e.m.Lock()
defer e.m.Unlock()
return len(e.exchanges)
}
// GetExchangeByName returns an exchange given an exchange name
func (bot *Engine) GetExchangeByName(exchName string) exchange.IBotExchange {
return bot.exchangeManager.getExchangeByName(exchName)
}
// UnloadExchange unloads an exchange by name
func (bot *Engine) UnloadExchange(exchName string) error {
exchCfg, err := bot.Config.GetExchangeConfig(exchName)
if err != nil {
return err
}
err = bot.exchangeManager.removeExchange(exchName)
if err != nil {
return err
}
exchCfg.Enabled = false
return nil
}
// GetExchanges retrieves the loaded exchanges
func (bot *Engine) GetExchanges() []exchange.IBotExchange {
return bot.exchangeManager.getExchanges()
}
// LoadExchange loads an exchange by name
func (bot *Engine) LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error {
nameLower := strings.ToLower(name)
var exch exchange.IBotExchange
if bot.exchangeManager.getExchangeByName(nameLower) != nil {
return ErrExchangeAlreadyLoaded
}
switch nameLower {
case "binance":
exch = new(binance.Binance)
case "bitfinex":
exch = new(bitfinex.Bitfinex)
case "bitflyer":
exch = new(bitflyer.Bitflyer)
case "bithumb":
exch = new(bithumb.Bithumb)
case "bitmex":
exch = new(bitmex.Bitmex)
case "bitstamp":
exch = new(bitstamp.Bitstamp)
case "bittrex":
exch = new(bittrex.Bittrex)
case "btc markets":
exch = new(btcmarkets.BTCMarkets)
case "btse":
exch = new(btse.BTSE)
case "coinbene":
exch = new(coinbene.Coinbene)
case "coinut":
exch = new(coinut.COINUT)
case "exmo":
exch = new(exmo.EXMO)
case "coinbasepro":
exch = new(coinbasepro.CoinbasePro)
case "ftx":
exch = new(ftx.FTX)
case "gateio":
exch = new(gateio.Gateio)
case "gemini":
exch = new(gemini.Gemini)
case "hitbtc":
exch = new(hitbtc.HitBTC)
case "huobi":
exch = new(huobi.HUOBI)
case "itbit":
exch = new(itbit.ItBit)
case "kraken":
exch = new(kraken.Kraken)
case "lakebtc":
exch = new(lakebtc.LakeBTC)
case "lbank":
exch = new(lbank.Lbank)
case "localbitcoins":
exch = new(localbitcoins.LocalBitcoins)
case "okcoin international":
exch = new(okcoin.OKCoin)
case "okex":
exch = new(okex.OKEX)
case "poloniex":
exch = new(poloniex.Poloniex)
case "yobit":
exch = new(yobit.Yobit)
case "zb":
exch = new(zb.ZB)
default:
return ErrExchangeNotFound
}
if exch == nil {
return ErrExchangeFailedToLoad
}
var localWG sync.WaitGroup
localWG.Add(1)
go func() {
exch.SetDefaults()
localWG.Done()
}()
exchCfg, err := bot.Config.GetExchangeConfig(name)
if err != nil {
return err
}
if bot.Settings.EnableAllPairs &&
exchCfg.CurrencyPairs != nil {
assets := exchCfg.CurrencyPairs.GetAssetTypes()
for x := range assets {
var pairs currency.Pairs
pairs, err = exchCfg.CurrencyPairs.GetPairs(assets[x], false)
if err != nil {
return err
}
exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true)
}
}
if bot.Settings.EnableExchangeVerbose {
exchCfg.Verbose = true
}
if exchCfg.Features != nil {
if bot.Settings.EnableExchangeWebsocketSupport &&
exchCfg.Features.Supports.Websocket {
exchCfg.Features.Enabled.Websocket = true
}
if bot.Settings.EnableExchangeAutoPairUpdates &&
exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates {
exchCfg.Features.Enabled.AutoPairUpdates = true
}
if bot.Settings.DisableExchangeAutoPairUpdates {
if exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates {
exchCfg.Features.Enabled.AutoPairUpdates = false
}
}
}
if bot.Settings.HTTPUserAgent != "" {
exchCfg.HTTPUserAgent = bot.Settings.HTTPUserAgent
}
if bot.Settings.HTTPProxy != "" {
exchCfg.ProxyAddress = bot.Settings.HTTPProxy
}
if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout {
exchCfg.HTTPTimeout = bot.Settings.HTTPTimeout
}
if bot.Settings.EnableExchangeHTTPDebugging {
exchCfg.HTTPDebugging = bot.Settings.EnableExchangeHTTPDebugging
}
localWG.Wait()
if !bot.Settings.EnableExchangeHTTPRateLimiter {
log.Warnf(log.ExchangeSys,
"Loaded exchange %s rate limiting has been turned off.\n",
exch.GetName(),
)
err = exch.DisableRateLimiter()
if err != nil {
log.Errorf(log.ExchangeSys,
"Loaded exchange %s rate limiting cannot be turned off: %s.\n",
exch.GetName(),
err,
)
}
}
exchCfg.Enabled = true
err = exch.Setup(exchCfg)
if err != nil {
exchCfg.Enabled = false
return err
}
bot.exchangeManager.add(exch)
base := exch.GetBase()
if base.API.AuthenticatedSupport ||
base.API.AuthenticatedWebsocketSupport {
assetTypes := base.GetAssetTypes()
var useAsset asset.Item
for a := range assetTypes {
err = base.CurrencyPairs.IsAssetEnabled(assetTypes[a])
if err != nil {
continue
}
useAsset = assetTypes[a]
break
}
err = exch.ValidateCredentials(useAsset)
if err != nil {
log.Warnf(log.ExchangeSys,
"%s: Cannot validate credentials, authenticated support has been disabled, Error: %s\n",
base.Name,
err)
base.API.AuthenticatedSupport = false
base.API.AuthenticatedWebsocketSupport = false
exchCfg.API.AuthenticatedSupport = false
exchCfg.API.AuthenticatedWebsocketSupport = false
}
}
if useWG {
exch.Start(wg)
} else {
tempWG := sync.WaitGroup{}
exch.Start(&tempWG)
tempWG.Wait()
}
return nil
}
// SetupExchanges sets up the exchanges used by the Bot
func (bot *Engine) SetupExchanges() error {
var wg sync.WaitGroup
configs := bot.Config.GetAllExchangeConfigs()
if bot.Settings.EnableAllPairs {
bot.dryrunParamInteraction("enableallpairs")
}
if bot.Settings.EnableAllExchanges {
bot.dryrunParamInteraction("enableallexchanges")
}
if bot.Settings.EnableExchangeVerbose {
bot.dryrunParamInteraction("exchangeverbose")
}
if bot.Settings.EnableExchangeWebsocketSupport {
bot.dryrunParamInteraction("exchangewebsocketsupport")
}
if bot.Settings.EnableExchangeAutoPairUpdates {
bot.dryrunParamInteraction("exchangeautopairupdates")
}
if bot.Settings.DisableExchangeAutoPairUpdates {
bot.dryrunParamInteraction("exchangedisableautopairupdates")
}
if bot.Settings.HTTPUserAgent != "" {
bot.dryrunParamInteraction("httpuseragent")
}
if bot.Settings.HTTPProxy != "" {
bot.dryrunParamInteraction("httpproxy")
}
if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout {
bot.dryrunParamInteraction("httptimeout")
}
if bot.Settings.EnableExchangeHTTPDebugging {
bot.dryrunParamInteraction("exchangehttpdebugging")
}
for x := range configs {
if !configs[x].Enabled && !bot.Settings.EnableAllExchanges {
log.Debugf(log.ExchangeSys, "%s: Exchange support: Disabled\n", configs[x].Name)
continue
}
wg.Add(1)
cfg := configs[x]
go func(currCfg config.ExchangeConfig) {
defer wg.Done()
err := bot.LoadExchange(currCfg.Name, true, &wg)
if err != nil {
log.Errorf(log.ExchangeSys, "LoadExchange %s failed: %s\n", currCfg.Name, err)
return
}
log.Debugf(log.ExchangeSys,
"%s: Exchange support: Enabled (Authenticated API support: %s - Verbose mode: %s).\n",
currCfg.Name,
common.IsEnabled(currCfg.API.AuthenticatedSupport),
common.IsEnabled(currCfg.Verbose),
)
}(cfg)
}
wg.Wait()
if len(bot.exchangeManager.exchanges) == 0 {
return errors.New("no exchanges are loaded")
}
return nil
}

193
engine/exchange_manager.go Normal file
View File

@@ -0,0 +1,193 @@
package engine
import (
"errors"
"fmt"
"strings"
"sync"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitflyer"
"github.com/thrasher-corp/gocryptotrader/exchanges/bithumb"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitmex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp"
"github.com/thrasher-corp/gocryptotrader/exchanges/bittrex"
"github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets"
"github.com/thrasher-corp/gocryptotrader/exchanges/btse"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbene"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinut"
"github.com/thrasher-corp/gocryptotrader/exchanges/exmo"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
"github.com/thrasher-corp/gocryptotrader/exchanges/gateio"
"github.com/thrasher-corp/gocryptotrader/exchanges/gemini"
"github.com/thrasher-corp/gocryptotrader/exchanges/hitbtc"
"github.com/thrasher-corp/gocryptotrader/exchanges/huobi"
"github.com/thrasher-corp/gocryptotrader/exchanges/itbit"
"github.com/thrasher-corp/gocryptotrader/exchanges/kraken"
"github.com/thrasher-corp/gocryptotrader/exchanges/lakebtc"
"github.com/thrasher-corp/gocryptotrader/exchanges/lbank"
"github.com/thrasher-corp/gocryptotrader/exchanges/localbitcoins"
"github.com/thrasher-corp/gocryptotrader/exchanges/okcoin"
"github.com/thrasher-corp/gocryptotrader/exchanges/okex"
"github.com/thrasher-corp/gocryptotrader/exchanges/poloniex"
"github.com/thrasher-corp/gocryptotrader/exchanges/yobit"
"github.com/thrasher-corp/gocryptotrader/exchanges/zb"
"github.com/thrasher-corp/gocryptotrader/log"
)
// vars related to exchange functions
var (
ErrNoExchangesLoaded = errors.New("no exchanges have been loaded")
ErrExchangeNotFound = errors.New("exchange not found")
ErrExchangeAlreadyLoaded = errors.New("exchange already loaded")
ErrExchangeFailedToLoad = errors.New("exchange failed to load")
)
// ExchangeManager manages what exchanges are loaded
type ExchangeManager struct {
m sync.Mutex
exchanges map[string]exchange.IBotExchange
}
// SetupExchangeManager creates a new exchange manager
func SetupExchangeManager() *ExchangeManager {
return &ExchangeManager{
exchanges: make(map[string]exchange.IBotExchange),
}
}
// Add adds or replaces an exchange
func (m *ExchangeManager) Add(exch exchange.IBotExchange) {
if exch == nil {
return
}
m.m.Lock()
m.exchanges[strings.ToLower(exch.GetName())] = exch
m.m.Unlock()
}
// GetExchanges returns all stored exchanges
func (m *ExchangeManager) GetExchanges() []exchange.IBotExchange {
m.m.Lock()
defer m.m.Unlock()
var exchs []exchange.IBotExchange
for _, x := range m.exchanges {
exchs = append(exchs, x)
}
return exchs
}
// RemoveExchange removes an exchange from the manager
func (m *ExchangeManager) RemoveExchange(exchName string) error {
if m.Len() == 0 {
return ErrNoExchangesLoaded
}
exch := m.GetExchangeByName(exchName)
if exch == nil {
return ErrExchangeNotFound
}
m.m.Lock()
defer m.m.Unlock()
delete(m.exchanges, strings.ToLower(exchName))
log.Infof(log.ExchangeSys, "%s exchange unloaded successfully.\n", exchName)
return nil
}
// GetExchangeByName returns an exchange by its name if it exists
func (m *ExchangeManager) GetExchangeByName(exchangeName string) exchange.IBotExchange {
if m == nil {
return nil
}
m.m.Lock()
defer m.m.Unlock()
exch, ok := m.exchanges[strings.ToLower(exchangeName)]
if !ok {
return nil
}
return exch
}
// Len says how many exchanges are loaded
func (m *ExchangeManager) Len() int {
m.m.Lock()
defer m.m.Unlock()
return len(m.exchanges)
}
// NewExchangeByName helps create a new exchange to be loaded
func (m *ExchangeManager) NewExchangeByName(name string) (exchange.IBotExchange, error) {
if m == nil {
return nil, fmt.Errorf("exchange manager %w", ErrNilSubsystem)
}
nameLower := strings.ToLower(name)
if m.GetExchangeByName(nameLower) != nil {
return nil, fmt.Errorf("%s %w", name, ErrExchangeAlreadyLoaded)
}
var exch exchange.IBotExchange
switch nameLower {
case "binance":
exch = new(binance.Binance)
case "bitfinex":
exch = new(bitfinex.Bitfinex)
case "bitflyer":
exch = new(bitflyer.Bitflyer)
case "bithumb":
exch = new(bithumb.Bithumb)
case "bitmex":
exch = new(bitmex.Bitmex)
case "bitstamp":
exch = new(bitstamp.Bitstamp)
case "bittrex":
exch = new(bittrex.Bittrex)
case "btc markets":
exch = new(btcmarkets.BTCMarkets)
case "btse":
exch = new(btse.BTSE)
case "coinbene":
exch = new(coinbene.Coinbene)
case "coinut":
exch = new(coinut.COINUT)
case "exmo":
exch = new(exmo.EXMO)
case "coinbasepro":
exch = new(coinbasepro.CoinbasePro)
case "ftx":
exch = new(ftx.FTX)
case "gateio":
exch = new(gateio.Gateio)
case "gemini":
exch = new(gemini.Gemini)
case "hitbtc":
exch = new(hitbtc.HitBTC)
case "huobi":
exch = new(huobi.HUOBI)
case "itbit":
exch = new(itbit.ItBit)
case "kraken":
exch = new(kraken.Kraken)
case "lakebtc":
exch = new(lakebtc.LakeBTC)
case "lbank":
exch = new(lbank.Lbank)
case "localbitcoins":
exch = new(localbitcoins.LocalBitcoins)
case "okcoin international":
exch = new(okcoin.OKCoin)
case "okex":
exch = new(okex.OKEX)
case "poloniex":
exch = new(poloniex.Poloniex)
case "yobit":
exch = new(yobit.Yobit)
case "zb":
exch = new(zb.ZB)
default:
return nil, fmt.Errorf("%s, %w", nameLower, ErrExchangeNotFound)
}
return exch, nil
}

View File

@@ -0,0 +1,45 @@
# GoCryptoTrader package Exchange_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/exchange_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This exchange_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Exchange_manager
+ The exchange manager subsystem is used load and store exchanges so that the engine Bot can use them to track orderbooks, submit orders etc etc
+ The exchange manager itself is not customisable, it is always enabled.
+ The exchange manager by default will load all exchanges that are enabled in your config, however, it will also load exchanges by request via GRPC commands
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,81 @@
package engine
import (
"strings"
"testing"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
)
func TestSetupExchangeManager(t *testing.T) {
t.Parallel()
m := SetupExchangeManager()
if m == nil {
t.Fatalf("unexpected response")
}
if m.exchanges == nil {
t.Error("unexpected response")
}
}
func TestExchangeManagerAdd(t *testing.T) {
t.Parallel()
m := SetupExchangeManager()
b := new(bitfinex.Bitfinex)
b.SetDefaults()
m.Add(b)
if exch := m.GetExchanges(); exch[0].GetName() != "Bitfinex" {
t.Error("unexpected exchange name")
}
}
func TestExchangeManagerGetExchanges(t *testing.T) {
t.Parallel()
m := SetupExchangeManager()
if exchanges := m.GetExchanges(); exchanges != nil {
t.Error("unexpected value")
}
b := new(bitfinex.Bitfinex)
b.SetDefaults()
m.Add(b)
if exch := m.GetExchanges(); exch[0].GetName() != "Bitfinex" {
t.Error("unexpected exchange name")
}
}
func TestExchangeManagerRemoveExchange(t *testing.T) {
t.Parallel()
m := SetupExchangeManager()
if err := m.RemoveExchange("Bitfinex"); err != ErrNoExchangesLoaded {
t.Error("no exchanges should be loaded")
}
b := new(bitfinex.Bitfinex)
b.SetDefaults()
m.Add(b)
if err := m.RemoveExchange("Bitstamp"); err != ErrExchangeNotFound {
t.Error("Bitstamp exchange should return an error")
}
if err := m.RemoveExchange("BiTFiNeX"); err != nil {
t.Error("exchange should have been removed")
}
if m.Len() != 0 {
t.Error("exchange manager len should be 0")
}
}
func TestNewExchangeByName(t *testing.T) {
m := SetupExchangeManager()
exchanges := []string{"binance", "bitfinex", "bitflyer", "bithumb", "bitmex", "bitstamp", "bittrex", "btc markets", "btse", "coinbene", "coinut", "exmo", "coinbasepro", "ftx", "gateio", "gemini", "hitbtc", "huobi", "itbit", "kraken", "lakebtc", "lbank", "localbitcoins", "okcoin international", "okex", "poloniex", "yobit", "zb", "fake"}
for i := range exchanges {
exch, err := m.NewExchangeByName(exchanges[i])
if err != nil && exchanges[i] != "fake" {
t.Error(err)
}
if err == nil {
exch.SetDefaults()
if !strings.EqualFold(exch.GetName(), exchanges[i]) {
t.Error("did not load expected exchange")
}
}
}
}

View File

@@ -1,170 +0,0 @@
package engine
import (
"testing"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
)
func TestExchangeManagerAdd(t *testing.T) {
t.Parallel()
var e exchangeManager
b := new(bitfinex.Bitfinex)
b.SetDefaults()
e.add(b)
if exch := e.getExchanges(); exch[0].GetName() != "Bitfinex" {
t.Error("unexpected exchange name")
}
}
func TestExchangeManagerGetExchanges(t *testing.T) {
t.Parallel()
var e exchangeManager
if exchanges := e.getExchanges(); exchanges != nil {
t.Error("unexpected value")
}
b := new(bitfinex.Bitfinex)
b.SetDefaults()
e.add(b)
if exch := e.getExchanges(); exch[0].GetName() != "Bitfinex" {
t.Error("unexpected exchange name")
}
}
func TestExchangeManagerRemoveExchange(t *testing.T) {
t.Parallel()
var e exchangeManager
if err := e.removeExchange("Bitfinex"); err != ErrNoExchangesLoaded {
t.Error("no exchanges should be loaded")
}
b := new(bitfinex.Bitfinex)
b.SetDefaults()
e.add(b)
if err := e.removeExchange(testExchange); err != ErrExchangeNotFound {
t.Error("Bitstamp exchange should return an error")
}
if err := e.removeExchange("BiTFiNeX"); err != nil {
t.Error("exchange should have been removed")
}
if e.Len() != 0 {
t.Error("exchange manager len should be 0")
}
}
func TestCheckExchangeExists(t *testing.T) {
e := CreateTestBot(t)
if e.GetExchangeByName(testExchange) == nil {
t.Errorf("TestGetExchangeExists: Unable to find exchange")
}
if e.GetExchangeByName("Asdsad") != nil {
t.Errorf("TestGetExchangeExists: Non-existent exchange found")
}
}
func TestGetExchangeByName(t *testing.T) {
e := CreateTestBot(t)
exch := e.GetExchangeByName(testExchange)
if exch == nil {
t.Errorf("TestGetExchangeByName: Failed to get exchange")
}
if !exch.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
exch.SetEnabled(false)
bfx := e.GetExchangeByName(testExchange)
if bfx.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
if exch.GetName() != testExchange {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
exch = e.GetExchangeByName("Asdasd")
if exch != nil {
t.Errorf("TestGetExchangeByName: Non-existent exchange found")
}
}
func TestUnloadExchange(t *testing.T) {
e := CreateTestBot(t)
err := e.UnloadExchange("asdf")
if err == nil || err.Error() != "exchange asdf not found" {
t.Errorf("TestUnloadExchange: Incorrect result: %s",
err)
}
err = e.UnloadExchange(testExchange)
if err != nil {
t.Errorf("TestUnloadExchange: Failed to get exchange. %s",
err)
}
err = e.UnloadExchange(fakePassExchange)
if err != nil {
t.Errorf("TestUnloadExchange: Failed to unload exchange. %s",
err)
}
err = e.UnloadExchange(testExchange)
if err != ErrNoExchangesLoaded {
t.Errorf("TestUnloadExchange: Incorrect result: %s",
err)
}
}
func TestDryRunParamInteraction(t *testing.T) {
bot := CreateTestBot(t)
// Simulate overiding default settings and ensure that enabling exchange
// verbose mode will be set on Bitfinex
var err error
if err = bot.UnloadExchange(testExchange); err != nil {
t.Error(err)
}
bot.Settings.CheckParamInteraction = false
bot.Settings.EnableExchangeVerbose = false
if err = bot.LoadExchange(testExchange, false, nil); err != nil {
t.Error(err)
}
exchCfg, err := bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if exchCfg.Verbose {
t.Error("verbose should have been disabled")
}
if err = bot.UnloadExchange(testExchange); err != nil {
t.Error(err)
}
// Now set dryrun mode to true,
// enable exchange verbose mode and verify that verbose mode
// will be set on Bitfinex
bot.Settings.EnableDryRun = true
bot.Settings.CheckParamInteraction = true
bot.Settings.EnableExchangeVerbose = true
if err = bot.LoadExchange(testExchange, false, nil); err != nil {
t.Error(err)
}
exchCfg, err = bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if !bot.Settings.EnableDryRun ||
!exchCfg.Verbose {
t.Error("dryrun should be true and verbose should be true")
}
}

View File

@@ -1,250 +0,0 @@
package engine
import (
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/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/trade"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
const (
fakePassExchange = "FakePassExchange"
)
// FakePassingExchange is used to override IBotExchange responses in tests
// In this context, we don't care what FakePassingExchange does as we're testing
// the engine package
type FakePassingExchange struct {
exchange.Base
}
// addPassingFakeExchange adds an exchange to engine tests where all funcs return a positive result
func addPassingFakeExchange(baseExchangeName string, bot *Engine) error {
testExch := bot.GetExchangeByName(baseExchangeName)
if testExch == nil {
return ErrExchangeNotFound
}
base := testExch.GetBase()
bot.Config.Exchanges = append(bot.Config.Exchanges, config.ExchangeConfig{
Name: fakePassExchange,
Enabled: true,
Verbose: false,
})
b := true
var pairStoreData = currency.PairStore{
AssetEnabled: &b,
}
var currencyMap = make(map[asset.Item]*currency.PairStore)
currencyMap[asset.Spot] = &pairStoreData
bot.exchangeManager.add(&FakePassingExchange{
Base: exchange.Base{
Name: fakePassExchange,
CurrencyPairs: currency.PairsManager{
Pairs: currencyMap},
Enabled: true,
LoadedByConfig: true,
SkipAuthCheck: true,
API: base.API,
Features: base.Features,
HTTPTimeout: base.HTTPTimeout,
HTTPUserAgent: base.HTTPUserAgent,
HTTPRecording: base.HTTPRecording,
HTTPDebugging: base.HTTPDebugging,
WebsocketResponseCheckTimeout: base.WebsocketResponseCheckTimeout,
WebsocketResponseMaxLimit: base.WebsocketResponseMaxLimit,
WebsocketOrderbookBufferLimit: base.WebsocketOrderbookBufferLimit,
Websocket: base.Websocket,
Requester: base.Requester,
Config: base.Config,
},
})
return nil
}
func (h *FakePassingExchange) Setup(_ *config.ExchangeConfig) error { return nil }
func (h *FakePassingExchange) Start(_ *sync.WaitGroup) {}
func (h *FakePassingExchange) SetDefaults() {}
func (h *FakePassingExchange) GetName() string { return fakePassExchange }
func (h *FakePassingExchange) IsEnabled() bool { return true }
func (h *FakePassingExchange) SetEnabled(bool) {}
func (h *FakePassingExchange) ValidateCredentials(_ asset.Item) error { return nil }
func (h *FakePassingExchange) FetchTicker(_ currency.Pair, _ asset.Item) (*ticker.Price, error) {
return nil, nil
}
func (h *FakePassingExchange) UpdateTicker(_ currency.Pair, _ asset.Item) (*ticker.Price, error) {
return nil, nil
}
func (h *FakePassingExchange) FetchOrderbook(_ currency.Pair, _ asset.Item) (*orderbook.Base, error) {
return nil, nil
}
func (h *FakePassingExchange) UpdateOrderbook(_ currency.Pair, _ asset.Item) (*orderbook.Base, error) {
return nil, nil
}
func (h *FakePassingExchange) FetchTradablePairs(_ asset.Item) ([]string, error) {
return nil, nil
}
func (h *FakePassingExchange) UpdateTradablePairs(_ bool) error { return nil }
func (h *FakePassingExchange) GetEnabledPairs(_ asset.Item) (currency.Pairs, error) {
return currency.Pairs{}, nil
}
func (h *FakePassingExchange) GetAvailablePairs(_ asset.Item) (currency.Pairs, error) {
return currency.Pairs{}, nil
}
func (h *FakePassingExchange) FetchAccountInfo(_ asset.Item) (account.Holdings, error) {
return account.Holdings{
Exchange: h.Name,
Accounts: []account.SubAccount{
{
Currencies: []account.Balance{
{
CurrencyName: currency.BTC,
TotalValue: 10.,
Hold: 0,
},
},
},
},
}, nil
}
func (h *FakePassingExchange) UpdateAccountInfo(_ asset.Item) (account.Holdings, error) {
return account.Holdings{
Exchange: h.Name,
Accounts: []account.SubAccount{
{
Currencies: []account.Balance{
{
CurrencyName: currency.BTC,
TotalValue: 20.,
Hold: 0,
},
},
},
},
}, nil
}
func (h *FakePassingExchange) GetAuthenticatedAPISupport(_ uint8) bool { return true }
func (h *FakePassingExchange) SetPairs(_ currency.Pairs, _ asset.Item, _ bool) error {
return nil
}
func (h *FakePassingExchange) GetAssetTypes() asset.Items { return asset.Items{asset.Spot} }
func (h *FakePassingExchange) GetHistoricTrades(_ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) {
return nil, nil
}
func (h *FakePassingExchange) GetRecentTrades(_ currency.Pair, _ asset.Item) ([]trade.Data, error) {
return nil, nil
}
func (h *FakePassingExchange) SupportsAutoPairUpdates() bool { return true }
func (h *FakePassingExchange) SupportsRESTTickerBatchUpdates() bool { return true }
func (h *FakePassingExchange) GetFeeByType(_ *exchange.FeeBuilder) (float64, error) {
return 0, nil
}
func (h *FakePassingExchange) GetLastPairsUpdateTime() int64 { return 0 }
func (h *FakePassingExchange) GetWithdrawPermissions() uint32 { return 0 }
func (h *FakePassingExchange) FormatWithdrawPermissions() string { return "" }
func (h *FakePassingExchange) SupportsWithdrawPermissions(_ uint32) bool { return true }
func (h *FakePassingExchange) GetFundingHistory() ([]exchange.FundHistory, error) { return nil, nil }
func (h *FakePassingExchange) SubmitOrder(_ *order.Submit) (order.SubmitResponse, error) {
return order.SubmitResponse{
IsOrderPlaced: true,
FullyMatched: true,
OrderID: "FakePassingExchangeOrder",
}, nil
}
func (h *FakePassingExchange) ModifyOrder(_ *order.Modify) (string, error) { return "", nil }
func (h *FakePassingExchange) CancelOrder(_ *order.Cancel) error { return nil }
func (h *FakePassingExchange) CancelBatchOrders(_ []order.Cancel) (order.CancelBatchResponse, error) {
return order.CancelBatchResponse{}, nil
}
func (h *FakePassingExchange) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) {
return order.CancelAllResponse{}, nil
}
func (h *FakePassingExchange) GetOrderInfo(_ string, _ currency.Pair, _ asset.Item) (order.Detail, error) {
return order.Detail{
Exchange: fakePassExchange,
ID: "fakeOrder",
}, nil
}
func (h *FakePassingExchange) GetWithdrawalsHistory(_ currency.Code) ([]exchange.WithdrawalHistory, error) {
return nil, nil
}
func (h *FakePassingExchange) GetDepositAddress(_ currency.Code, _ string) (string, error) {
return "", nil
}
func (h *FakePassingExchange) GetOrderHistory(_ *order.GetOrdersRequest) ([]order.Detail, error) {
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,
Amount: 1337,
Exchange: fakePassExchange,
ID: "fakeOrder",
Type: order.Market,
Side: order.Buy,
Status: order.Active,
AssetType: asset.Spot,
Date: time.Now(),
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() (*stream.Websocket, error) { return nil, nil }
func (h *FakePassingExchange) SubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error {
return nil
}
func (h *FakePassingExchange) UnsubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error {
return nil
}
func (h *FakePassingExchange) AuthenticateWebsocket() error { return nil }
func (h *FakePassingExchange) GetSubscriptions() ([]stream.ChannelSubscription, error) {
return nil, nil
}
func (h *FakePassingExchange) GetDefaultConfig() (*config.ExchangeConfig, error) { return nil, nil }
func (h *FakePassingExchange) SupportsAsset(_ asset.Item) bool { return true }
func (h *FakePassingExchange) GetHistoricCandles(_ currency.Pair, _ asset.Item, _, _ time.Time, _ kline.Interval) (kline.Item, error) {
return kline.Item{}, nil
}
func (h *FakePassingExchange) GetHistoricCandlesExtended(_ currency.Pair, _ asset.Item, _, _ time.Time, _ kline.Interval) (kline.Item, error) {
return kline.Item{}, nil
}
func (h *FakePassingExchange) DisableRateLimiter() error { return nil }
func (h *FakePassingExchange) EnableRateLimiter() error { return nil }
func (h *FakePassingExchange) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, nil
}
func (h *FakePassingExchange) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, nil
}
func (h *FakePassingExchange) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, nil
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/pquerna/otp/totp"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
@@ -28,8 +29,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/gctscript/vm"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio"
)
var (
@@ -41,19 +42,19 @@ var (
// GetSubsystemsStatus returns the status of various subsystems
func (bot *Engine) GetSubsystemsStatus() map[string]bool {
systems := make(map[string]bool)
systems["communications"] = bot.CommsManager.Started()
systems["internet_monitor"] = bot.ConnectionManager.Started()
systems["orders"] = bot.OrderManager.Started()
systems["portfolio"] = bot.PortfolioManager.Started()
systems["ntp_timekeeper"] = bot.NTPManager.Started()
systems["database"] = bot.DatabaseManager.Started()
systems["exchange_syncer"] = bot.Settings.EnableExchangeSyncManager
systems["grpc"] = bot.Settings.EnableGRPC
systems["grpc_proxy"] = bot.Settings.EnableGRPCProxy
systems["gctscript"] = bot.GctScriptManager.Started()
systems["deprecated_rpc"] = bot.Settings.EnableDeprecatedRPC
systems["websocket_rpc"] = bot.Settings.EnableWebsocketRPC
systems["dispatch"] = dispatch.IsRunning()
systems[SyncManagerName] = bot.CommunicationsManager.IsRunning()
systems[ConnectionManagerName] = bot.connectionManager.IsRunning()
systems[OrderManagerName] = bot.OrderManager.IsRunning()
systems[PortfolioManagerName] = bot.portfolioManager.IsRunning()
systems[NTPManagerName] = bot.ntpManager.IsRunning()
systems[DatabaseConnectionManagerName] = bot.DatabaseManager.IsRunning()
systems[SyncManagerName] = bot.Settings.EnableExchangeSyncManager
systems[grpcName] = bot.Settings.EnableGRPC
systems[grpcProxyName] = bot.Settings.EnableGRPCProxy
systems[vm.Name] = bot.gctScriptManager.IsRunning()
systems[DeprecatedName] = bot.Settings.EnableDeprecatedRPC
systems[WebsocketName] = bot.Settings.EnableWebsocketRPC
systems[dispatch.Name] = dispatch.IsRunning()
return systems
}
@@ -66,19 +67,19 @@ type RPCEndpoint struct {
// GetRPCEndpoints returns a list of RPC endpoints and their listen addrs
func GetRPCEndpoints() map[string]RPCEndpoint {
endpoints := make(map[string]RPCEndpoint)
endpoints["grpc"] = RPCEndpoint{
endpoints[grpcName] = RPCEndpoint{
Started: Bot.Settings.EnableGRPC,
ListenAddr: "grpc://" + Bot.Config.RemoteControl.GRPC.ListenAddress,
}
endpoints["grpc_proxy"] = RPCEndpoint{
endpoints[grpcProxyName] = RPCEndpoint{
Started: Bot.Settings.EnableGRPCProxy,
ListenAddr: "http://" + Bot.Config.RemoteControl.GRPC.GRPCProxyListenAddress,
}
endpoints["deprecated_rpc"] = RPCEndpoint{
endpoints[DeprecatedName] = RPCEndpoint{
Started: Bot.Settings.EnableDeprecatedRPC,
ListenAddr: "http://" + Bot.Config.RemoteControl.DeprecatedRPC.ListenAddress,
}
endpoints["websocket_rpc"] = RPCEndpoint{
endpoints[WebsocketName] = RPCEndpoint{
Started: Bot.Settings.EnableWebsocketRPC,
ListenAddr: "ws://" + Bot.Config.RemoteControl.WebsocketRPC.ListenAddress,
}
@@ -86,53 +87,157 @@ func GetRPCEndpoints() map[string]RPCEndpoint {
}
// SetSubsystem enables or disables an engine subsystem
func (bot *Engine) SetSubsystem(subsys string, enable bool) error {
switch strings.ToLower(subsys) {
case "communications":
func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error {
var err error
switch strings.ToLower(subSystemName) {
case CommunicationsManagerName:
if enable {
return bot.CommsManager.Start()
if bot.CommunicationsManager == nil {
communicationsConfig := bot.Config.GetCommunicationsConfig()
bot.CommunicationsManager, err = SetupCommunicationManager(&communicationsConfig)
if err != nil {
return err
}
}
return bot.CommunicationsManager.Start()
}
return bot.CommsManager.Stop()
case "internet_monitor":
return bot.CommunicationsManager.Stop()
case ConnectionManagerName:
if enable {
return bot.ConnectionManager.Start(&bot.Config.ConnectionMonitor)
if bot.connectionManager == nil {
bot.connectionManager, err = setupConnectionManager(&bot.Config.ConnectionMonitor)
if err != nil {
return err
}
}
return bot.connectionManager.Start()
}
return bot.CommsManager.Stop()
case "orders":
return bot.connectionManager.Stop()
case OrderManagerName:
if enable {
return bot.OrderManager.Start(bot)
if bot.OrderManager == nil {
bot.OrderManager, err = SetupOrderManager(
bot.ExchangeManager,
bot.CommunicationsManager,
&bot.ServicesWG,
bot.Settings.Verbose)
if err != nil {
return err
}
}
return bot.OrderManager.Start()
}
return bot.OrderManager.Stop()
case "portfolio":
case PortfolioManagerName:
if enable {
return bot.PortfolioManager.Start()
if bot.portfolioManager == nil {
bot.portfolioManager, err = setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, &bot.Config.Portfolio)
if err != nil {
return err
}
}
return bot.portfolioManager.Start(&bot.ServicesWG)
}
return bot.OrderManager.Stop()
case "ntp_timekeeper":
return bot.portfolioManager.Stop()
case NTPManagerName:
if enable {
return bot.NTPManager.Start()
if bot.ntpManager == nil {
bot.ntpManager, err = setupNTPManager(
&bot.Config.NTPClient,
*bot.Config.Logging.Enabled)
if err != nil {
return err
}
}
return bot.ntpManager.Start()
}
return bot.NTPManager.Stop()
case "database":
return bot.ntpManager.Stop()
case DatabaseConnectionManagerName:
if enable {
return bot.DatabaseManager.Start(bot)
if bot.DatabaseManager == nil {
bot.DatabaseManager, err = SetupDatabaseConnectionManager(&bot.Config.Database)
if err != nil {
return err
}
}
return bot.DatabaseManager.Start(&bot.ServicesWG)
}
return bot.DatabaseManager.Stop()
case "exchange_syncer":
case SyncManagerName:
if enable {
bot.ExchangeCurrencyPairManager.Start()
if bot.currencyPairSyncer == nil {
exchangeSyncCfg := &Config{
SyncTicker: bot.Settings.EnableTickerSyncing,
SyncOrderbook: bot.Settings.EnableOrderbookSyncing,
SyncTrades: bot.Settings.EnableTradeSyncing,
SyncContinuously: bot.Settings.SyncContinuously,
NumWorkers: bot.Settings.SyncWorkers,
Verbose: bot.Settings.Verbose,
SyncTimeoutREST: bot.Settings.SyncTimeoutREST,
SyncTimeoutWebsocket: bot.Settings.SyncTimeoutWebsocket,
}
bot.currencyPairSyncer, err = setupSyncManager(exchangeSyncCfg,
bot.ExchangeManager,
bot.websocketRoutineManager,
&bot.Config.RemoteControl)
if err != nil {
return err
}
}
return bot.currencyPairSyncer.Start()
}
bot.ExchangeCurrencyPairManager.Stop()
case "dispatch":
return bot.currencyPairSyncer.Stop()
case dispatch.Name:
if enable {
return dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit)
}
return dispatch.Stop()
case "gctscript":
case DeprecatedName:
if enable {
return bot.GctScriptManager.Start(&bot.ServicesWG)
if bot.apiServer == nil {
var filePath string
filePath, err = config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile)
if err != nil {
return err
}
bot.apiServer, err = setupAPIServerManager(&bot.Config.RemoteControl, &bot.Config.Profiler, bot.ExchangeManager, bot, bot.portfolioManager, filePath)
if err != nil {
return err
}
}
return bot.apiServer.StartRESTServer()
}
return bot.GctScriptManager.Stop()
return bot.apiServer.StopRESTServer()
case WebsocketName:
if enable {
if bot.apiServer == nil {
var filePath string
filePath, err = config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile)
if err != nil {
return err
}
bot.apiServer, err = setupAPIServerManager(&bot.Config.RemoteControl, &bot.Config.Profiler, bot.ExchangeManager, bot, bot.portfolioManager, filePath)
if err != nil {
return err
}
}
return bot.apiServer.StartWebsocketServer()
}
return bot.apiServer.StopWebsocketServer()
case grpcName, grpcProxyName:
return errors.New("cannot manage GRPC subsystem via GRPC. Please manually change your config")
case vm.Name:
if enable {
if bot.gctScriptManager == nil {
bot.gctScriptManager, err = vm.NewManager(&bot.Config.GCTScript)
if err != nil {
return err
}
}
return bot.gctScriptManager.Start(&bot.ServicesWG)
}
return bot.gctScriptManager.Stop()
}
return errors.New("subsystem not found")
@@ -162,9 +267,9 @@ func (bot *Engine) GetExchangeOTPs() (map[string]string, error) {
return otpCodes, nil
}
// GetExchangeoOTPByName returns a OTP code for the desired exchange
// GetExchangeOTPByName returns a OTP code for the desired exchange
// if it exists
func (bot *Engine) GetExchangeoOTPByName(exchName string) (string, error) {
func (bot *Engine) GetExchangeOTPByName(exchName string) (string, error) {
for x := range bot.Config.Exchanges {
if !strings.EqualFold(bot.Config.Exchanges[x].Name, exchName) {
continue
@@ -193,18 +298,7 @@ func (bot *Engine) GetAuthAPISupportedExchanges() []string {
// IsOnline returns whether or not the engine has Internet connectivity
func (bot *Engine) IsOnline() bool {
return bot.ConnectionManager.IsOnline()
}
// GetAvailableExchanges returns a list of enabled exchanges
func (bot *Engine) GetAvailableExchanges() []string {
var enExchanges []string
for x := range bot.Config.Exchanges {
if bot.Config.Exchanges[x].Enabled {
enExchanges = append(enExchanges, bot.Config.Exchanges[x].Name)
}
}
return enExchanges
return bot.connectionManager.IsOnline()
}
// GetAllAvailablePairs returns a list of all available pairs on either enabled
@@ -494,90 +588,6 @@ func GetExchangeLowestPriceByCurrencyPair(p currency.Pair, assetType asset.Item)
return result[0].Exchange, nil
}
// SeedExchangeAccountInfo seeds account info
func SeedExchangeAccountInfo(accounts []account.Holdings) {
if len(accounts) == 0 {
return
}
port := portfolio.GetPortfolio()
for x := range accounts {
exchangeName := accounts[x].Exchange
var currencies []account.Balance
for y := range accounts[x].Accounts {
for z := range accounts[x].Accounts[y].Currencies {
var update bool
for i := range currencies {
if accounts[x].Accounts[y].Currencies[z].CurrencyName == currencies[i].CurrencyName {
currencies[i].Hold += accounts[x].Accounts[y].Currencies[z].Hold
currencies[i].TotalValue += accounts[x].Accounts[y].Currencies[z].TotalValue
update = true
}
}
if update {
continue
}
currencies = append(currencies, account.Balance{
CurrencyName: accounts[x].Accounts[y].Currencies[z].CurrencyName,
TotalValue: accounts[x].Accounts[y].Currencies[z].TotalValue,
Hold: accounts[x].Accounts[y].Currencies[z].Hold,
})
}
}
for x := range currencies {
currencyName := currencies[x].CurrencyName
total := currencies[x].TotalValue
if !port.ExchangeAddressExists(exchangeName, currencyName) {
if total <= 0 {
continue
}
log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n",
exchangeName,
currencyName,
total,
portfolio.PortfolioAddressExchange)
port.Addresses = append(
port.Addresses,
portfolio.Address{Address: exchangeName,
CoinType: currencyName,
Balance: total,
Description: portfolio.PortfolioAddressExchange})
} else {
if total <= 0 {
log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n",
exchangeName,
currencyName)
port.RemoveExchangeAddress(exchangeName, currencyName)
} else {
balance, ok := port.GetAddressBalance(exchangeName,
portfolio.PortfolioAddressExchange,
currencyName)
if !ok {
continue
}
if balance != total {
log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n",
exchangeName,
currencyName,
total)
port.UpdateExchangeAddressBalance(exchangeName,
currencyName,
total)
}
}
}
}
}
}
// GetCryptocurrenciesByExchange returns a list of cryptocurrencies the exchange supports
func (bot *Engine) GetCryptocurrenciesByExchange(exchangeName string, enabledExchangesOnly, enabledPairs bool, assetType asset.Item) ([]string, error) {
var cryptocurrencies []string
@@ -636,7 +646,7 @@ func (bot *Engine) GetCryptocurrencyDepositAddressesByExchange(exchName string)
// exchange
func (bot *Engine) GetExchangeCryptocurrencyDepositAddress(exchName, accountID string, item currency.Code) (string, error) {
if bot.DepositAddressManager != nil {
return bot.DepositAddressManager.GetDepositAddressByExchange(exchName, item)
return bot.DepositAddressManager.GetDepositAddressByExchangeAndCurrency(exchName, item)
}
exch := bot.GetExchangeByName(exchName)
@@ -680,21 +690,16 @@ func (bot *Engine) GetExchangeCryptocurrencyDepositAddresses() map[string]map[st
return result
}
// FormatCurrency is a method that formats and returns a currency pair
// based on the user currency display preferences
func (bot *Engine) FormatCurrency(p currency.Pair) currency.Pair {
return p.Format(bot.Config.Currency.CurrencyPairFormat.Delimiter,
bot.Config.Currency.CurrencyPairFormat.Uppercase)
}
// GetExchangeNames returns a list of enabled or disabled exchanges
func (bot *Engine) GetExchangeNames(enabledOnly bool) []string {
exchangeNames := bot.GetAvailableExchanges()
if enabledOnly {
return exchangeNames
exchanges := bot.ExchangeManager.GetExchanges()
var response []string
for i := range exchanges {
if !enabledOnly || (enabledOnly && exchanges[i].IsEnabled()) {
response = append(response, exchanges[i].GetName())
}
}
exchangeNames = append(exchangeNames, bot.Config.GetDisabledExchanges()...)
return exchangeNames
return response
}
// GetAllActiveTickers returns all enabled exchange tickers
@@ -732,44 +737,6 @@ func (bot *Engine) GetAllActiveTickers() []EnabledExchangeCurrencies {
return tickerData
}
// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges
func (bot *Engine) GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts {
var response AllEnabledExchangeAccounts
exchanges := bot.GetExchanges()
for x := range exchanges {
if exchanges[x] == nil || !exchanges[x].IsEnabled() {
continue
}
if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if bot.Settings.Verbose {
log.Debugf(log.ExchangeSys,
"GetAllEnabledExchangeAccountInfo: Skipping %s due to disabled authenticated API support.\n",
exchanges[x].GetName())
}
continue
}
assetTypes := exchanges[x].GetAssetTypes()
var exchangeHoldings account.Holdings
for y := range assetTypes {
accountHoldings, err := exchanges[x].FetchAccountInfo(assetTypes[y])
if err != nil {
log.Errorf(log.ExchangeSys,
"Error encountered retrieving exchange account info for %s. Error %s\n",
exchanges[x].GetName(),
err)
continue
}
for z := range accountHoldings.Accounts {
accountHoldings.Accounts[z].AssetType = assetTypes[y]
}
exchangeHoldings.Exchange = exchanges[x].GetName()
exchangeHoldings.Accounts = append(exchangeHoldings.Accounts, accountHoldings.Accounts...)
}
response.Data = append(response.Data, exchangeHoldings)
}
return response
}
func verifyCert(pemData []byte) error {
var pemBlock *pem.Block
pemBlock, _ = pem.Decode(pemData)

View File

@@ -7,6 +7,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"net"
"os"
@@ -25,6 +26,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
var testExchange = "Bitstamp"
func CreateTestBot(t *testing.T) *Engine {
bot, err := NewFromSettings(&Settings{ConfigFile: config.TestFile, EnableDryRun: true}, nil)
if err != nil {
@@ -35,19 +38,14 @@ func CreateTestBot(t *testing.T) *Engine {
if err != nil {
t.Fatalf("Failed to retrieve config currency pairs. %s", err)
}
bot.ExchangeManager = SetupExchangeManager()
if bot.GetExchangeByName(testExchange) == nil {
err = bot.LoadExchange(testExchange, false, nil)
if err != nil {
t.Fatalf("SetupTest: Failed to load exchange: %s", err)
}
}
if bot.GetExchangeByName(fakePassExchange) == nil {
err = addPassingFakeExchange(testExchange, bot)
err := bot.LoadExchange(testExchange, false, nil)
if err != nil {
t.Fatalf("SetupTest: Failed to load exchange: %s", err)
}
}
return bot
}
@@ -62,7 +60,7 @@ func TestGetExchangeOTPs(t *testing.T) {
if err != nil {
t.Fatal(err)
}
bCfg, err := bot.Config.GetExchangeConfig("Bitstamp")
bCfg, err := bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Fatal(err)
}
@@ -93,18 +91,18 @@ func TestGetExchangeOTPs(t *testing.T) {
func TestGetExchangeoOTPByName(t *testing.T) {
bot := CreateTestBot(t)
_, err := bot.GetExchangeoOTPByName("Bitstamp")
_, err := bot.GetExchangeOTPByName(testExchange)
if err == nil {
t.Fatal("Expected err with no exchange OTP secrets set")
}
bCfg, err := bot.Config.GetExchangeConfig("Bitstamp")
bCfg, err := bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Fatal(err)
}
bCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP"
result, err := bot.GetExchangeoOTPByName("Bitstamp")
result, err := bot.GetExchangeOTPByName(testExchange)
if err != nil {
t.Fatal(err)
}
@@ -118,6 +116,25 @@ func TestGetExchangeoOTPByName(t *testing.T) {
func TestGetAuthAPISupportedExchanges(t *testing.T) {
e := CreateTestBot(t)
if result := e.GetAuthAPISupportedExchanges(); len(result) != 0 {
t.Fatal("Unexpected result", result)
}
exch := e.ExchangeManager.GetExchangeByName(testExchange)
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Error(err)
}
cfg.Enabled = true
cfg.API.AuthenticatedSupport = true
cfg.API.AuthenticatedWebsocketSupport = true
cfg.API.Credentials.Key = "test"
cfg.API.Credentials.Secret = "test"
cfg.WebsocketTrafficTimeout = time.Minute
err = exch.Setup(cfg)
if err != nil {
t.Error(err)
}
if result := e.GetAuthAPISupportedExchanges(); len(result) != 1 {
t.Fatal("Unexpected result", result)
}
@@ -125,11 +142,16 @@ func TestGetAuthAPISupportedExchanges(t *testing.T) {
func TestIsOnline(t *testing.T) {
e := CreateTestBot(t)
var err error
e.connectionManager, err = setupConnectionManager(&e.Config.ConnectionMonitor)
if err != nil {
t.Fatal(err)
}
if r := e.IsOnline(); r {
t.Fatal("Unexpected result")
}
if err := e.ConnectionManager.Start(&e.Config.ConnectionMonitor); err != nil {
if err = e.connectionManager.Start(); err != nil {
t.Fatal(err)
}
@@ -141,7 +163,7 @@ func TestIsOnline(t *testing.T) {
t.Fatal("Test timeout")
default:
if e.IsOnline() {
if err := e.ConnectionManager.Stop(); err != nil {
if err := e.connectionManager.Stop(); err != nil {
t.Fatal("unable to shutdown connection manager")
}
return
@@ -150,13 +172,6 @@ func TestIsOnline(t *testing.T) {
}
}
func TestGetAvailableExchanges(t *testing.T) {
e := CreateTestBot(t)
if r := len(e.GetAvailableExchanges()); r == 0 {
t.Error("Expected len > 0")
}
}
func TestGetSpecificAvailablePairs(t *testing.T) {
e := CreateTestBot(t)
assetType := asset.Spot
@@ -475,7 +490,7 @@ func TestMapCurrenciesByExchange(t *testing.T) {
}
result := e.MapCurrenciesByExchange(pairs, true, asset.Spot)
pairs, ok := result["Bitstamp"]
pairs, ok := result[testExchange]
if !ok {
t.Fatal("Unexpected result")
}
@@ -507,7 +522,7 @@ func TestGetExchangeNamesByCurrency(t *testing.T) {
result := e.GetExchangeNamesByCurrency(btsusd,
true,
assetType)
if !common.StringDataCompare(result, "Bitstamp") {
if !common.StringDataCompare(result, testExchange) {
t.Fatal("Unexpected result")
}
@@ -529,8 +544,6 @@ func TestGetExchangeNamesByCurrency(t *testing.T) {
func TestGetSpecificOrderbook(t *testing.T) {
e := CreateTestBot(t)
e.LoadExchange("Bitstamp", false, nil)
var bids []orderbook.Item
bids = append(bids, orderbook.Item{Price: 1000, Amount: 1})
@@ -551,7 +564,7 @@ func TestGetSpecificOrderbook(t *testing.T) {
t.Fatal(err)
}
ob, err := e.GetSpecificOrderbook(btsusd, "Bitstamp", asset.Spot)
ob, err := e.GetSpecificOrderbook(btsusd, testExchange, asset.Spot)
if err != nil {
t.Fatal(err)
}
@@ -565,18 +578,19 @@ func TestGetSpecificOrderbook(t *testing.T) {
t.Fatal(err)
}
_, err = e.GetSpecificOrderbook(ethltc, "Bitstamp", asset.Spot)
_, err = e.GetSpecificOrderbook(ethltc, testExchange, asset.Spot)
if err == nil {
t.Fatal("Unexpected result")
}
e.UnloadExchange("Bitstamp")
err = e.UnloadExchange(testExchange)
if err != nil {
t.Error(err)
}
}
func TestGetSpecificTicker(t *testing.T) {
e := CreateTestBot(t)
e.LoadExchange("Bitstamp", false, nil)
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
@@ -586,12 +600,12 @@ func TestGetSpecificTicker(t *testing.T) {
Pair: p,
Last: 1000,
AssetType: asset.Spot,
ExchangeName: "Bitstamp"})
ExchangeName: testExchange})
if err != nil {
t.Fatal("ProcessTicker error", err)
}
tick, err := e.GetSpecificTicker(p, "Bitstamp", asset.Spot)
tick, err := e.GetSpecificTicker(p, testExchange, asset.Spot)
if err != nil {
t.Fatal(err)
}
@@ -605,12 +619,15 @@ func TestGetSpecificTicker(t *testing.T) {
t.Fatal(err)
}
_, err = e.GetSpecificTicker(ethltc, "Bitstamp", asset.Spot)
_, err = e.GetSpecificTicker(ethltc, testExchange, asset.Spot)
if err == nil {
t.Fatal("Unexpected result")
}
e.UnloadExchange("Bitstamp")
err = e.UnloadExchange(testExchange)
if err != nil {
t.Error(err)
}
}
func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) {
@@ -634,7 +651,7 @@ func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) {
exchangeInfo = append(exchangeInfo, bitfinexHoldings)
var bitstampHoldings account.Holdings
bitstampHoldings.Exchange = "Bitstamp"
bitstampHoldings.Exchange = testExchange
bitstampHoldings.Accounts = append(bitstampHoldings.Accounts,
account.SubAccount{
Currencies: []account.Balance{
@@ -681,14 +698,20 @@ func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) {
t.Fatal(err)
}
stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
stats.Add("Bitstamp", p, asset.Spot, 1337, 10000)
err = stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
if err != nil {
t.Error(err)
}
err = stats.Add(testExchange, p, asset.Spot, 1337, 10000)
if err != nil {
t.Error(err)
}
exchangeName, err := GetExchangeHighestPriceByCurrencyPair(p, asset.Spot)
if err != nil {
t.Error(err)
}
if exchangeName != "Bitstamp" {
if exchangeName != testExchange {
t.Error("Unexpected result")
}
@@ -711,8 +734,14 @@ func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) {
t.Fatal(err)
}
stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
stats.Add("Bitstamp", p, asset.Spot, 1337, 10000)
err = stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
if err != nil {
t.Error(err)
}
err = stats.Add(testExchange, p, asset.Spot, 1337, 10000)
if err != nil {
t.Error(err)
}
exchangeName, err := GetExchangeLowestPriceByCurrencyPair(p, asset.Spot)
if err != nil {
t.Error(err)
@@ -753,8 +782,22 @@ func TestGetExchangeNames(t *testing.T) {
if e := bot.GetExchangeNames(true); common.StringDataCompare(e, testExchange) {
t.Error("Bitstamp should be missing")
}
if e := bot.GetExchangeNames(false); len(e) != 0 {
t.Errorf("Expected %v Received %v", len(e), 0)
}
for i := range bot.Config.Exchanges {
exch, err := bot.ExchangeManager.NewExchangeByName(bot.Config.Exchanges[i].Name)
if err != nil && !errors.Is(err, ErrExchangeAlreadyLoaded) {
t.Error(err)
}
if exch != nil {
exch.SetDefaults()
bot.ExchangeManager.Add(exch)
}
}
if e := bot.GetExchangeNames(false); len(e) != len(bot.Config.Exchanges) {
t.Errorf("Expected %v Received %v", len(e), len(bot.Config.Exchanges))
t.Errorf("Expected %v Received %v", len(bot.Config.Exchanges), len(e))
}
}

205
engine/ntp_manager.go Normal file
View File

@@ -0,0 +1,205 @@
package engine
import (
"encoding/binary"
"fmt"
"net"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/log"
)
// setupNTPManager creates a new NTP manager
func setupNTPManager(cfg *config.NTPClientConfig, loggingEnabled bool) (*ntpManager, error) {
if cfg == nil {
return nil, errNilConfig
}
if cfg.AllowedNegativeDifference == nil ||
cfg.AllowedDifference == nil {
return nil, errNilConfigValues
}
return &ntpManager{
shutdown: make(chan struct{}),
level: int64(cfg.Level),
allowedDifference: *cfg.AllowedDifference,
allowedNegativeDifference: *cfg.AllowedNegativeDifference,
pools: cfg.Pool,
checkInterval: defaultNTPCheckInterval,
retryLimit: defaultRetryLimit,
loggingEnabled: loggingEnabled,
}, nil
}
// IsRunning safely checks whether the subsystem is running
func (m *ntpManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Start runs the subsystem
func (m *ntpManager) Start() error {
if m == nil {
return fmt.Errorf("ntp manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("NTP manager %w", ErrSubSystemAlreadyStarted)
}
if m.level == 0 && m.loggingEnabled {
// Sometimes the NTP client can have transient issues due to UDP, try
// the default retry limits before giving up
check:
for i := 0; i < m.retryLimit; i++ {
err := m.processTime()
switch err {
case nil:
break check
case ErrSubSystemNotStarted:
log.Debugln(log.TimeMgr, "NTP manager: User disabled NTP prompts. Exiting.")
atomic.CompareAndSwapInt32(&m.started, 1, 0)
return nil
default:
if i == m.retryLimit-1 {
return err
}
}
}
}
if m.level != 1 {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
return errNTPManagerDisabled
}
m.shutdown = make(chan struct{})
go m.run()
log.Debugf(log.TimeMgr, "NTP manager %s", MsgSubSystemStarted)
return nil
}
// Stop attempts to shutdown the subsystem
func (m *ntpManager) Stop() error {
if m == nil {
return fmt.Errorf("ntp manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("NTP manager %w", ErrSubSystemNotStarted)
}
defer func() {
log.Debugf(log.TimeMgr, "NTP manager %s", MsgSubSystemShutdown)
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}()
log.Debugf(log.TimeMgr, "NTP manager %s", MsgSubSystemShuttingDown)
close(m.shutdown)
return nil
}
// continuously checks the internet connection at intervals
func (m *ntpManager) run() {
t := time.NewTicker(m.checkInterval)
defer func() {
t.Stop()
}()
for {
select {
case <-m.shutdown:
return
case <-t.C:
err := m.processTime()
if err != nil {
log.Error(log.TimeMgr, err)
}
}
}
}
// FetchNTPTime returns the time from defined NTP pools
func (m *ntpManager) FetchNTPTime() (time.Time, error) {
if m == nil {
return time.Time{}, fmt.Errorf("ntp manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return time.Time{}, fmt.Errorf("NTP manager %w", ErrSubSystemNotStarted)
}
return checkTimeInPools(m.pools), nil
}
// processTime determines the difference between system time and NTP time
// to discover discrepancies
func (m *ntpManager) processTime() error {
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("NTP manager %w", ErrSubSystemNotStarted)
}
NTPTime, err := m.FetchNTPTime()
if err != nil {
return err
}
currentTime := time.Now()
diff := NTPTime.Sub(currentTime)
configNTPTime := m.allowedDifference
negDiff := m.allowedNegativeDifference
configNTPNegativeTime := -negDiff
if diff > configNTPTime || diff < configNTPNegativeTime {
log.Warnf(log.TimeMgr, "NTP manager: Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v\n",
NTPTime,
currentTime,
diff,
configNTPTime,
configNTPNegativeTime)
}
return nil
}
// checkTimeInPools returns local based on ntp servers provided timestamp
// if no server can be reached will return local time in UTC()
func checkTimeInPools(pool []string) time.Time {
for i := range pool {
con, err := net.DialTimeout("udp", pool[i], 5*time.Second)
if err != nil {
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", pool[i])
continue
}
if err = con.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
log.Warnf(log.TimeMgr, "Unable to SetDeadline. Error: %s\n", err)
err = con.Close()
if err != nil {
log.Error(log.TimeMgr, err)
}
continue
}
req := &ntpPacket{Settings: 0x1B}
if err = binary.Write(con, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "Unable to write. Error: %s\n", err)
err = con.Close()
if err != nil {
log.Error(log.TimeMgr, err)
}
continue
}
rsp := &ntpPacket{}
if err = binary.Read(con, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "Unable to read. Error: %s\n", err)
err = con.Close()
if err != nil {
log.Error(log.TimeMgr, err)
}
continue
}
secs := float64(rsp.TxTimeSec) - 2208988800
nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32
err = con.Close()
if err != nil {
log.Error(log.TimeMgr, err)
}
return time.Unix(int64(secs), nanos)
}
log.Warnln(log.TimeMgr, "No valid NTP servers found, using current system time")
return time.Now().UTC()
}

56
engine/ntp_manager.md Normal file
View File

@@ -0,0 +1,56 @@
# GoCryptoTrader package Ntp_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/ntp_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This ntp_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Ntp_manager
+ The NTP manager subsystem is used highlight discrepancies between your system time and specified NTP server times
+ It is useful for debugging and understanding why a request to an exchange may be rejected
+ The NTP manager cannot update your system clock, so when it does alert you of issues, you must take it upon yourself to change your system time in the event your requests are being rejected for being too far out of sync
+ In order to modify the behaviour of the NTP manager subsystem, you can edit the following inside your config file under `ntpclient`:
### ntpclient
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | An integer value representing whether the NTP manager is enabled. It will warn you of time sync discrepancies on startup with a value of 0 and will alert you periodically with a value of 1. A value of -1 will disable the manager | `1` |
| pool | A string array of the NTP servers to check for time discrepancies | `["0.pool.ntp.org:123","pool.ntp.org:123"]` |
| allowedDifference | A Golang time.Duration representation of the allowable time discrepancy between NTP server and your system time. Any discrepancy greater than this allowance will display an alert to your logging output | `50000000` |
| allowedNegativeDifference | A Golang time.Duration representation of the allowable negative time discrepancy between NTP server and your system time. Any discrepancy greater than this allowance will display an alert to your logging output | `50000000` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

206
engine/ntp_manager_test.go Normal file
View File

@@ -0,0 +1,206 @@
package engine
import (
"errors"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
)
func TestSetupNTPManager(t *testing.T) {
_, err := setupNTPManager(nil, false)
if !errors.Is(err, errNilConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilConfig)
}
_, err = setupNTPManager(&config.NTPClientConfig{}, false)
if !errors.Is(err, errNilConfigValues) {
t.Errorf("error '%v', expected '%v'", err, errNilConfigValues)
}
sec := time.Second
cfg := &config.NTPClientConfig{
AllowedDifference: &sec,
AllowedNegativeDifference: &sec,
}
m, err := setupNTPManager(cfg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expected manager")
}
}
func TestNTPManagerIsRunning(t *testing.T) {
var m *ntpManager
if m.IsRunning() {
t.Error("expected false")
}
sec := time.Second
cfg := &config.NTPClientConfig{
AllowedDifference: &sec,
AllowedNegativeDifference: &sec,
Level: 1,
}
m, err := setupNTPManager(cfg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.IsRunning() {
t.Error("expected false")
}
err = m.Start()
if err != nil {
t.Error(err)
}
if !m.IsRunning() {
t.Error("expected true")
}
}
func TestNTPManagerStart(t *testing.T) {
var m *ntpManager
err := m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
sec := time.Second
cfg := &config.NTPClientConfig{
AllowedDifference: &sec,
AllowedNegativeDifference: &sec,
}
m, err = setupNTPManager(cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, errNTPManagerDisabled) {
t.Errorf("error '%v', expected '%v'", err, errNTPManagerDisabled)
}
m.level = 1
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
}
func TestNTPManagerStop(t *testing.T) {
var m *ntpManager
err := m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
sec := time.Second
cfg := &config.NTPClientConfig{
AllowedDifference: &sec,
AllowedNegativeDifference: &sec,
Level: 1,
}
m, err = setupNTPManager(cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestFetchNTPTime(t *testing.T) {
var m *ntpManager
_, err := m.FetchNTPTime()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
sec := time.Second
cfg := &config.NTPClientConfig{
AllowedDifference: &sec,
AllowedNegativeDifference: &sec,
Level: 1,
}
m, err = setupNTPManager(cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
_, err = m.FetchNTPTime()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
tt, err := m.FetchNTPTime()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if tt.IsZero() {
t.Error("expected time")
}
m.pools = []string{"0.pool.ntp.org:123"}
tt, err = m.FetchNTPTime()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if tt.IsZero() {
t.Error("expected time")
}
}
func TestProcessTime(t *testing.T) {
sec := time.Second
cfg := &config.NTPClientConfig{
AllowedDifference: &sec,
AllowedNegativeDifference: &sec,
Level: 1,
Pool: []string{"0.pool.ntp.org:123"},
}
m, err := setupNTPManager(cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.processTime()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.processTime()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.allowedDifference = time.Duration(1)
m.allowedNegativeDifference = time.Duration(1)
err = m.processTime()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}

View File

@@ -0,0 +1,49 @@
package engine
import (
"errors"
"time"
)
const (
defaultNTPCheckInterval = time.Second * 30
defaultRetryLimit = 3
// NTPManagerName is an exported subsystem name
NTPManagerName = "ntp_timekeeper"
)
var (
errNilConfigValues = errors.New("nil allowed time differences received")
errNTPManagerDisabled = errors.New("NTP manager disabled")
)
// ntpManager starts the NTP manager
type ntpManager struct {
started int32
shutdown chan struct{}
level int64
allowedDifference time.Duration
allowedNegativeDifference time.Duration
pools []string
checkInterval time.Duration
retryLimit int
loggingEnabled bool
}
type ntpPacket struct {
Settings uint8 // leap yr indicator, ver number, and mode
Stratum uint8 // stratum of local clock
Poll int8 // poll exponent
Precision int8 // precision exponent
RootDelay uint32 // root delay
RootDispersion uint32 // root dispersion
ReferenceID uint32 // reference id
RefTimeSec uint32 // reference timestamp sec
RefTimeFrac uint32 // reference timestamp fractional
OrigTimeSec uint32 // origin time secs
OrigTimeFrac uint32 // origin time fractional
RxTimeSec uint32 // receive time secs
RxTimeFrac uint32 // receive time frac
TxTimeSec uint32 // transmit time secs
TxTimeFrac uint32 // transmit time frac
}

738
engine/order_manager.go Normal file
View File

@@ -0,0 +1,738 @@
package engine
import (
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"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/log"
)
// SetupOrderManager will boot up the OrderManager
func SetupOrderManager(exchangeManager iExchangeManager, communicationsManager iCommsManager, wg *sync.WaitGroup, verbose bool) (*OrderManager, error) {
if exchangeManager == nil {
return nil, errNilExchangeManager
}
if communicationsManager == nil {
return nil, errNilCommunicationsManager
}
if wg == nil {
return nil, errNilWaitGroup
}
return &OrderManager{
shutdown: make(chan struct{}),
orderStore: store{
Orders: make(map[string][]*order.Detail),
exchangeManager: exchangeManager,
commsManager: communicationsManager,
wg: wg,
},
verbose: verbose,
}, nil
}
// IsRunning safely checks whether the subsystem is running
func (m *OrderManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Start runs the subsystem
func (m *OrderManager) Start() error {
if m == nil {
return fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("order manager %w", ErrSubSystemAlreadyStarted)
}
log.Debugln(log.OrderMgr, "Order manager starting...")
m.shutdown = make(chan struct{})
go m.run()
return nil
}
// Stop attempts to shutdown the subsystem
func (m *OrderManager) Stop() error {
if m == nil {
return fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}()
log.Debugln(log.OrderMgr, "Order manager shutting down...")
close(m.shutdown)
return nil
}
// gracefulShutdown cancels all orders (if enabled) before shutting down
func (m *OrderManager) gracefulShutdown() {
if m.cfg.CancelOrdersOnShutdown {
log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...")
m.CancelAllOrders(m.orderStore.exchangeManager.GetExchanges())
}
}
// run will periodically process orders
func (m *OrderManager) run() {
log.Debugln(log.OrderMgr, "Order manager started.")
tick := time.NewTicker(orderManagerDelay)
m.orderStore.wg.Add(1)
defer func() {
log.Debugln(log.OrderMgr, "Order manager shutdown.")
tick.Stop()
m.orderStore.wg.Done()
}()
for {
select {
case <-m.shutdown:
m.gracefulShutdown()
return
case <-tick.C:
go m.processOrders()
}
}
}
// CancelAllOrders iterates and cancels all orders for each exchange provided
func (m *OrderManager) CancelAllOrders(exchangeNames []exchange.IBotExchange) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return
}
orders := m.orderStore.get()
if orders == nil {
return
}
for i := range exchangeNames {
exchangeOrders, ok := orders[strings.ToLower(exchangeNames[i].GetName())]
if !ok {
continue
}
for j := range exchangeOrders {
log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.", exchangeNames[i].GetName())
err := m.Cancel(&order.Cancel{
Exchange: exchangeOrders[j].Exchange,
ID: exchangeOrders[j].ID,
AccountID: exchangeOrders[j].AccountID,
ClientID: exchangeOrders[j].ClientID,
WalletAddress: exchangeOrders[j].WalletAddress,
Type: exchangeOrders[j].Type,
Side: exchangeOrders[j].Side,
Pair: exchangeOrders[j].Pair,
AssetType: exchangeOrders[j].AssetType,
})
if err != nil {
log.Error(log.OrderMgr, err)
}
}
}
}
// Cancel will find the order in the OrderManager, send a cancel request
// to the exchange and if successful, update the status of the order
func (m *OrderManager) Cancel(cancel *order.Cancel) error {
if m == nil {
return fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
var err error
defer func() {
if err != nil {
m.orderStore.commsManager.PushEvent(base.Event{
Type: "order",
Message: err.Error(),
})
}
}()
if cancel == nil {
err = errors.New("order cancel param is nil")
return err
}
if cancel.Exchange == "" {
err = errors.New("order exchange name is empty")
return err
}
if cancel.ID == "" {
err = errors.New("order id is empty")
return err
}
exch := m.orderStore.exchangeManager.GetExchangeByName(cancel.Exchange)
if exch == nil {
err = ErrExchangeNotFound
return err
}
if cancel.AssetType.String() != "" && !exch.GetAssetTypes().Contains(cancel.AssetType) {
err = errors.New("order asset type not supported by exchange")
return err
}
log.Debugf(log.OrderMgr, "Order manager: Cancelling order ID %v [%+v]",
cancel.ID, cancel)
err = exch.CancelOrder(cancel)
if err != nil {
err = fmt.Errorf("%v - Failed to cancel order: %w", cancel.Exchange, err)
return err
}
var od *order.Detail
od, err = m.orderStore.getByExchangeAndID(cancel.Exchange, cancel.ID)
if err != nil {
err = fmt.Errorf("%v - Failed to retrieve order %v to update cancelled status: %w", cancel.Exchange, cancel.ID, err)
return err
}
od.Status = order.Cancelled
msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.",
od.Exchange, od.ID)
log.Debugln(log.OrderMgr, msg)
m.orderStore.commsManager.PushEvent(base.Event{
Type: "order",
Message: msg,
})
return nil
}
// GetOrderInfo calls the exchange's wrapper GetOrderInfo function
// and stores the result in the order manager
func (m *OrderManager) GetOrderInfo(exchangeName, orderID string, cp currency.Pair, a asset.Item) (order.Detail, error) {
if m == nil {
return order.Detail{}, fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return order.Detail{}, fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
if orderID == "" {
return order.Detail{}, ErrOrderIDCannotBeEmpty
}
exch := m.orderStore.exchangeManager.GetExchangeByName(exchangeName)
if exch == nil {
return order.Detail{}, ErrExchangeNotFound
}
result, err := exch.GetOrderInfo(orderID, cp, a)
if err != nil {
return order.Detail{}, err
}
err = m.orderStore.add(&result)
if err != nil && err != ErrOrdersAlreadyExists {
return order.Detail{}, err
}
return result, nil
}
// validate ensures a submitted order is valid before adding to the manager
func (m *OrderManager) validate(newOrder *order.Submit) error {
if newOrder == nil {
return errors.New("order cannot be nil")
}
if newOrder.Exchange == "" {
return errors.New("order exchange name must be specified")
}
if err := newOrder.Validate(); err != nil {
return fmt.Errorf("order manager: %w", err)
}
if m.cfg.EnforceLimitConfig {
if !m.cfg.AllowMarketOrders && newOrder.Type == order.Market {
return errors.New("order market type is not allowed")
}
if m.cfg.LimitAmount > 0 && newOrder.Amount > m.cfg.LimitAmount {
return errors.New("order limit exceeds allowed limit")
}
if len(m.cfg.AllowedExchanges) > 0 &&
!common.StringDataCompareInsensitive(m.cfg.AllowedExchanges, newOrder.Exchange) {
return errors.New("order exchange not found in allowed list")
}
if len(m.cfg.AllowedPairs) > 0 && !m.cfg.AllowedPairs.Contains(newOrder.Pair, true) {
return errors.New("order pair not found in allowed list")
}
}
return nil
}
// Submit will take in an order struct, send it to the exchange and
// populate it in the OrderManager if successful
func (m *OrderManager) Submit(newOrder *order.Submit) (*OrderSubmitResponse, error) {
if m == nil {
return nil, fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
err := m.validate(newOrder)
if err != nil {
return nil, err
}
exch := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
// Checks for exchange min max limits for order amounts before order
// execution can occur
err = exch.CheckOrderExecutionLimits(newOrder.AssetType,
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.Type)
if err != nil {
return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w",
newOrder.Exchange,
err)
}
result, err := exch.SubmitOrder(newOrder)
if err != nil {
return nil, err
}
return m.processSubmittedOrder(newOrder, result)
}
// SubmitFakeOrder runs through the same process as order submission
// but does not touch live endpoints
func (m *OrderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse, checkExchangeLimits bool) (*OrderSubmitResponse, error) {
if m == nil {
return nil, fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
err := m.validate(newOrder)
if err != nil {
return nil, err
}
exch := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
if checkExchangeLimits {
// Checks for exchange min max limits for order amounts before order
// execution can occur
err = exch.CheckOrderExecutionLimits(newOrder.AssetType,
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.Type)
if err != nil {
return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w",
newOrder.Exchange,
err)
}
}
return m.processSubmittedOrder(newOrder, resultingOrder)
}
// GetOrdersSnapshot returns a snapshot of all orders in the orderstore. It optionally filters any orders that do not match the status
// but a status of "" or ANY will include all
// the time adds contexts for the when the snapshot is relevant for
func (m *OrderManager) GetOrdersSnapshot(s order.Status) ([]order.Detail, time.Time) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return nil, time.Time{}
}
var os []order.Detail
var latestUpdate time.Time
for _, v := range m.orderStore.Orders {
for i := range v {
if s != v[i].Status &&
s != order.AnyStatus &&
s != "" {
continue
}
if v[i].LastUpdated.After(latestUpdate) {
latestUpdate = v[i].LastUpdated
}
cpy := *v[i]
os = append(os, cpy)
}
}
return os, latestUpdate
}
// processSubmittedOrder adds a new order to the manager
func (m *OrderManager) processSubmittedOrder(newOrder *order.Submit, result order.SubmitResponse) (*OrderSubmitResponse, error) {
if !result.IsOrderPlaced {
return nil, errors.New("order unable to be placed")
}
id, err := uuid.NewV4()
if err != nil {
log.Warnf(log.OrderMgr,
"Order manager: Unable to generate UUID. Err: %s",
err)
}
msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v for time %v.",
newOrder.Exchange,
result.OrderID,
id.String(),
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.Side,
newOrder.Type,
newOrder.Date)
log.Debugln(log.OrderMgr, msg)
m.orderStore.commsManager.PushEvent(base.Event{
Type: "order",
Message: msg,
})
status := order.New
if result.FullyMatched {
status = order.Filled
}
err = m.orderStore.add(&order.Detail{
ImmediateOrCancel: newOrder.ImmediateOrCancel,
HiddenOrder: newOrder.HiddenOrder,
FillOrKill: newOrder.FillOrKill,
PostOnly: newOrder.PostOnly,
Price: newOrder.Price,
Amount: newOrder.Amount,
LimitPriceUpper: newOrder.LimitPriceUpper,
LimitPriceLower: newOrder.LimitPriceLower,
TriggerPrice: newOrder.TriggerPrice,
TargetAmount: newOrder.TargetAmount,
ExecutedAmount: newOrder.ExecutedAmount,
RemainingAmount: newOrder.RemainingAmount,
Fee: newOrder.Fee,
Exchange: newOrder.Exchange,
InternalOrderID: id.String(),
ID: result.OrderID,
AccountID: newOrder.AccountID,
ClientID: newOrder.ClientID,
WalletAddress: newOrder.WalletAddress,
Type: newOrder.Type,
Side: newOrder.Side,
Status: status,
AssetType: newOrder.AssetType,
Date: time.Now(),
LastUpdated: time.Now(),
Pair: newOrder.Pair,
Leverage: newOrder.Leverage,
})
if err != nil {
return nil, fmt.Errorf("unable to add %v order %v to orderStore: %s", newOrder.Exchange, result.OrderID, err)
}
return &OrderSubmitResponse{
SubmitResponse: order.SubmitResponse{
IsOrderPlaced: result.IsOrderPlaced,
OrderID: result.OrderID,
},
InternalOrderID: id.String(),
}, nil
}
// processOrders iterates over all exchange orders via API
// and adds them to the internal order store
func (m *OrderManager) processOrders() {
exchanges := m.orderStore.exchangeManager.GetExchanges()
for i := range exchanges {
if !exchanges[i].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
continue
}
log.Debugf(log.OrderMgr,
"Order manager: Processing orders for exchange %v.",
exchanges[i].GetName())
supportedAssets := exchanges[i].GetAssetTypes()
for y := range supportedAssets {
pairs, err := exchanges[i].GetEnabledPairs(supportedAssets[y])
if err != nil {
log.Errorf(log.OrderMgr,
"Order manager: Unable to get enabled pairs for %s and asset type %s: %s",
exchanges[i].GetName(),
supportedAssets[y],
err)
continue
}
if len(pairs) == 0 {
if m.verbose {
log.Debugf(log.OrderMgr,
"Order manager: No pairs enabled for %s and asset type %s, skipping...",
exchanges[i].GetName(),
supportedAssets[y])
}
continue
}
req := order.GetOrdersRequest{
Side: order.AnySide,
Type: order.AnyType,
Pairs: pairs,
AssetType: supportedAssets[y],
}
result, err := exchanges[i].GetActiveOrders(&req)
if err != nil {
log.Warnf(log.OrderMgr,
"Order manager: Unable to get active orders for %s and asset type %s: %s",
exchanges[i].GetName(),
supportedAssets[y],
err)
continue
}
for z := range result {
ord := &result[z]
result := m.orderStore.add(ord)
if result != ErrOrdersAlreadyExists {
msg := fmt.Sprintf("Order manager: Exchange %s added order ID=%v pair=%v price=%v amount=%v side=%v type=%v.",
ord.Exchange, ord.ID, ord.Pair, ord.Price, ord.Amount, ord.Side, ord.Type)
log.Debugf(log.OrderMgr, "%v", msg)
m.orderStore.commsManager.PushEvent(base.Event{
Type: "order",
Message: msg,
})
continue
}
}
}
}
}
// Exists checks whether an order exists in the order store
func (m *OrderManager) Exists(o *order.Detail) bool {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return false
}
return m.orderStore.exists(o)
}
// Add adds an order to the orderstore
func (m *OrderManager) Add(o *order.Detail) error {
if m == nil {
return fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
return m.orderStore.add(o)
}
// GetByExchangeAndID returns a copy of an order from an exchange if it matches the ID
func (m *OrderManager) GetByExchangeAndID(exchangeName, id string) (*order.Detail, error) {
if m == nil {
return nil, fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
o, err := m.orderStore.getByExchangeAndID(exchangeName, id)
if err != nil {
return nil, err
}
var cpy order.Detail
cpy.UpdateOrderFromDetail(o)
return &cpy, nil
}
// UpdateExistingOrder will update an existing order in the orderstore
func (m *OrderManager) UpdateExistingOrder(od *order.Detail) error {
if m == nil {
return fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
return m.orderStore.updateExisting(od)
}
// UpsertOrder updates an existing order or adds a new one to the orderstore
func (m *OrderManager) UpsertOrder(od *order.Detail) error {
if m == nil {
return fmt.Errorf("order manager %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
return m.orderStore.upsert(od)
}
// get returns all orders for all exchanges
// should not be exported as it can have large impact if used improperly
func (s *store) get() map[string][]*order.Detail {
s.m.Lock()
orders := s.Orders
s.m.Unlock()
return orders
}
// getByExchangeAndID returns a specific order by exchange and id
func (s *store) getByExchangeAndID(exchange, id string) (*order.Detail, error) {
s.m.Lock()
defer s.m.Unlock()
r, ok := s.Orders[strings.ToLower(exchange)]
if !ok {
return nil, ErrExchangeNotFound
}
for x := range r {
if r[x].ID == id {
return r[x], nil
}
}
return nil, ErrOrderNotFound
}
// updateExisting checks if an order exists in the orderstore
// and then updates it
func (s *store) updateExisting(od *order.Detail) error {
s.m.Lock()
defer s.m.Unlock()
r, ok := s.Orders[strings.ToLower(od.Exchange)]
if !ok {
return ErrExchangeNotFound
}
for x := range r {
if r[x].ID == od.ID {
r[x].UpdateOrderFromDetail(od)
return nil
}
}
return ErrOrderNotFound
}
func (s *store) upsert(od *order.Detail) error {
lName := strings.ToLower(od.Exchange)
exch := s.exchangeManager.GetExchangeByName(lName)
if exch == nil {
return ErrExchangeNotFound
}
s.m.Lock()
defer s.m.Unlock()
r, ok := s.Orders[lName]
if !ok {
s.Orders[lName] = []*order.Detail{od}
return nil
}
for x := range r {
if r[x].ID == od.ID {
r[x].UpdateOrderFromDetail(od)
return nil
}
}
s.Orders[lName] = append(s.Orders[lName], od)
return nil
}
// getByExchange returns orders by exchange
func (s *store) getByExchange(exchange string) ([]*order.Detail, error) {
s.m.Lock()
defer s.m.Unlock()
r, ok := s.Orders[strings.ToLower(exchange)]
if !ok {
return nil, ErrExchangeNotFound
}
return r, nil
}
// getByInternalOrderID will search all orders for our internal orderID
// and return the order
func (s *store) getByInternalOrderID(internalOrderID string) (*order.Detail, error) {
s.m.Lock()
defer s.m.Unlock()
for _, v := range s.Orders {
for x := range v {
if v[x].InternalOrderID == internalOrderID {
return v[x], nil
}
}
}
return nil, ErrOrderNotFound
}
// exists verifies if the orderstore contains the provided order
func (s *store) exists(det *order.Detail) bool {
if det == nil {
return false
}
s.m.Lock()
defer s.m.Unlock()
r, ok := s.Orders[strings.ToLower(det.Exchange)]
if !ok {
return false
}
for x := range r {
if r[x].ID == det.ID {
return true
}
}
return false
}
// Add Adds an order to the orderStore for tracking the lifecycle
func (s *store) add(det *order.Detail) error {
if det == nil {
return errors.New("order store: Order is nil")
}
exch := s.exchangeManager.GetExchangeByName(det.Exchange)
if exch == nil {
return ErrExchangeNotFound
}
if s.exists(det) {
return ErrOrdersAlreadyExists
}
// Untracked websocket orders will not have internalIDs yet
if det.InternalOrderID == "" {
id, err := uuid.NewV4()
if err != nil {
log.Warnf(log.OrderMgr,
"Order manager: Unable to generate UUID. Err: %s",
err)
} else {
det.InternalOrderID = id.String()
}
}
s.m.Lock()
defer s.m.Unlock()
orders := s.Orders[strings.ToLower(det.Exchange)]
orders = append(orders, det)
s.Orders[strings.ToLower(det.Exchange)] = orders
return nil
}

45
engine/order_manager.md Normal file
View File

@@ -0,0 +1,45 @@
# GoCryptoTrader package Order_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/order_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This order_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Order_manager
+ The order manager subsystem stores and monitors all orders from enabled exchanges with API keys and `authenticatedSupport` enabled
+ It can be enabled or disabled via runtime command `-ordermanager=false` and defaults to true
+ All orders placed via GoCryptoTrader will be added to the order manager store
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,561 @@
package engine
import (
"errors"
"sync"
"testing"
"time"
"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"
)
// omfExchange aka ordermanager fake exchange overrides exchange functions
// we're not testing an actual exchange's implemented functions
type omfExchange struct {
exchange.IBotExchange
}
// CancelOrder overrides testExchange's cancel order function
// to do the bare minimum required with no API calls or credentials required
func (f omfExchange) CancelOrder(o *order.Cancel) error {
o.Status = order.Cancelled
return nil
}
// GetOrderInfo overrides testExchange's get order function
// to do the bare minimum required with no API calls or credentials required
func (f omfExchange) GetOrderInfo(orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) {
if orderID == "" {
return order.Detail{}, errors.New("")
}
return order.Detail{
Exchange: testExchange,
ID: orderID,
Pair: pair,
AssetType: assetType,
}, nil
}
func TestSetupOrderManager(t *testing.T) {
_, err := SetupOrderManager(nil, nil, nil, false)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
_, err = SetupOrderManager(SetupExchangeManager(), nil, nil, false)
if !errors.Is(err, errNilCommunicationsManager) {
t.Errorf("error '%v', expected '%v'", err, errNilCommunicationsManager)
}
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, nil, false)
if !errors.Is(err, errNilWaitGroup) {
t.Errorf("error '%v', expected '%v'", err, errNilWaitGroup)
}
var wg sync.WaitGroup
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestOrderManagerStart(t *testing.T) {
var m *OrderManager
err := m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
var wg sync.WaitGroup
m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
}
func TestOrderManagerIsRunning(t *testing.T) {
var m *OrderManager
if m.IsRunning() {
t.Error("expected false")
}
var wg sync.WaitGroup
m, err := SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.IsRunning() {
t.Error("expected false")
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.IsRunning() {
t.Error("expected true")
}
}
func TestOrderManagerStop(t *testing.T) {
var m *OrderManager
err := m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
var wg sync.WaitGroup
m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func OrdersSetup(t *testing.T) *OrderManager {
var wg sync.WaitGroup
em := SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Error(err)
}
exch.SetDefaults()
fakeExchange := omfExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
m, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
return m
}
func TestOrdersGet(t *testing.T) {
m := OrdersSetup(t)
if m.orderStore.get() == nil {
t.Error("orderStore not established")
}
}
func TestOrdersAdd(t *testing.T) {
m := OrdersSetup(t)
err := m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestOrdersAdd",
})
if err != nil {
t.Error(err)
}
err = m.orderStore.add(&order.Detail{
Exchange: "testTest",
ID: "TestOrdersAdd",
})
if err == nil {
t.Error("Expected error from non existent exchange")
}
err = m.orderStore.add(nil)
if err == nil {
t.Error("Expected error from nil order")
}
err = m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestOrdersAdd",
})
if err == nil {
t.Error("Expected error re-adding order")
}
}
func TestGetByInternalOrderID(t *testing.T) {
m := OrdersSetup(t)
err := m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByInternalOrderID",
InternalOrderID: "internalTest",
})
if err != nil {
t.Error(err)
}
o, err := m.orderStore.getByInternalOrderID("internalTest")
if err != nil {
t.Error(err)
}
if o == nil {
t.Fatal("Expected a matching order")
}
if o.ID != "TestGetByInternalOrderID" {
t.Error("Expected to retrieve order")
}
_, err = m.orderStore.getByInternalOrderID("NoOrder")
if err != ErrOrderNotFound {
t.Error(err)
}
}
func TestGetByExchange(t *testing.T) {
m := OrdersSetup(t)
err := m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchange",
InternalOrderID: "internalTestGetByExchange",
})
if err != nil {
t.Error(err)
}
err = m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchange2",
InternalOrderID: "internalTestGetByExchange2",
})
if err != nil {
t.Error(err)
}
err = m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchange3",
InternalOrderID: "internalTest3",
})
if err != nil {
t.Error(err)
}
var o []*order.Detail
o, err = m.orderStore.getByExchange(testExchange)
if err != nil {
t.Error(err)
}
if o == nil {
t.Error("Expected non nil response")
}
var o1Found, o2Found bool
for i := range o {
if o[i].ID == "TestGetByExchange" && o[i].Exchange == testExchange {
o1Found = true
}
if o[i].ID == "TestGetByExchange2" && o[i].Exchange == testExchange {
o2Found = true
}
}
if !o1Found || !o2Found {
t.Error("Expected orders 'TestGetByExchange' and 'TestGetByExchange2' to be returned")
}
_, err = m.orderStore.getByInternalOrderID("NoOrder")
if err != ErrOrderNotFound {
t.Error(err)
}
err = m.orderStore.add(&order.Detail{
Exchange: "thisWillFail",
})
if err == nil {
t.Error("Expected exchange not found error")
}
}
func TestGetByExchangeAndID(t *testing.T) {
m := OrdersSetup(t)
err := m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchangeAndID",
})
if err != nil {
t.Error(err)
}
o, err := m.orderStore.getByExchangeAndID(testExchange, "TestGetByExchangeAndID")
if err != nil {
t.Error(err)
}
if o.ID != "TestGetByExchangeAndID" {
t.Error("Expected to retrieve order")
}
_, err = m.orderStore.getByExchangeAndID("", "TestGetByExchangeAndID")
if err != ErrExchangeNotFound {
t.Error(err)
}
_, err = m.orderStore.getByExchangeAndID(testExchange, "")
if err != ErrOrderNotFound {
t.Error(err)
}
}
func TestExists(t *testing.T) {
m := OrdersSetup(t)
if m.orderStore.exists(nil) {
t.Error("Expected false")
}
o := &order.Detail{
Exchange: testExchange,
ID: "TestExists",
}
err := m.orderStore.add(o)
if err != nil {
t.Error(err)
}
b := m.orderStore.exists(o)
if !b {
t.Error("Expected true")
}
}
func TestCancelOrder(t *testing.T) {
m := OrdersSetup(t)
err := m.Cancel(nil)
if err == nil {
t.Error("Expected error due to empty order")
}
err = m.Cancel(&order.Cancel{})
if err == nil {
t.Error("Expected error due to empty order")
}
err = m.Cancel(&order.Cancel{
Exchange: testExchange,
})
if err == nil {
t.Error("Expected error due to no order ID")
}
err = m.Cancel(&order.Cancel{
ID: "ID",
})
if err == nil {
t.Error("Expected error due to no Exchange")
}
err = m.Cancel(&order.Cancel{
ID: "ID",
Exchange: testExchange,
AssetType: asset.Binary,
})
if err == nil {
t.Error("Expected error due to bad asset type")
}
o := &order.Detail{
Exchange: testExchange,
ID: "1337",
Status: order.New,
}
err = m.orderStore.add(o)
if err != nil {
t.Error(err)
}
err = m.Cancel(&order.Cancel{
ID: "Unknown",
Exchange: testExchange,
AssetType: asset.Spot,
})
if err == nil {
t.Error("Expected error due to no order found")
}
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
cancel := &order.Cancel{
Exchange: testExchange,
ID: "1337",
Side: order.Sell,
Status: order.New,
AssetType: asset.Spot,
Date: time.Now(),
Pair: pair,
}
err = m.Cancel(cancel)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if o.Status != order.Cancelled {
t.Error("Failed to cancel")
}
}
func TestGetOrderInfo(t *testing.T) {
m := OrdersSetup(t)
_, err := m.GetOrderInfo("", "", currency.Pair{}, "")
if err == nil {
t.Error("Expected error due to empty order")
}
var result order.Detail
result, err = m.GetOrderInfo(testExchange, "1337", currency.Pair{}, "")
if err != nil {
t.Error(err)
}
if result.ID != "1337" {
t.Error("unexpected order returned")
}
result, err = m.GetOrderInfo(testExchange, "1337", currency.Pair{}, "")
if err != nil {
t.Error(err)
}
if result.ID != "1337" {
t.Error("unexpected order returned")
}
}
func TestCancelAllOrders(t *testing.T) {
m := OrdersSetup(t)
o := &order.Detail{
Exchange: testExchange,
ID: "TestCancelAllOrders",
Status: order.New,
}
err := m.orderStore.add(o)
if err != nil {
t.Error(err)
}
exch := m.orderStore.exchangeManager.GetExchangeByName(testExchange)
m.CancelAllOrders([]exchange.IBotExchange{})
if o.Status == order.Cancelled {
t.Error("Order should not be cancelled")
}
m.CancelAllOrders([]exchange.IBotExchange{exch})
if o.Status != order.Cancelled {
t.Error("Order should be cancelled")
}
o.Status = order.New
m.CancelAllOrders(nil)
if o.Status != order.New {
t.Error("Order should not be cancelled")
}
}
func TestSubmit(t *testing.T) {
m := OrdersSetup(t)
_, err := m.Submit(nil)
if err == nil {
t.Error("Expected error from nil order")
}
o := &order.Submit{
Exchange: "",
ID: "FakePassingExchangeOrder",
Status: order.New,
Type: order.Market,
}
_, err = m.Submit(o)
if err == nil {
t.Error("Expected error from empty exchange")
}
o.Exchange = testExchange
_, err = m.Submit(o)
if err == nil {
t.Error("Expected error from validation")
}
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
m.cfg.EnforceLimitConfig = true
m.cfg.AllowMarketOrders = false
o.Pair = pair
o.AssetType = asset.Spot
o.Side = order.Buy
o.Amount = 1
o.Price = 1
_, err = m.Submit(o)
if err == nil {
t.Error("Expected fail due to order market type is not allowed")
}
m.cfg.AllowMarketOrders = true
m.cfg.LimitAmount = 1
o.Amount = 2
_, err = m.Submit(o)
if err == nil {
t.Error("Expected fail due to order limit exceeds allowed limit")
}
m.cfg.LimitAmount = 0
m.cfg.AllowedExchanges = []string{"fake"}
_, err = m.Submit(o)
if err == nil {
t.Error("Expected fail due to order exchange not found in allowed list")
}
failPair, err := currency.NewPairFromString("BTCAUD")
if err != nil {
t.Fatal(err)
}
m.cfg.AllowedExchanges = nil
m.cfg.AllowedPairs = currency.Pairs{failPair}
_, err = m.Submit(o)
if err == nil {
t.Error("Expected fail due to order pair not found in allowed list")
}
m.cfg.AllowedPairs = nil
_, err = m.Submit(o)
if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) {
t.Errorf("error '%v', expected '%v'", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
err = m.orderStore.add(&order.Detail{
Exchange: testExchange,
ID: "FakePassingExchangeOrder",
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
o2, err := m.orderStore.getByExchangeAndID(testExchange, "FakePassingExchangeOrder")
if err != nil {
t.Error(err)
}
if o2.InternalOrderID == "" {
t.Error("Failed to assign internal order id")
}
}
func TestProcessOrders(t *testing.T) {
m := OrdersSetup(t)
m.processOrders()
}

View File

@@ -0,0 +1,59 @@
package engine
import (
"errors"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// OrderManagerName is an exported subsystem name
const OrderManagerName = "orders"
// vars for the fund manager package
var (
orderManagerDelay = time.Second * 10
// ErrOrdersAlreadyExists occurs when the order already exists in the manager
ErrOrdersAlreadyExists = errors.New("order already exists")
// ErrOrderNotFound occurs when an order is not found in the orderstore
ErrOrderNotFound = errors.New("order does not exist")
errNilCommunicationsManager = errors.New("cannot start with nil communications manager")
// ErrOrderIDCannotBeEmpty occurs when an order does not have an ID
ErrOrderIDCannotBeEmpty = errors.New("orderID cannot be empty")
)
type orderManagerConfig struct {
EnforceLimitConfig bool
AllowMarketOrders bool
CancelOrdersOnShutdown bool
LimitAmount float64
AllowedPairs currency.Pairs
AllowedExchanges []string
OrderSubmissionRetries int64
}
// store holds all orders by exchange
type store struct {
m sync.Mutex
Orders map[string][]*order.Detail
commsManager iCommsManager
exchangeManager iExchangeManager
wg *sync.WaitGroup
}
// OrderManager processes and stores orders across enabled exchanges
type OrderManager struct {
started int32
shutdown chan struct{}
orderStore store
cfg orderManagerConfig
verbose bool
}
// OrderSubmitResponse contains the order response along with an internal order ID
type OrderSubmitResponse struct {
order.SubmitResponse
InternalOrderID string
}

View File

@@ -1,573 +0,0 @@
package engine
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine/subsystem"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// vars for the fund manager package
var (
orderManagerDelay = time.Second * 10
ErrOrdersAlreadyExists = errors.New("order already exists")
ErrOrderNotFound = errors.New("order does not exist")
)
// get returns all orders for all exchanges
// should not be exported as it can have large impact if used improperly
func (o *orderStore) get() map[string][]*order.Detail {
o.m.RLock()
orders := o.Orders
o.m.RUnlock()
return orders
}
// GetByExchangeAndID returns a specific order by exchange and id
func (o *orderStore) GetByExchangeAndID(exchange, id string) (*order.Detail, error) {
o.m.RLock()
defer o.m.RUnlock()
r, ok := o.Orders[strings.ToLower(exchange)]
if !ok {
return nil, ErrExchangeNotFound
}
for x := range r {
if r[x].ID == id {
return r[x], nil
}
}
return nil, ErrOrderNotFound
}
// GetByExchange returns orders by exchange
func (o *orderStore) GetByExchange(exchange string) ([]*order.Detail, error) {
o.m.RLock()
defer o.m.RUnlock()
r, ok := o.Orders[strings.ToLower(exchange)]
if !ok {
return nil, ErrExchangeNotFound
}
return r, nil
}
// GetByInternalOrderID will search all orders for our internal orderID
// and return the order
func (o *orderStore) GetByInternalOrderID(internalOrderID string) (*order.Detail, error) {
o.m.RLock()
defer o.m.RUnlock()
for _, v := range o.Orders {
for x := range v {
if v[x].InternalOrderID == internalOrderID {
return v[x], nil
}
}
}
return nil, ErrOrderNotFound
}
func (o *orderStore) exists(det *order.Detail) bool {
if det == nil {
return false
}
o.m.RLock()
defer o.m.RUnlock()
r, ok := o.Orders[strings.ToLower(det.Exchange)]
if !ok {
return false
}
for x := range r {
if r[x].ID == det.ID {
return true
}
}
return false
}
// Add Adds an order to the orderStore for tracking the lifecycle
func (o *orderStore) Add(det *order.Detail) error {
if det == nil {
return errors.New("order store: Order is nil")
}
exch := o.bot.GetExchangeByName(det.Exchange)
if exch == nil {
return ErrExchangeNotFound
}
if o.exists(det) {
return ErrOrdersAlreadyExists
}
// Untracked websocket orders will not have internalIDs yet
if det.InternalOrderID == "" {
id, err := uuid.NewV4()
if err != nil {
log.Warnf(log.OrderMgr,
"Order manager: Unable to generate UUID. Err: %s",
err)
} else {
det.InternalOrderID = id.String()
}
}
o.m.Lock()
defer o.m.Unlock()
orders := o.Orders[strings.ToLower(det.Exchange)]
orders = append(orders, det)
o.Orders[strings.ToLower(det.Exchange)] = orders
return nil
}
// Started returns the status of the orderManager
func (o *orderManager) Started() bool {
return atomic.LoadInt32(&o.started) == 1
}
// Start will boot up the orderManager
func (o *orderManager) Start(bot *Engine) error {
if bot == nil {
return errors.New("cannot start with nil bot")
}
if !atomic.CompareAndSwapInt32(&o.started, 0, 1) {
return fmt.Errorf("order manager %w", subsystem.ErrSubSystemAlreadyStarted)
}
log.Debugln(log.OrderBook, "Order manager starting...")
o.shutdown = make(chan struct{})
o.orderStore.Orders = make(map[string][]*order.Detail)
o.orderStore.bot = bot
go o.run()
return nil
}
// Stop will attempt to shutdown the orderManager
func (o *orderManager) Stop() error {
if atomic.LoadInt32(&o.started) == 0 {
return fmt.Errorf("order manager %w", subsystem.ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&o.started, 1, 0)
}()
log.Debugln(log.OrderBook, "Order manager shutting down...")
close(o.shutdown)
return nil
}
func (o *orderManager) gracefulShutdown() {
if o.cfg.CancelOrdersOnShutdown {
log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...")
o.CancelAllOrders(o.orderStore.bot.Config.GetEnabledExchanges())
}
}
func (o *orderManager) run() {
log.Debugln(log.OrderBook, "Order manager started.")
tick := time.NewTicker(orderManagerDelay)
o.orderStore.bot.ServicesWG.Add(1)
defer func() {
log.Debugln(log.OrderMgr, "Order manager shutdown.")
tick.Stop()
o.orderStore.bot.ServicesWG.Done()
}()
for {
select {
case <-o.shutdown:
o.gracefulShutdown()
return
case <-tick.C:
go o.processOrders()
}
}
}
// CancelAllOrders iterates and cancels all orders for each exchange provided
func (o *orderManager) CancelAllOrders(exchangeNames []string) {
orders := o.orderStore.get()
if orders == nil {
return
}
for k, v := range orders {
log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.", k)
if !common.StringDataCompareInsensitive(exchangeNames, k) {
continue
}
for y := range v {
err := o.Cancel(&order.Cancel{
Exchange: k,
ID: v[y].ID,
AccountID: v[y].AccountID,
ClientID: v[y].ClientID,
WalletAddress: v[y].WalletAddress,
Type: v[y].Type,
Side: v[y].Side,
Pair: v[y].Pair,
AssetType: v[y].AssetType,
})
if err != nil {
log.Error(log.OrderMgr, err)
continue
}
}
}
}
// Cancel will find the order in the orderManager, send a cancel request
// to the exchange and if successful, update the status of the order
func (o *orderManager) Cancel(cancel *order.Cancel) error {
var err error
defer func() {
if err != nil {
o.orderStore.bot.CommsManager.PushEvent(base.Event{
Type: "order",
Message: err.Error(),
})
}
}()
if cancel == nil {
err = errors.New("order cancel param is nil")
return err
}
if cancel.Exchange == "" {
err = errors.New("order exchange name is empty")
return err
}
if cancel.ID == "" {
err = errors.New("order id is empty")
return err
}
exch := o.orderStore.bot.GetExchangeByName(cancel.Exchange)
if exch == nil {
err = ErrExchangeNotFound
return err
}
if cancel.AssetType.String() != "" && !exch.GetAssetTypes().Contains(cancel.AssetType) {
err = errors.New("order asset type not supported by exchange")
return err
}
log.Debugf(log.OrderMgr, "Order manager: Cancelling order ID %v [%+v]",
cancel.ID, cancel)
err = exch.CancelOrder(cancel)
if err != nil {
err = fmt.Errorf("%v - Failed to cancel order: %v", cancel.Exchange, err)
return err
}
var od *order.Detail
od, err = o.orderStore.GetByExchangeAndID(cancel.Exchange, cancel.ID)
if err != nil {
err = fmt.Errorf("%v - Failed to retrieve order %v to update cancelled status: %v", cancel.Exchange, cancel.ID, err)
return err
}
od.Status = order.Cancelled
msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.",
od.Exchange, od.ID)
log.Debugln(log.OrderMgr, msg)
o.orderStore.bot.CommsManager.PushEvent(base.Event{
Type: "order",
Message: msg,
})
return nil
}
// GetOrderInfo calls the exchange's wrapper GetOrderInfo function
// and stores the result in the order manager
func (o *orderManager) GetOrderInfo(exchangeName, orderID string, cp currency.Pair, a asset.Item) (order.Detail, error) {
if orderID == "" {
return order.Detail{}, errOrderIDCannotBeEmpty
}
exch := o.orderStore.bot.GetExchangeByName(exchangeName)
if exch == nil {
return order.Detail{}, ErrExchangeNotFound
}
result, err := exch.GetOrderInfo(orderID, cp, a)
if err != nil {
return order.Detail{}, err
}
err = o.orderStore.Add(&result)
if err != nil && err != ErrOrdersAlreadyExists {
return order.Detail{}, err
}
return result, nil
}
func (o *orderManager) validate(newOrder *order.Submit) error {
if newOrder == nil {
return errors.New("order cannot be nil")
}
if newOrder.Exchange == "" {
return errors.New("order exchange name must be specified")
}
if err := newOrder.Validate(); err != nil {
return fmt.Errorf("order manager: %w", err)
}
if o.cfg.EnforceLimitConfig {
if !o.cfg.AllowMarketOrders && newOrder.Type == order.Market {
return errors.New("order market type is not allowed")
}
if o.cfg.LimitAmount > 0 && newOrder.Amount > o.cfg.LimitAmount {
return errors.New("order limit exceeds allowed limit")
}
if len(o.cfg.AllowedExchanges) > 0 &&
!common.StringDataCompareInsensitive(o.cfg.AllowedExchanges, newOrder.Exchange) {
return errors.New("order exchange not found in allowed list")
}
if len(o.cfg.AllowedPairs) > 0 && !o.cfg.AllowedPairs.Contains(newOrder.Pair, true) {
return errors.New("order pair not found in allowed list")
}
}
return nil
}
// Submit will take in an order struct, send it to the exchange and
// populate it in the orderManager if successful
func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, error) {
err := o.validate(newOrder)
if err != nil {
return nil, err
}
exch := o.orderStore.bot.GetExchangeByName(newOrder.Exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
// Checks for exchange min max limits for order amounts before order
// execution can occur
err = exch.CheckOrderExecutionLimits(newOrder.AssetType,
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.Type)
if err != nil {
return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w",
newOrder.Exchange,
err)
}
result, err := exch.SubmitOrder(newOrder)
if err != nil {
return nil, err
}
return o.processSubmittedOrder(newOrder, result)
}
// SubmitFakeOrder runs through the same process as order submission
// but does not touch live endpoints
func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse, checkExchangeLimits bool) (*orderSubmitResponse, error) {
err := o.validate(newOrder)
if err != nil {
return nil, err
}
exch := o.orderStore.bot.GetExchangeByName(newOrder.Exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
if checkExchangeLimits {
// Checks for exchange min max limits for order amounts before order
// execution can occur
err = exch.CheckOrderExecutionLimits(newOrder.AssetType,
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.Type)
if err != nil {
return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w",
newOrder.Exchange,
err)
}
}
return o.processSubmittedOrder(newOrder, resultingOrder)
}
// GetOrdersSnapshot returns a snapshot of all orders in the orderstore. It optionally filters any orders that do not match the status
// but a status of "" or ANY will include all
// the time adds contexts for the when the snapshot is relevant for
func (o *orderManager) GetOrdersSnapshot(s order.Status) ([]order.Detail, time.Time) {
var os []order.Detail
var latestUpdate time.Time
for _, v := range o.orderStore.Orders {
for i := range v {
if s != v[i].Status &&
s != order.AnyStatus &&
s != "" {
continue
}
if v[i].LastUpdated.After(latestUpdate) {
latestUpdate = v[i].LastUpdated
}
cpy := *v[i]
os = append(os, cpy)
}
}
return os, latestUpdate
}
func (o *orderManager) processSubmittedOrder(newOrder *order.Submit, result order.SubmitResponse) (*orderSubmitResponse, error) {
if !result.IsOrderPlaced {
return nil, errors.New("order unable to be placed")
}
id, err := uuid.NewV4()
if err != nil {
log.Warnf(log.OrderMgr,
"Order manager: Unable to generate UUID. Err: %s",
err)
}
msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v for time %v.",
newOrder.Exchange,
result.OrderID,
id.String(),
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.Side,
newOrder.Type,
newOrder.Date)
log.Debugln(log.OrderMgr, msg)
o.orderStore.bot.CommsManager.PushEvent(base.Event{
Type: "order",
Message: msg,
})
status := order.New
if result.FullyMatched {
status = order.Filled
}
err = o.orderStore.Add(&order.Detail{
ImmediateOrCancel: newOrder.ImmediateOrCancel,
HiddenOrder: newOrder.HiddenOrder,
FillOrKill: newOrder.FillOrKill,
PostOnly: newOrder.PostOnly,
Price: newOrder.Price,
Amount: newOrder.Amount,
LimitPriceUpper: newOrder.LimitPriceUpper,
LimitPriceLower: newOrder.LimitPriceLower,
TriggerPrice: newOrder.TriggerPrice,
TargetAmount: newOrder.TargetAmount,
ExecutedAmount: newOrder.ExecutedAmount,
RemainingAmount: newOrder.RemainingAmount,
Fee: newOrder.Fee,
Exchange: newOrder.Exchange,
InternalOrderID: id.String(),
ID: result.OrderID,
AccountID: newOrder.AccountID,
ClientID: newOrder.ClientID,
WalletAddress: newOrder.WalletAddress,
Type: newOrder.Type,
Side: newOrder.Side,
Status: status,
AssetType: newOrder.AssetType,
Date: time.Now(),
LastUpdated: time.Now(),
Pair: newOrder.Pair,
Leverage: newOrder.Leverage,
})
if err != nil {
return nil, fmt.Errorf("unable to add %v order %v to orderStore: %s", newOrder.Exchange, result.OrderID, err)
}
return &orderSubmitResponse{
SubmitResponse: order.SubmitResponse{
IsOrderPlaced: result.IsOrderPlaced,
OrderID: result.OrderID,
},
InternalOrderID: id.String(),
}, nil
}
func (o *orderManager) processOrders() {
authExchanges := o.orderStore.bot.GetAuthAPISupportedExchanges()
for x := range authExchanges {
log.Debugf(log.OrderMgr,
"Order manager: Processing orders for exchange %v.",
authExchanges[x])
exch := o.orderStore.bot.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 o.orderStore.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: pairs,
AssetType: supportedAssets[y],
}
result, err := exch.GetActiveOrders(&req)
if err != nil {
log.Warnf(log.OrderMgr,
"Order manager: Unable to get active orders for %s and asset type %s: %s",
authExchanges[x],
supportedAssets[y],
err)
continue
}
for z := range result {
ord := &result[z]
result := o.orderStore.Add(ord)
if result != ErrOrdersAlreadyExists {
msg := fmt.Sprintf("Order manager: Exchange %s added order ID=%v pair=%v price=%v amount=%v side=%v type=%v.",
ord.Exchange, ord.ID, ord.Pair, ord.Price, ord.Amount, ord.Side, ord.Type)
log.Debugf(log.OrderMgr, "%v", msg)
o.orderStore.bot.CommsManager.PushEvent(base.Event{
Type: "order",
Message: msg,
})
continue
}
}
}
}
}

View File

@@ -1,416 +0,0 @@
package engine
import (
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
func OrdersSetup(t *testing.T) *Engine {
bot := CreateTestBot(t)
err := bot.OrderManager.Start(bot)
if err != nil {
t.Fatal(err)
}
bot.ServicesWG.Wait()
if !bot.OrderManager.Started() {
t.Fatal("Order manager not started")
}
return bot
}
func TestOrdersGet(t *testing.T) {
bot := OrdersSetup(t)
if bot.OrderManager.orderStore.get() == nil {
t.Error("orderStore not established")
}
}
func TestOrdersAdd(t *testing.T) {
bot := OrdersSetup(t)
err := bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: testExchange,
ID: "TestOrdersAdd",
})
if err != nil {
t.Error(err)
}
err = bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: "testTest",
ID: "TestOrdersAdd",
})
if err == nil {
t.Error("Expected error from non existent exchange")
}
err = bot.OrderManager.orderStore.Add(nil)
if err == nil {
t.Error("Expected error from nil order")
}
err = bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: testExchange,
ID: "TestOrdersAdd",
})
if err == nil {
t.Error("Expected error re-adding order")
}
}
func TestGetByInternalOrderID(t *testing.T) {
bot := OrdersSetup(t)
err := bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByInternalOrderID",
InternalOrderID: "internalTest",
})
if err != nil {
t.Error(err)
}
o, err := bot.OrderManager.orderStore.GetByInternalOrderID("internalTest")
if err != nil {
t.Error(err)
}
if o == nil {
t.Fatal("Expected a matching order")
}
if o.ID != "TestGetByInternalOrderID" {
t.Error("Expected to retrieve order")
}
_, err = bot.OrderManager.orderStore.GetByInternalOrderID("NoOrder")
if err != ErrOrderNotFound {
t.Error(err)
}
}
func TestGetByExchange(t *testing.T) {
bot := OrdersSetup(t)
err := bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchange",
InternalOrderID: "internalTestGetByExchange",
})
if err != nil {
t.Error(err)
}
err = bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchange2",
InternalOrderID: "internalTestGetByExchange2",
})
if err != nil {
t.Error(err)
}
err = bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: fakePassExchange,
ID: "TestGetByExchange3",
InternalOrderID: "internalTest3",
})
if err != nil {
t.Error(err)
}
var o []*order.Detail
o, err = bot.OrderManager.orderStore.GetByExchange(testExchange)
if err != nil {
t.Error(err)
}
if o == nil {
t.Error("Expected non nil response")
}
var o1Found, o2Found bool
for i := range o {
if o[i].ID == "TestGetByExchange" && o[i].Exchange == testExchange {
o1Found = true
}
if o[i].ID == "TestGetByExchange2" && o[i].Exchange == testExchange {
o2Found = true
}
}
if !o1Found || !o2Found {
t.Error("Expected orders 'TestGetByExchange' and 'TestGetByExchange2' to be returned")
}
_, err = bot.OrderManager.orderStore.GetByInternalOrderID("NoOrder")
if err != ErrOrderNotFound {
t.Error(err)
}
err = bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: "thisWillFail",
})
if err == nil {
t.Error("Expected exchange not found error")
}
}
func TestGetByExchangeAndID(t *testing.T) {
bot := OrdersSetup(t)
err := bot.OrderManager.orderStore.Add(&order.Detail{
Exchange: testExchange,
ID: "TestGetByExchangeAndID",
})
if err != nil {
t.Error(err)
}
o, err := bot.OrderManager.orderStore.GetByExchangeAndID(testExchange, "TestGetByExchangeAndID")
if err != nil {
t.Error(err)
}
if o.ID != "TestGetByExchangeAndID" {
t.Error("Expected to retrieve order")
}
_, err = bot.OrderManager.orderStore.GetByExchangeAndID("", "TestGetByExchangeAndID")
if err != ErrExchangeNotFound {
t.Error(err)
}
_, err = bot.OrderManager.orderStore.GetByExchangeAndID(testExchange, "")
if err != ErrOrderNotFound {
t.Error(err)
}
}
func TestExists(t *testing.T) {
bot := OrdersSetup(t)
if bot.OrderManager.orderStore.exists(nil) {
t.Error("Expected false")
}
o := &order.Detail{
Exchange: testExchange,
ID: "TestExists",
}
err := bot.OrderManager.orderStore.Add(o)
if err != nil {
t.Error(err)
}
b := bot.OrderManager.orderStore.exists(o)
if !b {
t.Error("Expected true")
}
}
func TestCancelOrder(t *testing.T) {
bot := OrdersSetup(t)
err := bot.OrderManager.Cancel(nil)
if err == nil {
t.Error("Expected error due to empty order")
}
err = bot.OrderManager.Cancel(&order.Cancel{})
if err == nil {
t.Error("Expected error due to empty order")
}
err = bot.OrderManager.Cancel(&order.Cancel{
Exchange: testExchange,
})
if err == nil {
t.Error("Expected error due to no order ID")
}
err = bot.OrderManager.Cancel(&order.Cancel{
ID: "ID",
})
if err == nil {
t.Error("Expected error due to no Exchange")
}
err = bot.OrderManager.Cancel(&order.Cancel{
ID: "ID",
Exchange: testExchange,
AssetType: asset.Binary,
})
if err == nil {
t.Error("Expected error due to bad asset type")
}
o := &order.Detail{
Exchange: fakePassExchange,
ID: "TestCancelOrder",
Status: order.New,
}
err = bot.OrderManager.orderStore.Add(o)
if err != nil {
t.Error(err)
}
err = bot.OrderManager.Cancel(&order.Cancel{
ID: "Unknown",
Exchange: fakePassExchange,
AssetType: asset.Spot,
})
if err == nil {
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",
Side: order.Sell,
Status: order.New,
AssetType: asset.Spot,
Date: time.Now(),
Pair: pair,
}
err = bot.OrderManager.Cancel(cancel)
if err != nil {
t.Error(err)
}
if o.Status != order.Cancelled {
t.Error("Failed to cancel")
}
}
func TestGetOrderInfo(t *testing.T) {
bot := OrdersSetup(t)
_, err := bot.OrderManager.GetOrderInfo("", "", currency.Pair{}, "")
if err == nil {
t.Error("Expected error due to empty order")
}
var result order.Detail
result, err = bot.OrderManager.GetOrderInfo(fakePassExchange, "1234", currency.Pair{}, "")
if err != nil {
t.Error(err)
}
if result.ID != "fakeOrder" {
t.Error("unexpected order returned")
}
result, err = bot.OrderManager.GetOrderInfo(fakePassExchange, "1234", currency.Pair{}, "")
if err != nil {
t.Error(err)
}
if result.ID != "fakeOrder" {
t.Error("unexpected order returned")
}
}
func TestCancelAllOrders(t *testing.T) {
bot := OrdersSetup(t)
o := &order.Detail{
Exchange: fakePassExchange,
ID: "TestCancelAllOrders",
Status: order.New,
}
err := bot.OrderManager.orderStore.Add(o)
if err != nil {
t.Error(err)
}
bot.OrderManager.CancelAllOrders([]string{"NotFound"})
if o.Status == order.Cancelled {
t.Error("Order should not be cancelled")
}
bot.OrderManager.CancelAllOrders([]string{fakePassExchange})
if o.Status != order.Cancelled {
t.Error("Order should be cancelled")
}
o.Status = order.New
bot.OrderManager.CancelAllOrders(nil)
if o.Status != order.New {
t.Error("Order should not be cancelled")
}
}
func TestSubmit(t *testing.T) {
bot := OrdersSetup(t)
_, err := bot.OrderManager.Submit(nil)
if err == nil {
t.Error("Expected error from nil order")
}
o := &order.Submit{
Exchange: "",
ID: "FakePassingExchangeOrder",
Status: order.New,
Type: order.Market,
}
_, err = bot.OrderManager.Submit(o)
if err == nil {
t.Error("Expected error from empty exchange")
}
o.Exchange = fakePassExchange
_, err = bot.OrderManager.Submit(o)
if err == nil {
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 = pair
o.AssetType = asset.Spot
o.Side = order.Buy
o.Amount = 1
o.Price = 1
_, err = bot.OrderManager.Submit(o)
if err == nil {
t.Error("Expected fail due to order market type is not allowed")
}
bot.OrderManager.cfg.AllowMarketOrders = true
bot.OrderManager.cfg.LimitAmount = 1
o.Amount = 2
_, err = bot.OrderManager.Submit(o)
if err == nil {
t.Error("Expected fail due to order limit exceeds allowed limit")
}
bot.OrderManager.cfg.LimitAmount = 0
bot.OrderManager.cfg.AllowedExchanges = []string{"fake"}
_, err = bot.OrderManager.Submit(o)
if err == nil {
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{failPair}
_, err = bot.OrderManager.Submit(o)
if err == nil {
t.Error("Expected fail due to order pair not found in allowed list")
}
bot.OrderManager.cfg.AllowedPairs = nil
_, err = bot.OrderManager.Submit(o)
if err != nil {
t.Error(err)
}
o2, err := bot.OrderManager.orderStore.GetByExchangeAndID(fakePassExchange, "FakePassingExchangeOrder")
if err != nil {
t.Error(err)
}
if o2.InternalOrderID == "" {
t.Error("Failed to assign internal order id")
}
}
func TestProcessOrders(t *testing.T) {
bot := OrdersSetup(t)
bot.OrderManager.processOrders()
}

View File

@@ -1,41 +0,0 @@
package engine
import (
"errors"
"sync"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
var (
errOrderIDCannotBeEmpty = errors.New("orderID cannot be empty")
)
type orderManagerConfig struct {
EnforceLimitConfig bool
AllowMarketOrders bool
CancelOrdersOnShutdown bool
LimitAmount float64
AllowedPairs currency.Pairs
AllowedExchanges []string
OrderSubmissionRetries int64
}
type orderStore struct {
m sync.RWMutex
Orders map[string][]*order.Detail
bot *Engine
}
type orderManager struct {
started int32
shutdown chan struct{}
orderStore orderStore
cfg orderManagerConfig
}
type orderSubmitResponse struct {
order.SubmitResponse
InternalOrderID string
}

View File

@@ -1,100 +0,0 @@
package engine
import (
"errors"
"fmt"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/engine/subsystem"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio"
)
// vars for the fund manager package
var (
PortfolioSleepDelay = time.Minute
)
type portfolioManager struct {
started int32
processing int32
shutdown chan struct{}
}
func (p *portfolioManager) Started() bool {
return atomic.LoadInt32(&p.started) == 1
}
func (p *portfolioManager) Start() error {
if atomic.AddInt32(&p.started, 1) != 1 {
return errors.New("portfolio manager already started")
}
log.Debugln(log.PortfolioMgr, "Portfolio manager starting...")
Bot.Portfolio = &portfolio.Portfolio
Bot.Portfolio.Seed(Bot.Config.Portfolio)
p.shutdown = make(chan struct{})
portfolio.Verbose = Bot.Settings.Verbose
go p.run()
return nil
}
func (p *portfolioManager) Stop() error {
if atomic.LoadInt32(&p.started) == 0 {
return fmt.Errorf("portfolio manager %w", subsystem.ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&p.started, 1, 0)
}()
log.Debugln(log.PortfolioMgr, "Portfolio manager shutting down...")
close(p.shutdown)
return nil
}
func (p *portfolioManager) run() {
log.Debugln(log.PortfolioMgr, "Portfolio manager started.")
Bot.ServicesWG.Add(1)
tick := time.NewTicker(Bot.Settings.PortfolioManagerDelay)
defer func() {
tick.Stop()
Bot.ServicesWG.Done()
log.Debugf(log.PortfolioMgr, "Portfolio manager shutdown.")
}()
go p.processPortfolio()
for {
select {
case <-p.shutdown:
return
case <-tick.C:
go p.processPortfolio()
}
}
}
func (p *portfolioManager) processPortfolio() {
if !atomic.CompareAndSwapInt32(&p.processing, 0, 1) {
return
}
pf := portfolio.GetPortfolio()
data := pf.GetPortfolioGroupedCoin()
for key, value := range data {
err := pf.UpdatePortfolio(value, key)
if err != nil {
log.Errorf(log.PortfolioMgr,
"PortfolioWatcher error %s for currency %s\n",
err,
key)
continue
}
log.Debugf(log.PortfolioMgr,
"Portfolio manager: Successfully updated address balance for %s address(es) %s\n",
key,
value)
}
SeedExchangeAccountInfo(Bot.GetAllEnabledExchangeAccountInfo().Data)
atomic.CompareAndSwapInt32(&p.processing, 1, 0)
}

327
engine/portfolio_manager.go Normal file
View File

@@ -0,0 +1,327 @@
package engine
import (
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio"
)
// PortfolioManagerName is an exported subsystem name
const PortfolioManagerName = "portfolio"
var (
// PortfolioSleepDelay defines the default sleep time between portfolio manager runs
PortfolioSleepDelay = time.Minute
)
// portfolioManager routinely retrieves a user's holdings through exchange APIs as well
// as through addresses provided in the config
type portfolioManager struct {
started int32
processing int32
portfolioManagerDelay time.Duration
exchangeManager *ExchangeManager
shutdown chan struct{}
base *portfolio.Base
}
// setupPortfolioManager creates a new portfolio manager
func setupPortfolioManager(e *ExchangeManager, portfolioManagerDelay time.Duration, cfg *portfolio.Base) (*portfolioManager, error) {
if e == nil {
return nil, errNilExchangeManager
}
if portfolioManagerDelay <= 0 {
portfolioManagerDelay = PortfolioSleepDelay
}
if cfg == nil {
cfg = &portfolio.Base{Addresses: []portfolio.Address{}}
}
m := &portfolioManager{
portfolioManagerDelay: portfolioManagerDelay,
exchangeManager: e,
shutdown: make(chan struct{}),
base: cfg,
}
return m, nil
}
// IsRunning safely checks whether the subsystem is running
func (m *portfolioManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Start runs the subsystem
func (m *portfolioManager) Start(wg *sync.WaitGroup) error {
if m == nil {
return fmt.Errorf("portfolio manager %w", ErrNilSubsystem)
}
if wg == nil {
return errNilWaitGroup
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return fmt.Errorf("portfolio manager %w", ErrSubSystemAlreadyStarted)
}
log.Debugf(log.PortfolioMgr, "Portfolio manager %s", MsgSubSystemStarting)
m.shutdown = make(chan struct{})
go m.run(wg)
return nil
}
// Stop attempts to shutdown the subsystem
func (m *portfolioManager) Stop() error {
if m == nil {
return fmt.Errorf("portfolio manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 1, 0) {
return fmt.Errorf("portfolio manager %w", ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&m.started, 1, 0)
}()
log.Debugf(log.PortfolioMgr, "Portfolio manager %s", MsgSubSystemShuttingDown)
close(m.shutdown)
return nil
}
// run periodically will check and update portfolio holdings
func (m *portfolioManager) run(wg *sync.WaitGroup) {
log.Debugln(log.PortfolioMgr, "Portfolio manager started.")
wg.Add(1)
tick := time.NewTicker(m.portfolioManagerDelay)
defer func() {
tick.Stop()
wg.Done()
log.Debugf(log.PortfolioMgr, "Portfolio manager shutdown.")
}()
go m.processPortfolio()
for {
select {
case <-m.shutdown:
return
case <-tick.C:
go m.processPortfolio()
}
}
}
// processPortfolio updates portfolio holdings
func (m *portfolioManager) processPortfolio() {
if !atomic.CompareAndSwapInt32(&m.processing, 0, 1) {
return
}
data := m.base.GetPortfolioGroupedCoin()
for key, value := range data {
err := m.base.UpdatePortfolio(value, key)
if err != nil {
log.Errorf(log.PortfolioMgr,
"PortfolioWatcher error %s for currency %s\n",
err,
key)
continue
}
log.Debugf(log.PortfolioMgr,
"Portfolio manager: Successfully updated address balance for %s address(es) %s\n",
key,
value)
}
d := m.getExchangeAccountInfo(m.exchangeManager.GetExchanges())
m.seedExchangeAccountInfo(d)
atomic.CompareAndSwapInt32(&m.processing, 1, 0)
}
// seedExchangeAccountInfo seeds account info
func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings) {
if len(accounts) == 0 {
return
}
for x := range accounts {
exchangeName := accounts[x].Exchange
var currencies []account.Balance
for y := range accounts[x].Accounts {
for z := range accounts[x].Accounts[y].Currencies {
var update bool
for i := range currencies {
if accounts[x].Accounts[y].Currencies[z].CurrencyName == currencies[i].CurrencyName {
currencies[i].Hold += accounts[x].Accounts[y].Currencies[z].Hold
currencies[i].TotalValue += accounts[x].Accounts[y].Currencies[z].TotalValue
update = true
}
}
if update {
continue
}
currencies = append(currencies, account.Balance{
CurrencyName: accounts[x].Accounts[y].Currencies[z].CurrencyName,
TotalValue: accounts[x].Accounts[y].Currencies[z].TotalValue,
Hold: accounts[x].Accounts[y].Currencies[z].Hold,
})
}
}
for x := range currencies {
currencyName := currencies[x].CurrencyName
total := currencies[x].TotalValue
if !m.base.ExchangeAddressExists(exchangeName, currencyName) {
if total <= 0 {
continue
}
log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n",
exchangeName,
currencyName,
total,
portfolio.ExchangeAddress)
m.base.Addresses = append(
m.base.Addresses,
portfolio.Address{Address: exchangeName,
CoinType: currencyName,
Balance: total,
Description: portfolio.ExchangeAddress})
} else {
if total <= 0 {
log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n",
exchangeName,
currencyName)
m.base.RemoveExchangeAddress(exchangeName, currencyName)
} else {
balance, ok := m.base.GetAddressBalance(exchangeName,
portfolio.ExchangeAddress,
currencyName)
if !ok {
continue
}
if balance != total {
log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n",
exchangeName,
currencyName,
total)
m.base.UpdateExchangeAddressBalance(exchangeName,
currencyName,
total)
}
}
}
}
}
}
// getExchangeAccountInfo returns all the current enabled exchanges
func (m *portfolioManager) getExchangeAccountInfo(exchanges []exchange.IBotExchange) []account.Holdings {
var response []account.Holdings
for x := range exchanges {
if exchanges[x] == nil || !exchanges[x].IsEnabled() {
continue
}
if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if m.base.Verbose {
log.Debugf(log.PortfolioMgr,
"skipping %s due to disabled authenticated API support.\n",
exchanges[x].GetName())
}
continue
}
assetTypes := exchanges[x].GetAssetTypes()
var exchangeHoldings account.Holdings
for y := range assetTypes {
accountHoldings, err := exchanges[x].FetchAccountInfo(assetTypes[y])
if err != nil {
log.Errorf(log.PortfolioMgr,
"Error encountered retrieving exchange account info for %s. Error %s\n",
exchanges[x].GetName(),
err)
continue
}
for z := range accountHoldings.Accounts {
accountHoldings.Accounts[z].AssetType = assetTypes[y]
}
exchangeHoldings.Exchange = exchanges[x].GetName()
exchangeHoldings.Accounts = append(exchangeHoldings.Accounts, accountHoldings.Accounts...)
}
response = append(response, exchangeHoldings)
}
return response
}
// AddAddress adds a new portfolio address for the portfolio manager to track
func (m *portfolioManager) AddAddress(address, description string, coinType currency.Code, balance float64) error {
if m == nil {
return fmt.Errorf("portfolio manager %w", ErrNilSubsystem)
}
if !m.IsRunning() {
return fmt.Errorf("portfolio manager %w", ErrSubSystemNotStarted)
}
return m.base.AddAddress(address, description, coinType, balance)
}
// RemoveAddress removes a portfolio address
func (m *portfolioManager) RemoveAddress(address, description string, coinType currency.Code) error {
if m == nil {
return fmt.Errorf("portfolio manager %w", ErrNilSubsystem)
}
if !m.IsRunning() {
return fmt.Errorf("portfolio manager %w", ErrSubSystemNotStarted)
}
return m.base.RemoveAddress(address, description, coinType)
}
// GetPortfolioSummary returns a summary of all portfolio holdings
func (m *portfolioManager) GetPortfolioSummary() portfolio.Summary {
if m == nil || !m.IsRunning() {
return portfolio.Summary{}
}
return m.base.GetPortfolioSummary()
}
// GetAddresses returns all addresses
func (m *portfolioManager) GetAddresses() []portfolio.Address {
if m == nil || !m.IsRunning() {
return nil
}
return m.base.Addresses
}
// GetPortfolio returns a copy of the internal portfolio base for
// saving addresses to the config
func (m *portfolioManager) GetPortfolio() *portfolio.Base {
if m == nil || !m.IsRunning() {
return nil
}
resp := m.base
return resp
}
// IsWhiteListed checks if an address is whitelisted to withdraw to
func (m *portfolioManager) IsWhiteListed(address string) bool {
if m == nil || !m.IsRunning() {
return false
}
return m.base.IsWhiteListed(address)
}
// IsExchangeSupported checks if an exchange is supported
func (m *portfolioManager) IsExchangeSupported(exchange, address string) bool {
if m == nil || !m.IsRunning() {
return false
}
return m.base.IsExchangeSupported(exchange, address)
}

View File

@@ -0,0 +1,66 @@
# GoCryptoTrader package Portfolio_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/portfolio_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This portfolio_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Portfolio_manager
+ The portfolio manager subsystem is used to synchronise and monitor wallet addresses
+ It can read addresses specified in your config file
+ If you have set API keys for an enabled exchange and enabled `authenticatedSupport`, it will store your exchange addresses
+ In order to modify the behaviour of the portfolio manager subsystem, you can edit the following inside your config file under `portfolioAddresses`:
### portfolioAddresses
| Config | Description | Example |
| ------ | ----------- | ------- |
| Verbose | Enabling this will output more detailed logs to your logging output | `false` |
| addresses | An array of portfolio wallet addresses to monitor, see below table | |
### addresses
| Config | Description | Example |
| ------ | ----------- | ------- |
| Address | The wallet address | `bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc` |
| CoinType | The coin for the wallet address | `BTC` |
| Balance | The balance of the wallet | |
| Description | A customisable description | `My secret billion stash` |
| WhiteListed | Determines whether GoCryptoTrader withdraw manager subsystem can make withdrawals from this address | `true` |
| ColdStorage | Describes whether the wallet address is a cold storage wallet eg Ledger | `false` |
| SupportedExchanges | A comma delimited string of which exchanges are allowed to interact with this wallet | `"Binance"` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,117 @@
package engine
import (
"errors"
"sync"
"testing"
)
func TestSetupPortfolioManager(t *testing.T) {
_, err := setupPortfolioManager(nil, 0, nil)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
m, err := setupPortfolioManager(SetupExchangeManager(), 0, nil)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expected manager")
}
}
func TestIsPortfolioManagerRunning(t *testing.T) {
var m *portfolioManager
if m.IsRunning() {
t.Error("expected false")
}
m, err := setupPortfolioManager(SetupExchangeManager(), 0, nil)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.IsRunning() {
t.Error("expected false")
}
var wg sync.WaitGroup
err = m.Start(&wg)
if err != nil {
t.Error(err)
}
if !m.IsRunning() {
t.Error("expected true")
}
}
func TestPortfolioManagerStart(t *testing.T) {
var m *portfolioManager
var wg sync.WaitGroup
err := m.Start(nil)
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
m, err = setupPortfolioManager(SetupExchangeManager(), 0, nil)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start(nil)
if !errors.Is(err, errNilWaitGroup) {
t.Errorf("error '%v', expected '%v'", err, errNilWaitGroup)
}
err = m.Start(&wg)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start(&wg)
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
}
func TestPortfolioManagerStop(t *testing.T) {
var m *portfolioManager
var wg sync.WaitGroup
err := m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
m, err = setupPortfolioManager(SetupExchangeManager(), 0, nil)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start(&wg)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestProcessPortfolio(t *testing.T) {
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("Bitstamp")
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
exch.SetDefaults()
em.Add(exch)
m, err := setupPortfolioManager(em, 0, nil)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
m.processPortfolio()
}

View File

@@ -1,118 +0,0 @@
package engine
import (
"fmt"
"net/http"
"net/http/pprof"
"runtime"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/log"
)
// RESTLogger logs the requests internally
func RESTLogger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
log.Debugf(log.RESTSys,
"%s\t%s\t%s\t%s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}
// StartRESTServer starts a REST server
func StartRESTServer(bot *Engine) {
listenAddr := bot.Config.RemoteControl.DeprecatedRPC.ListenAddress
log.Debugf(log.RESTSys,
"Deprecated RPC server support enabled. Listen URL: http://%s:%d\n",
common.ExtractHost(listenAddr), common.ExtractPort(listenAddr))
err := http.ListenAndServe(listenAddr, newRouter(bot, true))
if err != nil {
log.Errorf(log.RESTSys, "Failed to start deprecated RPC server. Err: %s", err)
}
}
// StartWebsocketServer starts a Websocket server
func StartWebsocketServer(bot *Engine) {
listenAddr := bot.Config.RemoteControl.WebsocketRPC.ListenAddress
log.Debugf(log.RESTSys,
"Websocket RPC support enabled. Listen URL: ws://%s:%d/ws\n",
common.ExtractHost(listenAddr), common.ExtractPort(listenAddr))
err := http.ListenAndServe(listenAddr, newRouter(bot, false))
if err != nil {
log.Errorf(log.RESTSys, "Failed to start websocket RPC server. Err: %s", err)
}
}
// newRouter takes in the exchange interfaces and returns a new multiplexor
// router
func newRouter(bot *Engine, isREST bool) *mux.Router {
router := mux.NewRouter().StrictSlash(true)
var routes []Route
var listenAddr string
if isREST {
listenAddr = bot.Config.RemoteControl.DeprecatedRPC.ListenAddress
} else {
listenAddr = bot.Config.RemoteControl.WebsocketRPC.ListenAddress
}
if common.ExtractPort(listenAddr) == 80 {
listenAddr = common.ExtractHost(listenAddr)
} else {
listenAddr = strings.Join([]string{common.ExtractHost(listenAddr),
strconv.Itoa(common.ExtractPort(listenAddr))}, ":")
}
if isREST {
routes = []Route{
{"", http.MethodGet, "/", getIndex},
{"GetAllSettings", http.MethodGet, "/config/all", RESTGetAllSettings},
{"SaveAllSettings", http.MethodPost, "/config/all/save", RESTSaveAllSettings},
{"AllEnabledAccountInfo", http.MethodGet, "/exchanges/enabled/accounts/all", RESTGetAllEnabledAccountInfo},
{"AllActiveExchangesAndCurrencies", http.MethodGet, "/exchanges/enabled/latest/all", RESTGetAllActiveTickers},
{"GetPortfolio", http.MethodGet, "/portfolio/all", RESTGetPortfolio},
{"AllActiveExchangesAndOrderbooks", http.MethodGet, "/exchanges/orderbook/latest/all", RESTGetAllActiveOrderbooks},
}
if bot.Config.Profiler.Enabled {
if bot.Config.Profiler.MutexProfileFraction > 0 {
runtime.SetMutexProfileFraction(bot.Config.Profiler.MutexProfileFraction)
}
log.Debugf(log.RESTSys,
"HTTP Go performance profiler (pprof) endpoint enabled: http://%s:%d/debug/pprof/\n",
common.ExtractHost(listenAddr),
common.ExtractPort(listenAddr))
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
}
} else {
routes = []Route{
{"ws", http.MethodGet, "/ws", WebsocketClientHandler},
}
}
for _, route := range routes {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(RESTLogger(route.HandlerFunc, route.Name)).
Host(listenAddr)
}
return router
}
func getIndex(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "<html>GoCryptoTrader RESTful interface. For the web GUI, please visit the <a href=https://github.com/thrasher-corp/gocryptotrader/blob/master/web/README.md>web GUI readme.</a></html>")
w.WriteHeader(http.StatusOK)
}

View File

@@ -1,135 +0,0 @@
package engine
import (
"encoding/json"
"net/http"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio"
)
// RESTfulJSONResponse outputs a JSON response of the response interface
func RESTfulJSONResponse(w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
return json.NewEncoder(w).Encode(response)
}
// RESTfulError prints the REST method and error
func RESTfulError(method string, err error) {
log.Errorf(log.RESTSys, "RESTful %s: server failed to send JSON response. Error %s\n",
method, err)
}
// RESTGetAllSettings replies to a request with an encoded JSON response about the
// trading Bots configuration.
func RESTGetAllSettings(w http.ResponseWriter, r *http.Request) {
err := RESTfulJSONResponse(w, config.Cfg)
if err != nil {
RESTfulError(r.Method, err)
}
}
// RESTSaveAllSettings saves all current settings from request body as a JSON
// document then reloads state and returns the settings
func RESTSaveAllSettings(w http.ResponseWriter, r *http.Request) {
// Get the data from the request
decoder := json.NewDecoder(r.Body)
var responseData config.Post
err := decoder.Decode(&responseData)
if err != nil {
RESTfulError(r.Method, err)
}
// Save change the settings
err = Bot.Config.UpdateConfig(Bot.Settings.ConfigFile, &responseData.Data, false)
if err != nil {
RESTfulError(r.Method, err)
}
err = RESTfulJSONResponse(w, Bot.Config)
if err != nil {
RESTfulError(r.Method, err)
}
Bot.SetupExchanges()
}
// GetAllActiveOrderbooks returns all enabled exchanges orderbooks
func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks {
var orderbookData []EnabledExchangeOrderbooks
exchanges := Bot.GetExchanges()
for x := range exchanges {
assets := exchanges[x].GetAssetTypes()
exchName := exchanges[x].GetName()
var exchangeOB EnabledExchangeOrderbooks
exchangeOB.ExchangeName = exchName
for y := range assets {
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 {
log.Errorf(log.RESTSys,
"Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName,
currencies[z].String(),
err)
continue
}
exchangeOB.ExchangeValues = append(exchangeOB.ExchangeValues, *ob)
}
orderbookData = append(orderbookData, exchangeOB)
}
orderbookData = append(orderbookData, exchangeOB)
}
return orderbookData
}
// RESTGetAllActiveOrderbooks returns all enabled exchange orderbooks
func RESTGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) {
var response AllEnabledExchangeOrderbooks
response.Data = GetAllActiveOrderbooks()
err := RESTfulJSONResponse(w, response)
if err != nil {
RESTfulError(r.Method, err)
}
}
// RESTGetPortfolio returns the Bot portfolio
func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) {
p := portfolio.GetPortfolio()
result := p.GetPortfolioSummary()
err := RESTfulJSONResponse(w, result)
if err != nil {
RESTfulError(r.Method, err)
}
}
// RESTGetAllActiveTickers returns all active tickers
func RESTGetAllActiveTickers(w http.ResponseWriter, r *http.Request) {
var response AllEnabledExchangeCurrencies
response.Data = Bot.GetAllActiveTickers()
err := RESTfulJSONResponse(w, response)
if err != nil {
RESTfulError(r.Method, err)
}
}
// RESTGetAllEnabledAccountInfo via get request returns JSON response of account
// info
func RESTGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) {
response := Bot.GetAllEnabledExchangeAccountInfo()
err := RESTfulJSONResponse(w, response)
if err != nil {
RESTfulError(r.Method, err)
}
}

View File

@@ -1,116 +0,0 @@
package engine
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"runtime"
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
)
func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response {
w := httptest.NewRecorder()
err := RESTfulJSONResponse(w, response)
if err != nil {
t.Error("Failed to make response.", err)
}
return w.Result()
}
// TestConfigAllJsonResponse test if config/all restful json response is valid
func TestConfigAllJsonResponse(t *testing.T) {
bot := CreateTestBot(t)
resp := makeHTTPGetRequest(t, bot.Config)
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
t.Error("Body not readable", err)
}
var responseConfig config.Config
jsonErr := json.Unmarshal(body, &responseConfig)
if jsonErr != nil {
t.Error("Response not parseable as json", err)
}
if reflect.DeepEqual(responseConfig, bot.Config) {
t.Error("Json not equal to config")
}
}
func TestInvalidHostRequest(t *testing.T) {
e := CreateTestBot(t)
req, err := http.NewRequest(http.MethodGet, "/config/all", nil)
if err != nil {
t.Fatal(err)
}
req.Host = "invalidsite.com"
resp := httptest.NewRecorder()
newRouter(e, true).ServeHTTP(resp, req)
if status := resp.Code; status != http.StatusNotFound {
t.Errorf("Response returned wrong status code expected %v got %v", http.StatusNotFound, status)
}
}
func TestValidHostRequest(t *testing.T) {
e := CreateTestBot(t)
if config.Cfg.Name == "" {
config.Cfg = *e.Config
}
req, err := http.NewRequest(http.MethodGet, "/config/all", nil)
if err != nil {
t.Fatal(err)
}
req.Host = "localhost:9050"
resp := httptest.NewRecorder()
newRouter(e, true).ServeHTTP(resp, req)
if status := resp.Code; status != http.StatusOK {
t.Errorf("Response returned wrong status code expected %v got %v", http.StatusOK, status)
}
}
func TestProfilerEnabledShouldEnableProfileEndPoint(t *testing.T) {
e := CreateTestBot(t)
req, err := http.NewRequest(http.MethodGet, "/debug/pprof/", nil)
if err != nil {
t.Fatal(err)
}
req.Host = "localhost:9050"
resp := httptest.NewRecorder()
newRouter(e, true).ServeHTTP(resp, req)
if status := resp.Code; status != http.StatusNotFound {
t.Errorf("Response returned wrong status code expected %v got %v", http.StatusNotFound, status)
}
e.Config.Profiler.Enabled = true
e.Config.Profiler.MutexProfileFraction = 5
req, err = http.NewRequest(http.MethodGet, "/debug/pprof/", nil)
if err != nil {
t.Fatal(err)
}
mutexValue := runtime.SetMutexProfileFraction(10)
if mutexValue != 0 {
t.Fatalf("SetMutexProfileFraction() should be 0 on first set received: %v", mutexValue)
}
resp = httptest.NewRecorder()
newRouter(e, true).ServeHTTP(resp, req)
mutexValue = runtime.SetMutexProfileFraction(10)
if mutexValue != 5 {
t.Fatalf("SetMutexProfileFraction() should be 5 after setup received: %v", mutexValue)
}
if status := resp.Code; status != http.StatusOK {
t.Errorf("Response returned wrong status code expected %v got %v", http.StatusOK, status)
}
}

View File

@@ -1,46 +0,0 @@
package engine
import (
"net/http"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
// Route is a sub type that holds the request routes
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks
type AllEnabledExchangeOrderbooks struct {
Data []EnabledExchangeOrderbooks `json:"data"`
}
// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective
// orderbooks
type EnabledExchangeOrderbooks struct {
ExchangeName string `json:"exchangeName"`
ExchangeValues []orderbook.Base `json:"exchangeValues"`
}
// AllEnabledExchangeCurrencies holds the enabled exchange currencies
type AllEnabledExchangeCurrencies struct {
Data []EnabledExchangeCurrencies `json:"data"`
}
// EnabledExchangeCurrencies is a sub type for singular exchanges and respective
// currencies
type EnabledExchangeCurrencies struct {
ExchangeName string `json:"exchangeName"`
ExchangeValues []ticker.Price `json:"exchangeValues"`
}
// AllEnabledExchangeAccounts holds all enabled accounts info
type AllEnabledExchangeAccounts struct {
Data []account.Holdings `json:"data"`
}

View File

@@ -1,427 +0,0 @@
package engine
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"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/log"
)
func printCurrencyFormat(price float64, displayCurrency currency.Code) string {
displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency)
if err != nil {
log.Errorf(log.Global, "Failed to get display symbol: %s\n", err)
}
return fmt.Sprintf("%s%.8f", displaySymbol, price)
}
func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64, displayCurrency currency.Code) string {
conv, err := currency.ConvertCurrency(origPrice,
origCurrency,
displayCurrency)
if err != nil {
log.Errorf(log.Global, "Failed to convert currency: %s\n", err)
}
displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency)
if err != nil {
log.Errorf(log.Global, "Failed to get display symbol: %s\n", err)
}
origSymbol, err := currency.GetSymbolByCurrencyName(origCurrency)
if err != nil {
log.Errorf(log.Global, "Failed to get original currency symbol for %s: %s\n",
origCurrency,
err)
}
return fmt.Sprintf("%s%.2f %s (%s%.2f %s)",
displaySymbol,
conv,
displayCurrency,
origSymbol,
origPrice,
origCurrency,
)
}
func printTickerSummary(result *ticker.Price, protocol string, err error) {
if err != nil {
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,
err)
return
}
stats.Add(result.ExchangeName, result.Pair, result.AssetType, result.Last, result.Volume)
if result.Pair.Quote.IsFiatCurrency() &&
Bot != nil &&
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",
result.ExchangeName,
protocol,
Bot.FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
printConvertCurrencyFormat(origCurrency, result.Last, Bot.Config.Currency.FiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.Ask, Bot.Config.Currency.FiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.Bid, Bot.Config.Currency.FiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.High, Bot.Config.Currency.FiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.Low, Bot.Config.Currency.FiatDisplayCurrency),
result.Volume)
} else {
if result.Pair.Quote.IsFiatCurrency() &&
Bot != nil &&
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",
result.ExchangeName,
protocol,
Bot.FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
printCurrencyFormat(result.Last, Bot.Config.Currency.FiatDisplayCurrency),
printCurrencyFormat(result.Ask, Bot.Config.Currency.FiatDisplayCurrency),
printCurrencyFormat(result.Bid, Bot.Config.Currency.FiatDisplayCurrency),
printCurrencyFormat(result.High, Bot.Config.Currency.FiatDisplayCurrency),
printCurrencyFormat(result.Low, Bot.Config.Currency.FiatDisplayCurrency),
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",
result.ExchangeName,
protocol,
Bot.FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
result.Last,
result.Ask,
result.Bid,
result.High,
result.Low,
result.Volume)
}
}
}
const (
book = "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n"
)
func printOrderbookSummary(result *orderbook.Base, protocol string, bot *Engine, err error) {
if err != nil {
if result == nil {
log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n",
protocol,
err)
return
}
if err == common.ErrNotYetImplemented {
log.Warnf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n",
protocol,
result.Exchange,
result.Pair,
result.Asset,
err)
return
}
log.Errorf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n",
protocol,
result.Exchange,
result.Pair,
result.Asset,
err)
return
}
bidsAmount, bidsValue := result.TotalBidsAmount()
asksAmount, asksValue := result.TotalAsksAmount()
var bidValueResult, askValueResult string
switch {
case result.Pair.Quote.IsFiatCurrency() && bot != nil && result.Pair.Quote != bot.Config.Currency.FiatDisplayCurrency:
origCurrency := result.Pair.Quote.Upper()
bidValueResult = printConvertCurrencyFormat(origCurrency, bidsValue, bot.Config.Currency.FiatDisplayCurrency)
askValueResult = printConvertCurrencyFormat(origCurrency, asksValue, bot.Config.Currency.FiatDisplayCurrency)
case result.Pair.Quote.IsFiatCurrency() && bot != nil && result.Pair.Quote == bot.Config.Currency.FiatDisplayCurrency:
bidValueResult = printCurrencyFormat(bidsValue, bot.Config.Currency.FiatDisplayCurrency)
askValueResult = printCurrencyFormat(asksValue, bot.Config.Currency.FiatDisplayCurrency)
default:
bidValueResult = strconv.FormatFloat(bidsValue, 'f', -1, 64)
askValueResult = strconv.FormatFloat(asksValue, 'f', -1, 64)
}
log.Infof(log.OrderBook, book,
result.Exchange,
protocol,
bot.FormatCurrency(result.Pair),
strings.ToUpper(result.Asset.String()),
len(result.Bids),
bidsAmount,
result.Pair.Base,
bidValueResult,
len(result.Asks),
asksAmount,
result.Pair.Base,
askValueResult,
)
}
func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) {
evt := WebsocketEvent{
Data: result,
Event: event,
AssetType: assetType,
Exchange: exchangeName,
}
err := BroadcastWebsocketMessage(evt)
if err != nil {
log.Errorf(log.WebsocketMgr, "Failed to broadcast websocket event %v. Error: %s\n",
event, err)
}
}
// WebsocketRoutine Initial routine management system for websocket
func (bot *Engine) WebsocketRoutine() {
if bot.Settings.Verbose {
log.Debugln(log.WebsocketMgr, "Connecting exchange websocket services...")
}
exchanges := bot.GetExchanges()
for i := range exchanges {
go func(i int) {
if exchanges[i].SupportsWebsocket() {
if bot.Settings.Verbose {
log.Debugf(log.WebsocketMgr,
"Exchange %s websocket support: Yes Enabled: %v\n",
exchanges[i].GetName(),
common.IsEnabled(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
}
// 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 bot.WebsocketDataReceiver(ws)
if ws.IsEnabled() {
err = ws.Connect()
if err != nil {
log.Errorf(log.WebsocketMgr, "%v\n", err)
}
err = ws.FlushChannels()
if err != nil {
log.Errorf(log.WebsocketMgr, "Failed to subscribe: %v\n", err)
}
}
} else if bot.Settings.Verbose {
log.Debugf(log.WebsocketMgr,
"Exchange %s websocket support: No\n",
exchanges[i].GetName(),
)
}
}(i)
}
}
var shutdowner = make(chan struct{}, 1)
var wg sync.WaitGroup
// WebsocketDataReceiver handles websocket data coming from a websocket feed
// associated with an exchange
func (bot *Engine) WebsocketDataReceiver(ws *stream.Websocket) {
wg.Add(1)
defer wg.Done()
for {
select {
case <-shutdowner:
return
case data := <-ws.ToRoutine:
err := bot.WebsocketDataHandler(ws.GetName(), data)
if err != nil {
log.Error(log.WebsocketMgr, err)
}
}
}
}
// WebsocketDataHandler is a central point for exchange websocket implementations to send
// processed data. WebsocketDataHandler will then pass that to an appropriate handler
func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error {
if data == nil {
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 stream.FundingData:
if bot.Settings.Verbose {
log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v",
exchName,
bot.FormatCurrency(d.CurrencyPair),
d.AssetType,
d)
}
case *ticker.Price:
if bot.Settings.EnableExchangeSyncManager && bot.ExchangeCurrencyPairManager != nil {
bot.ExchangeCurrencyPairManager.update(exchName,
d.Pair,
d.AssetType,
SyncItemTicker,
nil)
}
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,
bot.FormatCurrency(d.Pair),
d.AssetType,
d)
}
case *orderbook.Base:
if bot.Settings.EnableExchangeSyncManager && bot.ExchangeCurrencyPairManager != nil {
bot.ExchangeCurrencyPairManager.update(exchName,
d.Pair,
d.Asset,
SyncItemOrderbook,
nil)
}
printOrderbookSummary(d, "websocket", bot, nil)
case *order.Detail:
if bot.Settings.Verbose {
printOrderSummary(d)
}
// TODO: Dont check if exists this creates two locks, on conflict update
// else insert.
if !bot.OrderManager.orderStore.exists(d) {
err := bot.OrderManager.orderStore.Add(d)
if err != nil {
return err
}
} else {
od, err := bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID)
if err != nil {
return err
}
od.UpdateOrderFromDetail(d)
}
case *order.Modify:
if bot.Settings.Verbose {
printOrderChangeSummary(d)
}
// TODO: On conflict update or insert if not found
od, err := bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID)
if err != nil {
return err
}
od.UpdateOrderFromModify(d)
case order.ClassificationError:
return errors.New(d.Error())
case stream.UnhandledMessageWarning:
log.Warn(log.WebsocketMgr, d.Message)
case account.Change:
if bot.Settings.Verbose {
printAccountHoldingsChangeSummary(d)
}
default:
if bot.Settings.Verbose {
log.Warnf(log.WebsocketMgr,
"%s websocket Unknown type: %+v",
exchName,
d)
}
}
return nil
}
// printOrderChangeSummary this function will be deprecated when a order manager
// update is done.
func printOrderChangeSummary(m *order.Modify) {
if m == nil {
return
}
log.Debugf(log.WebsocketMgr,
"Order Change: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f",
m.Exchange,
m.AssetType,
m.Pair,
m.Status,
m.Type,
m.Side,
m.ID,
m.ClientOrderID,
m.Price,
m.Amount,
m.ExecutedAmount,
m.RemainingAmount)
}
// printOrderSummary this function will be deprecated when a order manager
// update is done.
func printOrderSummary(m *order.Detail) {
if m == nil {
return
}
log.Debugf(log.WebsocketMgr,
"New Order: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f",
m.Exchange,
m.AssetType,
m.Pair,
m.Status,
m.Type,
m.Side,
m.ID,
m.ClientOrderID,
m.Price,
m.Amount,
m.ExecutedAmount,
m.RemainingAmount)
}
// printAccountHoldingsChangeSummary this function will be deprecated when a
// account holdings update is done.
func printAccountHoldingsChangeSummary(m account.Change) {
log.Debugf(log.WebsocketMgr,
"Account Holdings Balance Changed: %s %s %s has changed balance by %f for account: %s",
m.Exchange,
m.Asset,
m.Currency,
m.Amount,
m.Account)
}

View File

@@ -1,121 +0,0 @@
package engine
import (
"errors"
"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"
)
func TestWebsocketDataHandlerProcess(t *testing.T) {
ws := sharedtestvalues.NewTestWebsocket()
b := OrdersSetup(t)
go b.WebsocketDataReceiver(ws)
ws.DataHandler <- "string"
time.Sleep(time.Second)
close(shutdowner)
}
func TestHandleData(t *testing.T) {
b := OrdersSetup(t)
var exchName = "exch"
var orderID = "testOrder.Detail"
err := b.WebsocketDataHandler(exchName, errors.New("error"))
if err == nil {
t.Error("Error not handled correctly")
}
err = b.WebsocketDataHandler(exchName, nil)
if err == nil {
t.Error("Expected nil data error")
}
err = b.WebsocketDataHandler(exchName, stream.FundingData{})
if err != nil {
t.Error(err)
}
err = b.WebsocketDataHandler(exchName, &ticker.Price{})
if err != nil {
t.Error(err)
}
err = b.WebsocketDataHandler(exchName, stream.KlineData{})
if err != nil {
t.Error(err)
}
origOrder := &order.Detail{
Exchange: fakePassExchange,
ID: orderID,
Amount: 1337,
Price: 1337,
}
err = b.WebsocketDataHandler(exchName, origOrder)
if err != nil {
t.Error(err)
}
// Send it again since it exists now
err = b.WebsocketDataHandler(exchName, &order.Detail{
Exchange: fakePassExchange,
ID: orderID,
Amount: 1338,
})
if err != nil {
t.Error(err)
}
if origOrder.Amount != 1338 {
t.Error("Bad pipeline")
}
err = b.WebsocketDataHandler(exchName, &order.Modify{
Exchange: fakePassExchange,
ID: orderID,
Status: order.Active,
})
if err != nil {
t.Error(err)
}
if origOrder.Status != order.Active {
t.Error("Expected order to be modified to Active")
}
// Send some gibberish
err = b.WebsocketDataHandler(exchName, order.Stop)
if err != nil {
t.Error(err)
}
err = b.WebsocketDataHandler(exchName, stream.UnhandledMessageWarning{
Message: "there's an issue here's a tissue"},
)
if err != nil {
t.Error(err)
}
classificationError := order.ClassificationError{
Exchange: "test",
OrderID: "one",
Err: errors.New("lol"),
}
err = b.WebsocketDataHandler(exchName, classificationError)
if err == nil {
t.Error("Expected error")
}
if err != nil && err.Error() != classificationError.Error() {
t.Errorf("Problem formatting error. Expected %v Received %v", classificationError.Error(), err.Error())
}
err = b.WebsocketDataHandler(exchName, &orderbook.Base{
Exchange: fakePassExchange,
Pair: currency.NewPair(currency.BTC, currency.USD),
})
if err != nil {
t.Error(err)
}
err = b.WebsocketDataHandler(exchName, "this is a test string")
if err != nil {
t.Error(err)
}
}

View File

@@ -62,15 +62,16 @@ var (
errDispatchSystem = errors.New("dispatch system offline")
errCurrencyNotEnabled = errors.New("currency not enabled")
errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list")
errNoTrades = errors.New("no trades returned from supplied params")
)
// RPCServer struct
type RPCServer struct {
*Engine
gctrpc.UnimplementedGoCryptoTraderServer
*Engine
}
func (bot *Engine) authenticateClient(ctx context.Context) (context.Context, error) {
func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx, fmt.Errorf("unable to extract metadata")
@@ -93,7 +94,7 @@ func (bot *Engine) authenticateClient(ctx context.Context) (context.Context, err
username := strings.Split(string(decoded), ":")[0]
password := strings.Split(string(decoded), ":")[1]
if username != bot.Config.RemoteControl.Username || password != bot.Config.RemoteControl.Password {
if username != s.Config.RemoteControl.Username || password != s.Config.RemoteControl.Password {
return ctx, fmt.Errorf("username/password mismatch")
}
@@ -108,7 +109,6 @@ func StartRPCServer(engine *Engine) {
log.Errorf(log.GRPCSys, "gRPC checkCerts failed. err: %s\n", err)
return
}
log.Debugf(log.GRPCSys, "gRPC server support enabled. Starting gRPC server on https://%v.\n", engine.Config.RemoteControl.GRPC.ListenAddress)
lis, err := net.Listen("tcp", engine.Config.RemoteControl.GRPC.ListenAddress)
if err != nil {
@@ -122,12 +122,12 @@ func StartRPCServer(engine *Engine) {
return
}
s := RPCServer{Engine: engine}
opts := []grpc.ServerOption{
grpc.Creds(creds),
grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(engine.authenticateClient)),
grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(s.authenticateClient)),
}
server := grpc.NewServer(opts...)
s := RPCServer{Engine: engine}
gctrpc.RegisterGoCryptoTraderServer(server, &s)
go func() {
@@ -180,8 +180,8 @@ func (s *RPCServer) StartRPCRESTProxy() {
}
// GetInfo returns info about the current GoCryptoTrader session
func (s *RPCServer) GetInfo(_ context.Context, r *gctrpc.GetInfoRequest) (*gctrpc.GetInfoResponse, error) {
d := time.Since(s.Uptime)
func (s *RPCServer) GetInfo(_ context.Context, _ *gctrpc.GetInfoRequest) (*gctrpc.GetInfoResponse, error) {
d := time.Since(s.uptime)
resp := gctrpc.GetInfoResponse{
Uptime: d.String(),
EnabledExchanges: int64(s.Config.CountEnabledExchanges()),
@@ -202,7 +202,7 @@ func (s *RPCServer) GetInfo(_ context.Context, r *gctrpc.GetInfoRequest) (*gctrp
}
// GetSubsystems returns a list of subsystems and their status
func (s *RPCServer) GetSubsystems(_ context.Context, r *gctrpc.GetSubsystemsRequest) (*gctrpc.GetSusbsytemsResponse, error) {
func (s *RPCServer) GetSubsystems(_ context.Context, _ *gctrpc.GetSubsystemsRequest) (*gctrpc.GetSusbsytemsResponse, error) {
return &gctrpc.GetSusbsytemsResponse{SubsystemsStatus: s.GetSubsystemsStatus()}, nil
}
@@ -227,7 +227,7 @@ func (s *RPCServer) DisableSubsystem(_ context.Context, r *gctrpc.GenericSubsyst
}
// GetRPCEndpoints returns a list of API endpoints
func (s *RPCServer) GetRPCEndpoints(_ context.Context, r *gctrpc.GetRPCEndpointsRequest) (*gctrpc.GetRPCEndpointsResponse, error) {
func (s *RPCServer) GetRPCEndpoints(_ context.Context, _ *gctrpc.GetRPCEndpointsRequest) (*gctrpc.GetRPCEndpointsResponse, error) {
endpoints := GetRPCEndpoints()
var resp gctrpc.GetRPCEndpointsResponse
resp.Endpoints = make(map[string]*gctrpc.RPCEndpoint)
@@ -241,8 +241,8 @@ func (s *RPCServer) GetRPCEndpoints(_ context.Context, r *gctrpc.GetRPCEndpoints
}
// GetCommunicationRelayers returns the status of the engines communication relayers
func (s *RPCServer) GetCommunicationRelayers(_ context.Context, r *gctrpc.GetCommunicationRelayersRequest) (*gctrpc.GetCommunicationRelayersResponse, error) {
relayers, err := s.CommsManager.GetStatus()
func (s *RPCServer) GetCommunicationRelayers(_ context.Context, _ *gctrpc.GetCommunicationRelayersRequest) (*gctrpc.GetCommunicationRelayersResponse, error) {
relayers, err := s.CommunicationsManager.GetStatus()
if err != nil {
return nil, err
}
@@ -285,13 +285,13 @@ func (s *RPCServer) EnableExchange(_ context.Context, r *gctrpc.GenericExchangeN
// GetExchangeOTPCode retrieves an exchanges OTP code
func (s *RPCServer) GetExchangeOTPCode(_ context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GetExchangeOTPReponse, error) {
result, err := s.GetExchangeoOTPByName(r.Exchange)
result, err := s.GetExchangeOTPByName(r.Exchange)
return &gctrpc.GetExchangeOTPReponse{OtpCode: result}, err
}
// GetExchangeOTPCodes retrieves OTP codes for all exchanges which have an
// OTP secret installed
func (s *RPCServer) GetExchangeOTPCodes(_ context.Context, r *gctrpc.GetExchangeOTPsRequest) (*gctrpc.GetExchangeOTPsResponse, error) {
func (s *RPCServer) GetExchangeOTPCodes(_ context.Context, _ *gctrpc.GetExchangeOTPsRequest) (*gctrpc.GetExchangeOTPsResponse, error) {
result, err := s.GetExchangeOTPs()
return &gctrpc.GetExchangeOTPsResponse{OtpCodes: result}, err
}
@@ -377,32 +377,33 @@ func (s *RPCServer) GetTicker(_ context.Context, r *gctrpc.GetTickerRequest) (*g
// GetTickers returns a list of tickers for all enabled exchanges and all
// enabled currency pairs
func (s *RPCServer) GetTickers(_ context.Context, r *gctrpc.GetTickersRequest) (*gctrpc.GetTickersResponse, error) {
func (s *RPCServer) GetTickers(_ context.Context, _ *gctrpc.GetTickersRequest) (*gctrpc.GetTickersResponse, error) {
activeTickers := s.GetAllActiveTickers()
var tickers []*gctrpc.Tickers
for x := range activeTickers {
var ticker gctrpc.Tickers
ticker.Exchange = activeTickers[x].ExchangeName
t := &gctrpc.Tickers{
Exchange: activeTickers[x].ExchangeName,
}
for y := range activeTickers[x].ExchangeValues {
t := activeTickers[x].ExchangeValues[y]
ticker.Tickers = append(ticker.Tickers, &gctrpc.TickerResponse{
val := activeTickers[x].ExchangeValues[y]
t.Tickers = append(t.Tickers, &gctrpc.TickerResponse{
Pair: &gctrpc.CurrencyPair{
Delimiter: t.Pair.Delimiter,
Base: t.Pair.Base.String(),
Quote: t.Pair.Quote.String(),
Delimiter: val.Pair.Delimiter,
Base: val.Pair.Base.String(),
Quote: val.Pair.Quote.String(),
},
LastUpdated: t.LastUpdated.Unix(),
Last: t.Last,
High: t.High,
Low: t.Low,
Bid: t.Bid,
Ask: t.Ask,
Volume: t.Volume,
PriceAth: t.PriceATH,
LastUpdated: val.LastUpdated.Unix(),
Last: val.Last,
High: val.High,
Low: val.Low,
Bid: val.Bid,
Ask: val.Ask,
Volume: val.Volume,
PriceAth: val.PriceATH,
})
}
tickers = append(tickers, &ticker)
tickers = append(tickers, t)
}
return &gctrpc.GetTickersResponse{Tickers: tickers}, nil
@@ -463,46 +464,66 @@ func (s *RPCServer) GetOrderbook(_ context.Context, r *gctrpc.GetOrderbookReques
// GetOrderbooks returns a list of orderbooks for all enabled exchanges and all
// enabled currency pairs
func (s *RPCServer) GetOrderbooks(_ context.Context, r *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) {
activeOrderbooks := GetAllActiveOrderbooks()
var orderbooks []*gctrpc.Orderbooks
for x := range activeOrderbooks {
var ob gctrpc.Orderbooks
ob.Exchange = activeOrderbooks[x].ExchangeName
for y := range activeOrderbooks[x].ExchangeValues {
o := activeOrderbooks[x].ExchangeValues[y]
var bids []*gctrpc.OrderbookItem
for z := range o.Bids {
bids = append(bids, &gctrpc.OrderbookItem{
Amount: o.Bids[z].Amount,
Price: o.Bids[z].Price,
})
}
var asks []*gctrpc.OrderbookItem
for z := range o.Asks {
asks = append(asks, &gctrpc.OrderbookItem{
Amount: o.Asks[z].Amount,
Price: o.Asks[z].Price,
})
}
ob.Orderbooks = append(ob.Orderbooks, &gctrpc.OrderbookResponse{
Pair: &gctrpc.CurrencyPair{
Delimiter: o.Pair.Delimiter,
Base: o.Pair.Base.String(),
Quote: o.Pair.Quote.String(),
},
LastUpdated: o.LastUpdated.Unix(),
Bids: bids,
Asks: asks,
})
func (s *RPCServer) GetOrderbooks(_ context.Context, _ *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) {
exchanges := s.ExchangeManager.GetExchanges()
var obResponse []*gctrpc.Orderbooks
var obs []*gctrpc.OrderbookResponse
for x := range exchanges {
if !exchanges[x].IsEnabled() {
continue
}
orderbooks = append(orderbooks, &ob)
assets := exchanges[x].GetAssetTypes()
exchName := exchanges[x].GetName()
for y := range assets {
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 {
resp, err := exchanges[x].FetchOrderbook(currencies[z], assets[y])
if err != nil {
log.Errorf(log.RESTSys,
"Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName,
currencies[z].String(),
err)
continue
}
ob := &gctrpc.OrderbookResponse{
Pair: &gctrpc.CurrencyPair{
Delimiter: currencies[z].Delimiter,
Base: currencies[z].Base.String(),
Quote: currencies[z].Quote.String(),
},
AssetType: assets[y].String(),
LastUpdated: resp.LastUpdated.Unix(),
}
for i := range resp.Bids {
ob.Bids = append(ob.Bids, &gctrpc.OrderbookItem{
Amount: resp.Bids[i].Amount,
Price: resp.Bids[i].Price,
})
}
for i := range resp.Asks {
ob.Asks = append(ob.Asks, &gctrpc.OrderbookItem{
Amount: resp.Asks[i].Amount,
Price: resp.Asks[i].Price,
})
}
obs = append(obs, ob)
}
}
obResponse = append(obResponse, &gctrpc.Orderbooks{
Exchange: exchanges[x].GetName(),
Orderbooks: obs,
})
}
return &gctrpc.GetOrderbooksResponse{Orderbooks: orderbooks}, nil
return &gctrpc.GetOrderbooksResponse{Orderbooks: obResponse}, nil
}
// GetAccountInfo returns an account balance for a specific exchange
@@ -528,7 +549,7 @@ func (s *RPCServer) GetAccountInfo(_ context.Context, r *gctrpc.GetAccountInfoRe
}
// UpdateAccountInfo forces an update of the account info
func (s *RPCServer) UpdateAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) {
func (s *RPCServer) UpdateAccountInfo(_ context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) {
assetType, err := asset.New(r.AssetType)
if err != nil {
return nil, err
@@ -613,7 +634,12 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream
return err
}
defer pipe.Release()
defer func() {
pipeErr := pipe.Release()
if pipeErr != nil {
log.Error(log.DispatchMgr, pipeErr)
}
}()
for {
data, ok := <-pipe.C
@@ -650,15 +676,14 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream
}
// GetConfig returns the bots config
func (s *RPCServer) GetConfig(_ context.Context, r *gctrpc.GetConfigRequest) (*gctrpc.GetConfigResponse, error) {
func (s *RPCServer) GetConfig(_ context.Context, _ *gctrpc.GetConfigRequest) (*gctrpc.GetConfigResponse, error) {
return &gctrpc.GetConfigResponse{}, common.ErrNotYetImplemented
}
// GetPortfolio returns the portfolio details
func (s *RPCServer) GetPortfolio(_ context.Context, r *gctrpc.GetPortfolioRequest) (*gctrpc.GetPortfolioResponse, error) {
// GetPortfolio returns the portfoliomanager details
func (s *RPCServer) GetPortfolio(_ context.Context, _ *gctrpc.GetPortfolioRequest) (*gctrpc.GetPortfolioResponse, error) {
var addrs []*gctrpc.PortfolioAddress
botAddrs := s.Portfolio.Addresses
botAddrs := s.portfolioManager.GetAddresses()
for x := range botAddrs {
addrs = append(addrs, &gctrpc.PortfolioAddress{
Address: botAddrs[x].Address,
@@ -675,9 +700,9 @@ func (s *RPCServer) GetPortfolio(_ context.Context, r *gctrpc.GetPortfolioReques
return resp, nil
}
// GetPortfolioSummary returns the portfolio summary
func (s *RPCServer) GetPortfolioSummary(_ context.Context, r *gctrpc.GetPortfolioSummaryRequest) (*gctrpc.GetPortfolioSummaryResponse, error) {
result := s.Portfolio.GetPortfolioSummary()
// GetPortfolioSummary returns the portfoliomanager summary
func (s *RPCServer) GetPortfolioSummary(_ context.Context, _ *gctrpc.GetPortfolioSummaryRequest) (*gctrpc.GetPortfolioSummaryResponse, error) {
result := s.portfolioManager.GetPortfolioSummary()
var resp gctrpc.GetPortfolioSummaryResponse
p := func(coins []portfolio.Coin) []*gctrpc.Coin {
@@ -731,9 +756,9 @@ func (s *RPCServer) GetPortfolioSummary(_ context.Context, r *gctrpc.GetPortfoli
return &resp, nil
}
// AddPortfolioAddress adds an address to the portfolio manager
// AddPortfolioAddress adds an address to the portfoliomanager manager
func (s *RPCServer) AddPortfolioAddress(_ context.Context, r *gctrpc.AddPortfolioAddressRequest) (*gctrpc.GenericResponse, error) {
err := s.Portfolio.AddAddress(r.Address,
err := s.portfolioManager.AddAddress(r.Address,
r.Description,
currency.NewCode(r.CoinType),
r.Balance)
@@ -743,9 +768,9 @@ func (s *RPCServer) AddPortfolioAddress(_ context.Context, r *gctrpc.AddPortfoli
return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil
}
// RemovePortfolioAddress removes an address from the portfolio manager
// RemovePortfolioAddress removes an address from the portfoliomanager manager
func (s *RPCServer) RemovePortfolioAddress(_ context.Context, r *gctrpc.RemovePortfolioAddressRequest) (*gctrpc.GenericResponse, error) {
err := s.Portfolio.RemoveAddress(r.Address,
err := s.portfolioManager.RemoveAddress(r.Address,
r.Description,
currency.NewCode(r.CoinType))
if err != nil {
@@ -755,7 +780,7 @@ func (s *RPCServer) RemovePortfolioAddress(_ context.Context, r *gctrpc.RemovePo
}
// GetForexProviders returns a list of available forex providers
func (s *RPCServer) GetForexProviders(_ context.Context, r *gctrpc.GetForexProvidersRequest) (*gctrpc.GetForexProvidersResponse, error) {
func (s *RPCServer) GetForexProviders(_ context.Context, _ *gctrpc.GetForexProvidersRequest) (*gctrpc.GetForexProvidersResponse, error) {
providers := s.Config.GetForexProviders()
if len(providers) == 0 {
return nil, fmt.Errorf("forex providers is empty")
@@ -777,7 +802,7 @@ func (s *RPCServer) GetForexProviders(_ context.Context, r *gctrpc.GetForexProvi
}
// GetForexRates returns a list of forex rates
func (s *RPCServer) GetForexRates(_ context.Context, r *gctrpc.GetForexRatesRequest) (*gctrpc.GetForexRatesResponse, error) {
func (s *RPCServer) GetForexRates(_ context.Context, _ *gctrpc.GetForexRatesRequest) (*gctrpc.GetForexRatesResponse, error) {
rates, err := currency.GetExchangeRates()
if err != nil {
return nil, err
@@ -1109,7 +1134,7 @@ func (s *RPCServer) WhaleBomb(_ context.Context, r *gctrpc.WhaleBombRequest) (*g
}
exch := s.GetExchangeByName(r.Exchange)
err := checkParams(r.Exchange, exch, asset.Item(""), p)
err := checkParams(r.Exchange, exch, asset.Spot, p)
if err != nil {
return nil, err
}
@@ -1126,6 +1151,9 @@ func (s *RPCServer) WhaleBomb(_ context.Context, r *gctrpc.WhaleBombRequest) (*g
}
result, err := o.WhaleBomb(r.PriceTarget, buy)
if err != nil {
return nil, err
}
var resp gctrpc.SimulateOrderResponse
for x := range result.Orders {
resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{
@@ -1245,18 +1273,18 @@ func (s *RPCServer) CancelAllOrders(_ context.Context, r *gctrpc.CancelAllOrders
}
// GetEvents returns the stored events list
func (s *RPCServer) GetEvents(_ context.Context, r *gctrpc.GetEventsRequest) (*gctrpc.GetEventsResponse, error) {
func (s *RPCServer) GetEvents(_ context.Context, _ *gctrpc.GetEventsRequest) (*gctrpc.GetEventsResponse, error) {
return &gctrpc.GetEventsResponse{}, common.ErrNotYetImplemented
}
// AddEvent adds an event
func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gctrpc.AddEventResponse, error) {
evtCondition := EventConditionParams{
CheckBids: r.ConditionParams.CheckBids,
CheckBidsAndAsks: r.ConditionParams.CheckBidsAndAsks,
Condition: r.ConditionParams.Condition,
OrderbookAmount: r.ConditionParams.OrderbookAmount,
Price: r.ConditionParams.Price,
CheckBids: r.ConditionParams.CheckBids,
CheckAsks: r.ConditionParams.CheckAsks,
Condition: r.ConditionParams.Condition,
OrderbookAmount: r.ConditionParams.OrderbookAmount,
Price: r.ConditionParams.Price,
}
p := currency.NewPairWithDelimiter(r.Pair.Base,
@@ -1273,7 +1301,7 @@ func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gct
return nil, err
}
id, err := Add(r.Exchange, r.Item, evtCondition, p, a, r.Action)
id, err := s.eventManager.Add(r.Exchange, r.Item, evtCondition, p, a, r.Action)
if err != nil {
return nil, err
}
@@ -1283,7 +1311,7 @@ func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gct
// RemoveEvent removes an event, specified by an event ID
func (s *RPCServer) RemoveEvent(_ context.Context, r *gctrpc.RemoveEventRequest) (*gctrpc.GenericResponse, error) {
if !Remove(r.Id) {
if !s.eventManager.Remove(r.Id) {
return nil, fmt.Errorf("event %d not removed", r.Id)
}
return &gctrpc.GenericResponse{Status: MsgStatusSuccess,
@@ -1335,7 +1363,7 @@ func (s *RPCServer) WithdrawCryptocurrencyFunds(_ context.Context, r *gctrpc.Wit
},
}
resp, err := s.Engine.SubmitWithdrawal(request)
resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(request)
if err != nil {
return nil, err
}
@@ -1377,7 +1405,7 @@ func (s *RPCServer) WithdrawFiatFunds(_ context.Context, r *gctrpc.WithdrawFiatR
},
}
resp, err := s.Engine.SubmitWithdrawal(request)
resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(request)
if err != nil {
return nil, err
}
@@ -1393,7 +1421,7 @@ func (s *RPCServer) WithdrawalEventByID(_ context.Context, r *gctrpc.WithdrawalE
if !s.Config.Database.Enabled {
return nil, database.ErrDatabaseSupportDisabled
}
v, err := WithdrawalEventByID(r.Id)
v, err := s.WithdrawManager.WithdrawalEventByID(r.Id)
if err != nil {
return nil, err
}
@@ -1468,14 +1496,14 @@ func (s *RPCServer) WithdrawalEventsByExchange(_ context.Context, r *gctrpc.With
return nil, database.ErrDatabaseSupportDisabled
}
if r.Id == "" {
ret, err := WithdrawalEventByExchange(r.Exchange, int(r.Limit))
ret, err := s.WithdrawManager.WithdrawalEventByExchange(r.Exchange, int(r.Limit))
if err != nil {
return nil, err
}
return parseMultipleEvents(ret), nil
}
ret, err := WithdrawalEventByExchangeID(r.Exchange, r.Id)
ret, err := s.WithdrawManager.WithdrawalEventByExchangeID(r.Exchange, r.Id)
if err != nil {
return nil, err
}
@@ -1495,7 +1523,7 @@ func (s *RPCServer) WithdrawalEventsByDate(_ context.Context, r *gctrpc.Withdraw
return nil, err
}
var ret []*withdraw.Response
ret, err = WithdrawEventByDate(r.Exchange, UTCStartTime, UTCEndTime, int(r.Limit))
ret, err = s.WithdrawManager.WithdrawEventByDate(r.Exchange, UTCStartTime, UTCEndTime, int(r.Limit))
if err != nil {
return nil, err
}
@@ -1715,7 +1743,12 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr
return err
}
defer pipe.Release()
defer func() {
pipeErr := pipe.Release()
if pipeErr != nil {
log.Error(log.DispatchMgr, pipeErr)
}
}()
for {
data, ok := <-pipe.C
@@ -1780,7 +1813,12 @@ func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gct
return err
}
defer pipe.Release()
defer func() {
pipeErr := pipe.Release()
if pipeErr != nil {
log.Error(log.DispatchMgr, pipeErr)
}
}()
for {
data, ok := <-pipe.C
@@ -1820,7 +1858,12 @@ func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamReq
return err
}
defer pipe.Release()
defer func() {
pipeErr := pipe.Release()
if pipeErr != nil {
log.Error(log.DispatchMgr, pipeErr)
}
}()
for {
data, ok := <-pipe.C
@@ -2061,8 +2104,8 @@ func fillMissingCandlesWithStoredTrades(startTime, endTime time.Time, klineItem
}
// GCTScriptStatus returns a slice of current running scripts that includes next run time and uuid
func (s *RPCServer) GCTScriptStatus(_ context.Context, r *gctrpc.GCTScriptStatusRequest) (*gctrpc.GCTScriptStatusResponse, error) {
if !s.GctScriptManager.Started() {
func (s *RPCServer) GCTScriptStatus(_ context.Context, _ *gctrpc.GCTScriptStatusRequest) (*gctrpc.GCTScriptStatusResponse, error) {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GCTScriptStatusResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2071,7 +2114,7 @@ func (s *RPCServer) GCTScriptStatus(_ context.Context, r *gctrpc.GCTScriptStatus
}
resp := &gctrpc.GCTScriptStatusResponse{
Status: fmt.Sprintf("%v of %v virtual machines running", gctscript.VMSCount.Len(), s.GctScriptManager.GetMaxVirtualMachines()),
Status: fmt.Sprintf("%v of %v virtual machines running", gctscript.VMSCount.Len(), s.gctScriptManager.GetMaxVirtualMachines()),
}
gctscript.AllVMSync.Range(func(k, v interface{}) bool {
@@ -2090,7 +2133,7 @@ func (s *RPCServer) GCTScriptStatus(_ context.Context, r *gctrpc.GCTScriptStatus
// GCTScriptQuery queries a running script and returns script running information
func (s *RPCServer) GCTScriptQuery(_ context.Context, r *gctrpc.GCTScriptQueryRequest) (*gctrpc.GCTScriptQueryResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GCTScriptQueryResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2121,7 +2164,7 @@ func (s *RPCServer) GCTScriptQuery(_ context.Context, r *gctrpc.GCTScriptQueryRe
// GCTScriptExecute execute a script
func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecuteRequest) (*gctrpc.GenericResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2129,7 +2172,7 @@ func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecu
r.Script.Path = gctscript.ScriptPath
}
gctVM := s.GctScriptManager.New()
gctVM := s.gctScriptManager.New()
if gctVM == nil {
return &gctrpc.GenericResponse{Status: MsgStatusError, Data: "unable to create VM instance"}, nil
}
@@ -2153,7 +2196,7 @@ func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecu
// GCTScriptStop terminate a running script
func (s *RPCServer) GCTScriptStop(_ context.Context, r *gctrpc.GCTScriptStopRequest) (*gctrpc.GenericResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2175,7 +2218,7 @@ func (s *RPCServer) GCTScriptStop(_ context.Context, r *gctrpc.GCTScriptStopRequ
// GCTScriptUpload upload a new script to ScriptPath
func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUploadRequest) (*gctrpc.GenericResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2231,7 +2274,7 @@ func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUpload
}
var failedFiles []string
for x := range files {
err = s.GctScriptManager.Validate(files[x])
err = s.gctScriptManager.Validate(files[x])
if err != nil {
failedFiles = append(failedFiles, files[x])
}
@@ -2248,7 +2291,7 @@ func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUpload
return &gctrpc.GenericResponse{Status: gctscript.ErrScriptFailedValidation, Data: strings.Join(failedFiles, ", ")}, nil
}
} else {
err = s.GctScriptManager.Validate(fPath)
err = s.gctScriptManager.Validate(fPath)
if err != nil {
errRemove := os.Remove(fPath)
if errRemove != nil {
@@ -2266,7 +2309,7 @@ func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUpload
// GCTScriptReadScript read a script and return contents
func (s *RPCServer) GCTScriptReadScript(_ context.Context, r *gctrpc.GCTScriptReadScriptRequest) (*gctrpc.GCTScriptQueryResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GCTScriptQueryResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2291,7 +2334,7 @@ func (s *RPCServer) GCTScriptReadScript(_ context.Context, r *gctrpc.GCTScriptRe
// GCTScriptListAll lists all scripts inside the default script path
func (s *RPCServer) GCTScriptListAll(context.Context, *gctrpc.GCTScriptListAllRequest) (*gctrpc.GCTScriptStatusResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GCTScriptStatusResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
@@ -2317,11 +2360,11 @@ func (s *RPCServer) GCTScriptListAll(context.Context, *gctrpc.GCTScriptListAllRe
// GCTScriptStopAll stops all running scripts
func (s *RPCServer) GCTScriptStopAll(context.Context, *gctrpc.GCTScriptStopAllRequest) (*gctrpc.GenericResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
err := s.GctScriptManager.ShutdownAll()
err := s.gctScriptManager.ShutdownAll()
if err != nil {
return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil
}
@@ -2334,19 +2377,19 @@ func (s *RPCServer) GCTScriptStopAll(context.Context, *gctrpc.GCTScriptStopAllRe
// GCTScriptAutoLoadToggle adds or removes an entry to the autoload list
func (s *RPCServer) GCTScriptAutoLoadToggle(_ context.Context, r *gctrpc.GCTScriptAutoLoadRequest) (*gctrpc.GenericResponse, error) {
if !s.GctScriptManager.Started() {
if !s.gctScriptManager.IsRunning() {
return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil
}
if r.Status {
err := s.GctScriptManager.Autoload(r.Script, true)
err := s.gctScriptManager.Autoload(r.Script, true)
if err != nil {
return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil
}
return &gctrpc.GenericResponse{Status: "success", Data: "script " + r.Script + " removed from autoload list"}, nil
}
err := s.GctScriptManager.Autoload(r.Script, false)
err := s.gctScriptManager.Autoload(r.Script, false)
if err != nil {
return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil
}
@@ -2709,7 +2752,7 @@ func (s *RPCServer) ConvertTradesToCandles(_ context.Context, r *gctrpc.ConvertT
return nil, err
}
if len(trades) == 0 {
return nil, fmt.Errorf("no trades returned from supplied params")
return nil, errNoTrades
}
interval := kline.Interval(r.TimeInterval)
var klineItem kline.Item
@@ -3010,11 +3053,12 @@ func (s *RPCServer) GetHistoricTrades(r *gctrpc.GetSavedTradesRequest, stream gc
})
}
stream.Send(grpcTrades)
err = stream.Send(grpcTrades)
if err != nil {
return err
}
}
stream.Send(resp)
return nil
return stream.Send(resp)
}
// GetRecentTrades returns trades
@@ -3069,7 +3113,7 @@ func checkParams(exchName string, e exchange.IBotExchange, a asset.Item, p curre
return fmt.Errorf("%s %w", exchName, errExchangeNotLoaded)
}
if !e.IsEnabled() {
return fmt.Errorf("%s %w", exchName, errExchangeDisabled)
return fmt.Errorf("%s %w", exchName, ErrExchangeNotFound)
}
if a.IsValid() {
b := e.GetBase()
@@ -3100,3 +3144,141 @@ func checkParams(exchName string, e exchange.IBotExchange, a asset.Item, p curre
}
return fmt.Errorf("%v %w", p, errCurrencyPairInvalid)
}
func parseMultipleEvents(ret []*withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse {
v := &gctrpc.WithdrawalEventsByExchangeResponse{}
for x := range ret {
tempEvent := &gctrpc.WithdrawalEventResponse{
Id: ret[x].ID.String(),
Exchange: &gctrpc.WithdrawlExchangeEvent{
Name: ret[x].Exchange.Name,
Id: ret[x].Exchange.ID,
Status: ret[x].Exchange.Status,
},
Request: &gctrpc.WithdrawalRequestEvent{
Currency: ret[x].RequestDetails.Currency.String(),
Description: ret[x].RequestDetails.Description,
Amount: ret[x].RequestDetails.Amount,
Type: int32(ret[x].RequestDetails.Type),
},
}
tempEvent.CreatedAt = timestamppb.New(ret[x].CreatedAt)
if err := tempEvent.CreatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseMultipleEvents CreatedAt: %s", err)
}
tempEvent.UpdatedAt = timestamppb.New(ret[x].UpdatedAt)
if err := tempEvent.UpdatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseMultipleEvents UpdatedAt: %s", err)
}
if ret[x].RequestDetails.Type == withdraw.Crypto {
tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent)
tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{
Address: ret[x].RequestDetails.Crypto.Address,
AddressTag: ret[x].RequestDetails.Crypto.AddressTag,
Fee: ret[x].RequestDetails.Crypto.FeeAmount,
}
} else if ret[x].RequestDetails.Type == withdraw.Fiat {
if ret[x].RequestDetails.Fiat != (withdraw.FiatRequest{}) {
tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent)
tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{
BankName: ret[x].RequestDetails.Fiat.Bank.BankName,
AccountName: ret[x].RequestDetails.Fiat.Bank.AccountName,
AccountNumber: ret[x].RequestDetails.Fiat.Bank.AccountNumber,
Bsb: ret[x].RequestDetails.Fiat.Bank.BSBNumber,
Swift: ret[x].RequestDetails.Fiat.Bank.SWIFTCode,
Iban: ret[x].RequestDetails.Fiat.Bank.IBAN,
}
}
}
v.Event = append(v.Event, tempEvent)
}
return v
}
func parseWithdrawalsHistory(ret []exchange.WithdrawalHistory, exchName string, limit int) *gctrpc.WithdrawalEventsByExchangeResponse {
v := &gctrpc.WithdrawalEventsByExchangeResponse{}
for x := range ret {
if limit > 0 && x >= limit {
return v
}
tempEvent := &gctrpc.WithdrawalEventResponse{
Id: ret[x].TransferID,
Exchange: &gctrpc.WithdrawlExchangeEvent{
Name: exchName,
Status: ret[x].Status,
},
Request: &gctrpc.WithdrawalRequestEvent{
Currency: ret[x].Currency,
Description: ret[x].Description,
Amount: ret[x].Amount,
},
}
tempEvent.UpdatedAt = timestamppb.New(ret[x].Timestamp)
if err := tempEvent.UpdatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseWithdrawalsHistory UpdatedAt: %s", err)
}
tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{
Address: ret[x].CryptoToAddress,
Fee: ret[x].Fee,
TxId: ret[x].CryptoTxID,
}
v.Event = append(v.Event, tempEvent)
}
return v
}
func parseSingleEvents(ret *withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse {
tempEvent := &gctrpc.WithdrawalEventResponse{
Id: ret.ID.String(),
Exchange: &gctrpc.WithdrawlExchangeEvent{
Name: ret.Exchange.Name,
Id: ret.Exchange.Name,
Status: ret.Exchange.Status,
},
Request: &gctrpc.WithdrawalRequestEvent{
Currency: ret.RequestDetails.Currency.String(),
Description: ret.RequestDetails.Description,
Amount: ret.RequestDetails.Amount,
Type: int32(ret.RequestDetails.Type),
},
}
tempEvent.CreatedAt = timestamppb.New(ret.CreatedAt)
if err := tempEvent.CreatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseSingleEvents CreatedAt %s", err)
}
tempEvent.UpdatedAt = timestamppb.New(ret.UpdatedAt)
if err := tempEvent.UpdatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseSingleEvents UpdatedAt: %s", err)
}
if ret.RequestDetails.Type == withdraw.Crypto {
tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent)
tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{
Address: ret.RequestDetails.Crypto.Address,
AddressTag: ret.RequestDetails.Crypto.AddressTag,
Fee: ret.RequestDetails.Crypto.FeeAmount,
}
} else if ret.RequestDetails.Type == withdraw.Fiat {
if ret.RequestDetails.Fiat != (withdraw.FiatRequest{}) {
tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent)
tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{
BankName: ret.RequestDetails.Fiat.Bank.BankName,
AccountName: ret.RequestDetails.Fiat.Bank.AccountName,
AccountNumber: ret.RequestDetails.Fiat.Bank.AccountNumber,
Bsb: ret.RequestDetails.Fiat.Bank.BSBNumber,
Swift: ret.RequestDetails.Fiat.Bank.SWIFTCode,
Iban: ret.RequestDetails.Fiat.Bank.IBAN,
}
}
}
return &gctrpc.WithdrawalEventsByExchangeResponse{
Event: []*gctrpc.WithdrawalEventResponse{tempEvent},
}
}

View File

@@ -3,11 +3,12 @@ package engine
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
@@ -21,12 +22,15 @@ import (
dbexchange "github.com/thrasher-corp/gocryptotrader/database/repository/exchange"
sqltrade "github.com/thrasher-corp/gocryptotrader/database/repository/trade"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
"github.com/thrasher-corp/goose"
)
@@ -37,14 +41,53 @@ const (
databaseName = "rpctestdb"
)
// fExchange is a fake exchange with function overrides
// we're not testing an actual exchange's implemented functions
type fExchange struct {
exchange.IBotExchange
}
// FetchAccountInfo overrides testExchange's fetch account info function
// to do the bare minimum required with no API calls or credentials required
func (f fExchange) FetchAccountInfo(a asset.Item) (account.Holdings, error) {
return account.Holdings{
Exchange: f.GetName(),
Accounts: []account.SubAccount{
{
ID: "1337",
AssetType: a,
Currencies: nil,
},
},
}, nil
}
// UpdateAccountInfo overrides testExchange's update account info function
// to do the bare minimum required with no API calls or credentials required
func (f fExchange) UpdateAccountInfo(a asset.Item) (account.Holdings, error) {
if a == asset.Futures {
return account.Holdings{}, errAssetTypeDisabled
}
return account.Holdings{
Exchange: f.GetName(),
Accounts: []account.SubAccount{
{
ID: "1337",
AssetType: a,
Currencies: nil,
},
},
}, nil
}
// Sets up everything required to run any function inside rpcserver
func RPCTestSetup(t *testing.T) *Engine {
database.DB.Mu.Lock()
var err error
dbConf := database.Config{
Enabled: true,
Driver: database.DBSQLite3,
ConnectionDetails: drivers.ConnectionDetails{
Host: "localhost",
Database: databaseName,
},
}
@@ -54,20 +97,22 @@ func RPCTestSetup(t *testing.T) *Engine {
if err != nil {
t.Fatalf("SetupTest: Failed to load config: %s", err)
}
if engerino.GetExchangeByName(testExchange) == nil {
err = engerino.LoadExchange(testExchange, false, nil)
if err != nil {
t.Fatalf("SetupTest: Failed to load exchange: %s", err)
}
engerino.ExchangeManager = SetupExchangeManager()
err = engerino.LoadExchange(testExchange, false, nil)
if err != nil {
log.Fatal(err)
}
engerino.Config.Database = dbConf
err = engerino.DatabaseManager.Start(engerino)
engerino.DatabaseManager, err = SetupDatabaseConnectionManager(&engerino.Config.Database)
if err != nil {
log.Fatal(err)
}
err = engerino.DatabaseManager.Start(&engerino.ServicesWG)
if err != nil {
log.Fatal(err)
}
path := filepath.Join("..", databaseFolder, migrationsFolder)
err = goose.Run("up", dbConn.SQL, repository.GetSQLDialect(), path, "")
err = goose.Run("up", database.DB.SQL, repository.GetSQLDialect(), path, "")
if err != nil {
t.Fatalf("failed to run migrations %v", err)
}
@@ -76,14 +121,11 @@ func RPCTestSetup(t *testing.T) *Engine {
if err != nil {
t.Fatalf("failed to insert exchange %v", err)
}
database.DB.Mu.Unlock()
return engerino
}
func CleanRPCTest(t *testing.T, engerino *Engine) {
database.DB.Mu.Lock()
defer database.DB.Mu.Unlock()
err := engerino.DatabaseManager.Stop()
if err != nil {
t.Error(err)
@@ -100,9 +142,6 @@ func TestGetSavedTrades(t *testing.T) {
defer CleanRPCTest(t, engerino)
s := RPCServer{Engine: engerino}
_, err := s.GetSavedTrades(context.Background(), &gctrpc.GetSavedTradesRequest{})
if err == nil {
t.Fatal(unexpectedLackOfError)
}
if !errors.Is(err, errInvalidArguments) {
t.Error(err)
}
@@ -117,10 +156,6 @@ func TestGetSavedTrades(t *testing.T) {
Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat),
End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat),
})
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errExchangeNotLoaded) {
t.Error(err)
}
@@ -178,10 +213,6 @@ func TestConvertTradesToCandles(t *testing.T) {
s := RPCServer{Engine: engerino}
// bad param test
_, err := s.ConvertTradesToCandles(context.Background(), &gctrpc.ConvertTradesToCandlesRequest{})
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errInvalidArguments) {
t.Error(err)
}
@@ -199,10 +230,6 @@ func TestConvertTradesToCandles(t *testing.T) {
End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat),
TimeInterval: int64(kline.OneHour.Duration()),
})
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errExchangeNotLoaded) {
t.Error(err)
}
@@ -220,17 +247,13 @@ func TestConvertTradesToCandles(t *testing.T) {
End: time.Date(2020, 2, 2, 2, 2, 2, 2, time.UTC).Format(common.SimpleTimeFormat),
TimeInterval: int64(kline.OneHour.Duration()),
})
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if err.Error() != "no trades returned from supplied params" {
t.Error(err)
if !errors.Is(err, errNoTrades) {
t.Errorf("received '%v' expected '%v'", err, errNoTrades)
}
// add a trade
err = sqltrade.Insert(sqltrade.Data{
Timestamp: time.Date(2020, 1, 1, 1, 1, 2, 1, time.UTC),
Timestamp: time.Date(2020, 1, 1, 1, 2, 2, 1, time.UTC),
Exchange: testExchange,
Base: currency.BTC.String(),
Quote: currency.USD.String(),
@@ -240,8 +263,7 @@ func TestConvertTradesToCandles(t *testing.T) {
Side: order.Buy.String(),
})
if err != nil {
t.Error(err)
return
t.Fatal(err)
}
// get candle from one trade
@@ -255,7 +277,7 @@ func TestConvertTradesToCandles(t *testing.T) {
},
AssetType: asset.Spot.String(),
Start: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat),
End: time.Date(2020, 2, 2, 2, 2, 2, 2, time.UTC).Format(common.SimpleTimeFormat),
End: time.Date(2020, 3, 2, 2, 2, 2, 2, time.UTC).Format(common.SimpleTimeFormat),
TimeInterval: int64(kline.OneHour.Duration()),
})
if err != nil {
@@ -712,10 +734,6 @@ func TestGetRecentTrades(t *testing.T) {
defer CleanRPCTest(t, engerino)
s := RPCServer{Engine: engerino}
_, err := s.GetRecentTrades(context.Background(), &gctrpc.GetSavedTradesRequest{})
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errInvalidArguments) {
t.Error(err)
}
@@ -730,10 +748,6 @@ func TestGetRecentTrades(t *testing.T) {
Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat),
End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat),
})
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errExchangeNotLoaded) {
t.Error(err)
}
@@ -756,10 +770,6 @@ func TestGetHistoricTrades(t *testing.T) {
defer CleanRPCTest(t, engerino)
s := RPCServer{Engine: engerino}
err := s.GetHistoricTrades(&gctrpc.GetSavedTradesRequest{}, nil)
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errInvalidArguments) {
t.Error(err)
}
@@ -774,10 +784,6 @@ func TestGetHistoricTrades(t *testing.T) {
Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat),
End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat),
}, nil)
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if !errors.Is(err, errExchangeNotLoaded) {
t.Error(err)
}
@@ -792,10 +798,6 @@ func TestGetHistoricTrades(t *testing.T) {
Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat),
End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat),
}, nil)
if err == nil {
t.Error(unexpectedLackOfError)
return
}
if err != common.ErrFunctionNotSupported {
t.Error(err)
}
@@ -803,39 +805,48 @@ func TestGetHistoricTrades(t *testing.T) {
func TestGetAccountInfo(t *testing.T) {
bot := CreateTestBot(t)
exch := bot.ExchangeManager.GetExchangeByName(testExchange)
b := exch.GetBase()
b.Name = "fake"
fakeExchange := fExchange{
IBotExchange: exch,
}
bot.ExchangeManager.Add(fakeExchange)
s := RPCServer{Engine: bot}
r, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakePassExchange, AssetType: asset.Spot.String()})
if err != nil {
t.Fatalf("TestGetAccountInfo: Failed to get account info: %s", err)
}
if r.Accounts[0].Currencies[0].TotalValue != 10 {
t.Fatal("TestGetAccountInfo: Unexpected value of the 'TotalValue'")
_, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Spot.String()})
if !errors.Is(err, nil) {
t.Errorf("expected %v, received %v", errAssetTypeDisabled, nil)
}
}
func TestUpdateAccountInfo(t *testing.T) {
bot := CreateTestBot(t)
exch := bot.ExchangeManager.GetExchangeByName(testExchange)
b := exch.GetBase()
b.Name = "fake"
fakeExchange := fExchange{
IBotExchange: exch,
}
bot.ExchangeManager.Add(fakeExchange)
s := RPCServer{Engine: bot}
getResponse, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakePassExchange, AssetType: asset.Spot.String()})
if err != nil {
t.Fatalf("TestGetAccountInfo: Failed to get account info: %s", err)
_, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Spot.String()})
if !errors.Is(err, nil) {
t.Errorf("expected %v, received %v", nil, err)
}
_, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakePassExchange, AssetType: asset.Futures.String()})
_, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Futures.String()})
if !errors.Is(err, errAssetTypeDisabled) {
t.Errorf("expected %v, received %v", errAssetTypeDisabled, err)
}
updateResp, err := s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{
Exchange: fakePassExchange,
_, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{
Exchange: "fake",
AssetType: asset.Spot.String(),
})
if !errors.Is(err, nil) {
t.Error(err)
} else if getResponse.Accounts[0].Currencies[0].TotalValue == updateResp.Accounts[0].Currencies[0].TotalValue {
t.Fatalf("TestGetAccountInfo: Unexpected value of the 'TotalValue'")
t.Errorf("expected %v, received %v", nil, err)
}
}
@@ -903,11 +914,8 @@ func TestGetOrders(t *testing.T) {
StartDate: time.Now().Format(common.SimpleTimeFormat),
EndDate: time.Now().Add(time.Hour).Format(common.SimpleTimeFormat),
})
if err != nil && !strings.Contains(err.Error(), "not supported due to unset/default API keys") {
t.Error(err)
}
if err == nil {
t.Error("expected error")
if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) {
t.Errorf("received '%v', expected '%v'", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
exch := engerino.GetExchangeByName(exchName)
@@ -982,18 +990,24 @@ func TestGetOrder(t *testing.T) {
t.Errorf("expected %v, received %v", asset.ErrNotSupported, err)
}
s.OrderManager, err = SetupOrderManager(engerino.ExchangeManager, engerino.CommunicationsManager, &engerino.ServicesWG, engerino.Settings.Verbose)
if err != nil {
t.Fatal(err)
}
err = s.OrderManager.Start()
if err != nil {
t.Fatal(err)
}
_, err = s.GetOrder(context.Background(), &gctrpc.GetOrderRequest{
Exchange: exchName,
OrderId: "",
Pair: p,
Asset: asset.Spot.String(),
})
if !errors.Is(err, errOrderIDCannotBeEmpty) {
t.Errorf("expected %v, received %v", errOrderIDCannotBeEmpty, err)
}
err = engerino.OrderManager.Start(engerino)
if err != nil {
t.Fatal(err)
if !errors.Is(err, ErrOrderIDCannotBeEmpty) {
t.Errorf("expected %v, received %v", ErrOrderIDCannotBeEmpty, err)
}
_, err = s.GetOrder(context.Background(), &gctrpc.GetOrderRequest{
Exchange: exchName,
@@ -1001,8 +1015,8 @@ func TestGetOrder(t *testing.T) {
Pair: p,
Asset: asset.Spot.String(),
})
if err == nil {
t.Error("expected error")
if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) {
t.Errorf("expected '%v' received '%v'", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
}
@@ -1021,8 +1035,8 @@ func TestCheckVars(t *testing.T) {
}
err = checkParams("Binance", e, asset.Spot, currency.NewPair(currency.BTC, currency.USDT))
if !errors.Is(err, errExchangeDisabled) {
t.Errorf("expected %v, got %v", errExchangeDisabled, err)
if !errors.Is(err, ErrExchangeNotFound) {
t.Errorf("expected %v, got %v", ErrExchangeNotFound, err)
}
e.SetEnabled(true)
@@ -1090,13 +1104,77 @@ func TestCheckVars(t *testing.T) {
t.Errorf("expected %v, got %v", errCurrencyNotEnabled, err)
}
e.GetBase().CurrencyPairs.EnablePair(
err = e.GetBase().CurrencyPairs.EnablePair(
asset.Spot,
currency.Pair{Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.USDT},
)
if err != nil {
t.Error(err)
}
err = checkParams("Binance", e, asset.Spot, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
}
}
func TestParseEvents(t *testing.T) {
var exchangeName = "Binance"
var testData []*withdraw.Response
for x := 0; x < 5; x++ {
test := fmt.Sprintf("test-%v", x)
resp := &withdraw.Response{
ID: withdraw.DryRunID,
Exchange: withdraw.ExchangeResponse{
Name: test,
ID: test,
Status: test,
},
RequestDetails: withdraw.Request{
Exchange: test,
Description: test,
Amount: 1.0,
},
}
if x%2 == 0 {
resp.RequestDetails.Currency = currency.AUD
resp.RequestDetails.Type = 1
resp.RequestDetails.Fiat = withdraw.FiatRequest{
Bank: banking.Account{
Enabled: false,
ID: fmt.Sprintf("test-%v", x),
BankName: fmt.Sprintf("test-%v-bank", x),
AccountName: "hello",
AccountNumber: fmt.Sprintf("test-%v", x),
BSBNumber: "123456",
SupportedCurrencies: "BTC-AUD",
SupportedExchanges: exchangeName,
},
}
} else {
resp.RequestDetails.Currency = currency.BTC
resp.RequestDetails.Type = 0
resp.RequestDetails.Crypto.Address = test
resp.RequestDetails.Crypto.FeeAmount = 0
resp.RequestDetails.Crypto.AddressTag = test
}
testData = append(testData, resp)
}
v := parseMultipleEvents(testData)
if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" {
t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse")
}
if testData == nil || len(testData) < 2 {
t.Fatal("expected at least 2")
}
v = parseSingleEvents(testData[0])
if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" {
t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse")
}
v = parseSingleEvents(testData[1])
if v.Event[0].Request.Type != 0 {
t.Fatal("Expected second entry in slice to return a Request.Type of Crypto")
}
}

View File

@@ -1,21 +0,0 @@
package subsystem
import "errors"
const (
// MsgSubSystemStarting message to return when subsystem is starting up
MsgSubSystemStarting = "manager starting..."
// MsgSubSystemStarted message to return when subsystem has started
MsgSubSystemStarted = "started."
// MsgSubSystemShuttingDown message to return when a subsystem is shutting down
MsgSubSystemShuttingDown = "shutting down..."
// MsgSubSystemShutdown message to return when a subsystem has shutdown
MsgSubSystemShutdown = "manager shutdown."
)
var (
// ErrSubSystemAlreadyStarted message to return when a subsystem is already started
ErrSubSystemAlreadyStarted = errors.New("manager already started")
// ErrSubSystemNotStarted message to return when subsystem not started
ErrSubSystemNotStarted = errors.New("not started")
)

85
engine/subsystem_types.go Normal file
View File

@@ -0,0 +1,85 @@
package engine
import (
"errors"
"github.com/thrasher-corp/gocryptotrader/communications/base"
"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/ticker"
"github.com/thrasher-corp/gocryptotrader/portfolio"
)
const (
// MsgSubSystemStarting message to return when subsystem is starting up
MsgSubSystemStarting = "starting..."
// MsgSubSystemStarted message to return when subsystem has started
MsgSubSystemStarted = "started."
// MsgSubSystemShuttingDown message to return when a subsystem is shutting down
MsgSubSystemShuttingDown = "shutting down..."
// MsgSubSystemShutdown message to return when a subsystem has shutdown
MsgSubSystemShutdown = "shutdown."
)
var (
// ErrSubSystemAlreadyStarted message to return when a subsystem is already started
ErrSubSystemAlreadyStarted = errors.New("subsystem already started")
// ErrSubSystemNotStarted message to return when subsystem not started
ErrSubSystemNotStarted = errors.New("subsystem not started")
// ErrNilSubsystem is returned when a subsystem hasn't had its Setup() func run
ErrNilSubsystem = errors.New("subsystem not setup")
errNilWaitGroup = errors.New("nil wait group received")
errNilExchangeManager = errors.New("cannot start with nil exchange manager")
)
// iExchangeManager limits exposure of accessible functions to exchange manager
// so that subsystems can use some functionality
type iExchangeManager interface {
GetExchanges() []exchange.IBotExchange
GetExchangeByName(string) exchange.IBotExchange
}
// iCommsManager limits exposure of accessible functions to communication manager
type iCommsManager interface {
PushEvent(evt base.Event)
}
// iOrderManager defines a limited scoped order manager
type iOrderManager interface {
Exists(*order.Detail) bool
Add(*order.Detail) error
Cancel(*order.Cancel) error
GetByExchangeAndID(string, string) (*order.Detail, error)
UpdateExistingOrder(*order.Detail) error
}
// iPortfolioManager limits exposure of accessible functions to portfolio manager
type iPortfolioManager interface {
GetPortfolioSummary() portfolio.Summary
IsWhiteListed(string) bool
IsExchangeSupported(string, string) bool
}
// iBot limits exposure of accessible functions to engine bot
type iBot interface {
SetupExchanges() error
}
// iWebsocketDataReceiver limits exposure of accessible functions to websocket data receiver
type iWebsocketDataReceiver interface {
IsRunning() bool
WebsocketDataReceiver(ws *stream.Websocket)
WebsocketDataHandler(string, interface{}) error
}
// iCurrencyPairSyncer defines a limited scoped currency pair syncer
type iCurrencyPairSyncer interface {
IsRunning() bool
PrintTickerSummary(*ticker.Price, string, error)
PrintOrderbookSummary(*orderbook.Base, string, error)
Update(string, currency.Pair, asset.Item, int, error) error
}

47
engine/subsystem_types.md Normal file
View File

@@ -0,0 +1,47 @@
# GoCryptoTrader package Subsystem_types
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/subsystem_types)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This subsystem_types package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Subsystem_types
+ Subsystem contains subsystems that are used at run time by an `engine.Engine`, however they can be setup and run individually.
+ Subsystems are designed to be self contained
+ All subsystems have a public `Setup(...) (..., error)` function to return a valid subsystem ready for use
+ Subsystems which are designed to be switched off also have `Start(...) error`, `IsRunning() bool` and `Stop(...) error` functions to allow the main `engine.Engine` instance to manage them
+ Common subsystem types such as errors can be found within the `subsystem.go` file
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

893
engine/sync_manager.go Normal file
View File

@@ -0,0 +1,893 @@
package engine
import (
"errors"
"fmt"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/log"
)
// const holds the sync item types
const (
SyncItemTicker = iota
SyncItemOrderbook
SyncItemTrade
SyncManagerName = "exchange_syncer"
)
var (
createdCounter = 0
removedCounter = 0
// DefaultSyncerWorkers limits the number of sync workers
DefaultSyncerWorkers = 15
// DefaultSyncerTimeoutREST the default time to switch from REST to websocket protocols without a response
DefaultSyncerTimeoutREST = time.Second * 15
DefaultSyncerTimeoutWebsocket = time.Minute
errNoSyncItemsEnabled = errors.New("no sync items enabled")
errUnknownSyncItem = errors.New("unknown sync item")
)
// setupSyncManager starts a new CurrencyPairSyncer
func setupSyncManager(c *Config, exchangeManager iExchangeManager, websocketDataReceiver iWebsocketDataReceiver, remoteConfig *config.RemoteControlConfig) (*syncManager, error) {
if !c.SyncOrderbook && !c.SyncTicker && !c.SyncTrades {
return nil, errNoSyncItemsEnabled
}
if exchangeManager == nil {
return nil, errNilExchangeManager
}
if remoteConfig == nil {
return nil, errNilConfig
}
if c.NumWorkers <= 0 {
c.NumWorkers = DefaultSyncerWorkers
}
if c.SyncTimeoutREST <= time.Duration(0) {
c.SyncTimeoutREST = DefaultSyncerTimeoutREST
}
if c.SyncTimeoutWebsocket <= time.Duration(0) {
c.SyncTimeoutWebsocket = DefaultSyncerTimeoutWebsocket
}
s := &syncManager{
config: *c,
remoteConfig: remoteConfig,
exchangeManager: exchangeManager,
websocketDataReceiver: websocketDataReceiver,
}
s.tickerBatchLastRequested = make(map[string]time.Time)
log.Debugf(log.SyncMgr,
"Exchange currency pair syncer config: continuous: %v ticker: %v"+
" orderbook: %v trades: %v workers: %v verbose: %v timeout REST: %v"+
" timeout Websocket: %v\n",
s.config.SyncContinuously, s.config.SyncTicker, s.config.SyncOrderbook,
s.config.SyncTrades, s.config.NumWorkers, s.config.Verbose, s.config.SyncTimeoutREST,
s.config.SyncTimeoutWebsocket)
return s, nil
}
// IsRunning safely checks whether the subsystem is running
func (m *syncManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Start runs the subsystem
func (m *syncManager) Start() error {
if m == nil {
return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return ErrSubSystemAlreadyStarted
}
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer started.")
exchanges := m.exchangeManager.GetExchanges()
for x := range exchanges {
exchangeName := exchanges[x].GetName()
supportsWebsocket := exchanges[x].SupportsWebsocket()
assetTypes := exchanges[x].GetAssetTypes()
supportsREST := exchanges[x].SupportsREST()
if !supportsREST && !supportsWebsocket {
log.Warnf(log.SyncMgr,
"Loaded exchange %s does not support REST or Websocket.\n",
exchangeName)
continue
}
var usingWebsocket bool
var usingREST bool
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)
usingREST = true
}
if !ws.IsConnected() && !ws.IsConnecting() {
if m.websocketDataReceiver.IsRunning() {
go m.websocketDataReceiver.WebsocketDataReceiver(ws)
}
err = ws.Connect()
if err == nil {
err = ws.FlushChannels()
}
if err != nil {
log.Errorf(log.SyncMgr,
"%s websocket failed to connect. Err: %s\n",
exchangeName,
err)
usingREST = true
} else {
usingWebsocket = true
}
} else {
usingWebsocket = true
}
} else if supportsREST {
usingREST = true
}
for y := range assetTypes {
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
}
wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y])
if !wsAssetSupported {
log.Warnf(log.SyncMgr,
"%s asset type %s websocket functionality is unsupported, REST fetching only.",
exchangeName,
assetTypes[y])
}
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 m.exists(exchangeName, enabledPairs[i], assetTypes[y]) {
continue
}
c := currencyPairSyncAgent{
AssetType: assetTypes[y],
Exchange: exchangeName,
Pair: enabledPairs[i],
}
sBase := syncBase{
IsUsingREST: usingREST || !wsAssetSupported,
IsUsingWebsocket: usingWebsocket && wsAssetSupported,
}
if m.config.SyncTicker {
c.Ticker = sBase
}
if m.config.SyncOrderbook {
c.Orderbook = sBase
}
if m.config.SyncTrades {
c.Trade = sBase
}
m.add(&c)
}
}
}
if atomic.CompareAndSwapInt32(&m.initSyncStarted, 0, 1) {
log.Debugf(log.SyncMgr,
"Exchange CurrencyPairSyncer initial sync started. %d items to process.\n",
createdCounter)
m.initSyncStartTime = time.Now()
}
go func() {
m.initSyncWG.Wait()
if atomic.CompareAndSwapInt32(&m.initSyncCompleted, 0, 1) {
log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync is complete.\n")
completedTime := time.Now()
log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync took %v [%v sync items].\n",
completedTime.Sub(m.initSyncStartTime), createdCounter)
if !m.config.SyncContinuously {
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopping.")
err := m.Stop()
if err != nil {
log.Error(log.SyncMgr, err)
}
return
}
}
}()
if atomic.LoadInt32(&m.initSyncCompleted) == 1 && !m.config.SyncContinuously {
return nil
}
for i := 0; i < m.config.NumWorkers; i++ {
go m.worker()
}
return nil
}
// Stop shuts down the exchange currency pair syncer
// Stop attempts to shutdown the subsystem
func (m *syncManager) Stop() error {
if m == nil {
return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 1, 0) {
return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrSubSystemNotStarted)
}
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopped.")
return nil
}
func (m *syncManager) get(exchangeName string, p currency.Pair, a asset.Item) (*currencyPairSyncAgent, error) {
m.mux.Lock()
defer m.mux.Unlock()
for x := range m.currencyPairs {
if m.currencyPairs[x].Exchange == exchangeName &&
m.currencyPairs[x].Pair.Equal(p) &&
m.currencyPairs[x].AssetType == a {
return &m.currencyPairs[x], nil
}
}
return nil, errors.New("exchange currency pair syncer not found")
}
func (m *syncManager) exists(exchangeName string, p currency.Pair, a asset.Item) bool {
m.mux.Lock()
defer m.mux.Unlock()
for x := range m.currencyPairs {
if m.currencyPairs[x].Exchange == exchangeName &&
m.currencyPairs[x].Pair.Equal(p) &&
m.currencyPairs[x].AssetType == a {
return true
}
}
return false
}
func (m *syncManager) add(c *currencyPairSyncAgent) {
m.mux.Lock()
defer m.mux.Unlock()
if m.config.SyncTicker {
if m.config.Verbose {
log.Debugf(log.SyncMgr,
"%s: Added ticker sync item %v: using websocket: %v using REST: %v\n",
c.Exchange, m.FormatCurrency(c.Pair).String(), c.Ticker.IsUsingWebsocket,
c.Ticker.IsUsingREST)
}
if atomic.LoadInt32(&m.initSyncCompleted) != 1 {
m.initSyncWG.Add(1)
createdCounter++
}
}
if m.config.SyncOrderbook {
if m.config.Verbose {
log.Debugf(log.SyncMgr,
"%s: Added orderbook sync item %v: using websocket: %v using REST: %v\n",
c.Exchange, m.FormatCurrency(c.Pair).String(), c.Orderbook.IsUsingWebsocket,
c.Orderbook.IsUsingREST)
}
if atomic.LoadInt32(&m.initSyncCompleted) != 1 {
m.initSyncWG.Add(1)
createdCounter++
}
}
if m.config.SyncTrades {
if m.config.Verbose {
log.Debugf(log.SyncMgr,
"%s: Added trade sync item %v: using websocket: %v using REST: %v\n",
c.Exchange, m.FormatCurrency(c.Pair).String(), c.Trade.IsUsingWebsocket,
c.Trade.IsUsingREST)
}
if atomic.LoadInt32(&m.initSyncCompleted) != 1 {
m.initSyncWG.Add(1)
createdCounter++
}
}
c.Created = time.Now()
m.currencyPairs = append(m.currencyPairs, *c)
}
func (m *syncManager) isProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int) bool {
m.mux.Lock()
defer m.mux.Unlock()
for x := range m.currencyPairs {
if m.currencyPairs[x].Exchange == exchangeName &&
m.currencyPairs[x].Pair.Equal(p) &&
m.currencyPairs[x].AssetType == a {
switch syncType {
case SyncItemTicker:
return m.currencyPairs[x].Ticker.IsProcessing
case SyncItemOrderbook:
return m.currencyPairs[x].Orderbook.IsProcessing
case SyncItemTrade:
return m.currencyPairs[x].Trade.IsProcessing
}
}
}
return false
}
func (m *syncManager) setProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int, processing bool) {
m.mux.Lock()
defer m.mux.Unlock()
for x := range m.currencyPairs {
if m.currencyPairs[x].Exchange == exchangeName &&
m.currencyPairs[x].Pair.Equal(p) &&
m.currencyPairs[x].AssetType == a {
switch syncType {
case SyncItemTicker:
m.currencyPairs[x].Ticker.IsProcessing = processing
case SyncItemOrderbook:
m.currencyPairs[x].Orderbook.IsProcessing = processing
case SyncItemTrade:
m.currencyPairs[x].Trade.IsProcessing = processing
}
}
}
}
// Update notifies the syncManager to change the last updated time for a exchange asset pair
func (m *syncManager) Update(exchangeName string, p currency.Pair, a asset.Item, syncType int, err error) error {
if m == nil {
return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem)
}
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrSubSystemNotStarted)
}
if atomic.LoadInt32(&m.initSyncStarted) != 1 {
return nil
}
switch syncType {
case SyncItemOrderbook:
if !m.config.SyncOrderbook {
return nil
}
case SyncItemTicker:
if !m.config.SyncTicker {
return nil
}
case SyncItemTrade:
if !m.config.SyncTrades {
return nil
}
default:
return fmt.Errorf("%v %w", syncType, errUnknownSyncItem)
}
m.mux.Lock()
defer m.mux.Unlock()
for x := range m.currencyPairs {
if m.currencyPairs[x].Exchange == exchangeName &&
m.currencyPairs[x].Pair.Equal(p) &&
m.currencyPairs[x].AssetType == a {
switch syncType {
case SyncItemTicker:
origHadData := m.currencyPairs[x].Ticker.HaveData
m.currencyPairs[x].Ticker.LastUpdated = time.Now()
if err != nil {
m.currencyPairs[x].Ticker.NumErrors++
}
m.currencyPairs[x].Ticker.HaveData = true
m.currencyPairs[x].Ticker.IsProcessing = false
if atomic.LoadInt32(&m.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s ticker sync complete %v [%d/%d].\n",
exchangeName,
m.FormatCurrency(p).String(),
removedCounter,
createdCounter)
m.initSyncWG.Done()
}
case SyncItemOrderbook:
origHadData := m.currencyPairs[x].Orderbook.HaveData
m.currencyPairs[x].Orderbook.LastUpdated = time.Now()
if err != nil {
m.currencyPairs[x].Orderbook.NumErrors++
}
m.currencyPairs[x].Orderbook.HaveData = true
m.currencyPairs[x].Orderbook.IsProcessing = false
if atomic.LoadInt32(&m.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s orderbook sync complete %v [%d/%d].\n",
exchangeName,
m.FormatCurrency(p).String(),
removedCounter,
createdCounter)
m.initSyncWG.Done()
}
case SyncItemTrade:
origHadData := m.currencyPairs[x].Trade.HaveData
m.currencyPairs[x].Trade.LastUpdated = time.Now()
if err != nil {
m.currencyPairs[x].Trade.NumErrors++
}
m.currencyPairs[x].Trade.HaveData = true
m.currencyPairs[x].Trade.IsProcessing = false
if atomic.LoadInt32(&m.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s trade sync complete %v [%d/%d].\n",
exchangeName,
m.FormatCurrency(p).String(),
removedCounter,
createdCounter)
m.initSyncWG.Done()
}
}
}
}
return nil
}
func (m *syncManager) worker() {
cleanup := func() {
log.Debugln(log.SyncMgr,
"Exchange CurrencyPairSyncer worker shutting down.")
}
defer cleanup()
for atomic.LoadInt32(&m.started) != 0 {
exchanges := m.exchangeManager.GetExchanges()
for x := range exchanges {
exchangeName := exchanges[x].GetName()
assetTypes := exchanges[x].GetAssetTypes()
supportsREST := exchanges[x].SupportsREST()
supportsRESTTickerBatching := exchanges[x].SupportsRESTTickerBatchUpdates()
var usingREST bool
var usingWebsocket bool
var switchedToRest bool
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)
usingREST = true
}
if ws.IsConnected() {
usingWebsocket = true
} else {
usingREST = true
}
} else if supportsREST {
usingREST = true
}
for y := range assetTypes {
if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil {
continue
}
wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y])
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(&m.started) == 0 {
return
}
if !m.exists(exchangeName, enabledPairs[i], assetTypes[y]) {
c := currencyPairSyncAgent{
AssetType: assetTypes[y],
Exchange: exchangeName,
Pair: enabledPairs[i],
}
sBase := syncBase{
IsUsingREST: usingREST || !wsAssetSupported,
IsUsingWebsocket: usingWebsocket && wsAssetSupported,
}
if m.config.SyncTicker {
c.Ticker = sBase
}
if m.config.SyncOrderbook {
c.Orderbook = sBase
}
if m.config.SyncTrades {
c.Trade = sBase
}
m.add(&c)
}
c, err := m.get(exchangeName, enabledPairs[i], assetTypes[y])
if err != nil {
log.Errorf(log.SyncMgr, "failed to get item. Err: %s\n", err)
continue
}
if switchedToRest && usingWebsocket {
log.Warnf(log.SyncMgr,
"%s %s: Websocket re-enabled, switching from rest to websocket\n",
c.Exchange, m.FormatCurrency(enabledPairs[i]).String())
switchedToRest = false
}
if m.config.SyncOrderbook {
if !m.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemOrderbook) {
if c.Orderbook.LastUpdated.IsZero() ||
(time.Since(c.Orderbook.LastUpdated) > m.config.SyncTimeoutREST && c.Orderbook.IsUsingREST) ||
(time.Since(c.Orderbook.LastUpdated) > m.config.SyncTimeoutWebsocket && c.Orderbook.IsUsingWebsocket) {
if c.Orderbook.IsUsingWebsocket {
if time.Since(c.Created) < m.config.SyncTimeoutWebsocket {
continue
}
if supportsREST {
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true)
c.Orderbook.IsUsingWebsocket = false
c.Orderbook.IsUsingREST = true
log.Warnf(log.SyncMgr,
"%s %s %s: No orderbook update after %s, switching from websocket to rest\n",
c.Exchange,
m.FormatCurrency(c.Pair).String(),
strings.ToUpper(c.AssetType.String()),
m.config.SyncTimeoutREST,
)
switchedToRest = true
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, false)
}
}
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true)
result, err := exchanges[x].UpdateOrderbook(c.Pair, c.AssetType)
m.PrintOrderbookSummary(result, "REST", err)
if err == nil {
if m.remoteConfig.WebsocketRPC.Enabled {
relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), exchangeName)
}
}
updateErr := m.Update(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, err)
if updateErr != nil {
log.Error(log.SyncMgr, updateErr)
}
} else {
time.Sleep(time.Millisecond * 50)
}
}
if m.config.SyncTicker {
if !m.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTicker) {
if c.Ticker.LastUpdated.IsZero() ||
(time.Since(c.Ticker.LastUpdated) > m.config.SyncTimeoutREST && c.Ticker.IsUsingREST) ||
(time.Since(c.Ticker.LastUpdated) > m.config.SyncTimeoutWebsocket && c.Ticker.IsUsingWebsocket) {
if c.Ticker.IsUsingWebsocket {
if time.Since(c.Created) < m.config.SyncTimeoutWebsocket {
continue
}
if supportsREST {
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true)
c.Ticker.IsUsingWebsocket = false
c.Ticker.IsUsingREST = true
log.Warnf(log.SyncMgr,
"%s %s %s: No ticker update after %s, switching from websocket to rest\n",
c.Exchange,
m.FormatCurrency(enabledPairs[i]).String(),
strings.ToUpper(c.AssetType.String()),
m.config.SyncTimeoutWebsocket,
)
switchedToRest = true
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, false)
}
}
if c.Ticker.IsUsingREST {
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true)
var result *ticker.Price
var err error
if supportsRESTTickerBatching {
m.mux.Lock()
batchLastDone, ok := m.tickerBatchLastRequested[exchangeName]
if !ok {
m.tickerBatchLastRequested[exchangeName] = time.Time{}
}
m.mux.Unlock()
if batchLastDone.IsZero() || time.Since(batchLastDone) > m.config.SyncTimeoutREST {
m.mux.Lock()
if m.config.Verbose {
log.Debugf(log.SyncMgr, "%s Init'ing REST ticker batching\n", exchangeName)
}
result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType)
m.tickerBatchLastRequested[exchangeName] = time.Now()
m.mux.Unlock()
} else {
if m.config.Verbose {
log.Debugf(log.SyncMgr, "%s Using recent batching cache\n", exchangeName)
}
result, err = exchanges[x].FetchTicker(c.Pair, c.AssetType)
}
} else {
result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType)
}
m.PrintTickerSummary(result, "REST", err)
if err == nil {
if m.remoteConfig.WebsocketRPC.Enabled {
relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName)
}
}
updateErr := m.Update(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, err)
if updateErr != nil {
log.Error(log.SyncMgr, updateErr)
}
}
} else {
time.Sleep(time.Millisecond * 50)
}
}
}
if m.config.SyncTrades {
if !m.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTrade) {
if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > m.config.SyncTimeoutREST {
m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, true)
err := m.Update(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, nil)
if err != nil {
log.Error(log.SyncMgr, err)
}
}
}
}
}
}
}
}
}
}
func printCurrencyFormat(price float64, displayCurrency currency.Code) string {
displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency)
if err != nil {
log.Errorf(log.SyncMgr, "Failed to get display symbol: %s\n", err)
}
return fmt.Sprintf("%s%.8f", displaySymbol, price)
}
func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64, displayCurrency currency.Code) string {
conv, err := currency.ConvertCurrency(origPrice,
origCurrency,
displayCurrency)
if err != nil {
log.Errorf(log.SyncMgr, "Failed to convert currency: %s\n", err)
}
displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency)
if err != nil {
log.Errorf(log.SyncMgr, "Failed to get display symbol: %s\n", err)
}
origSymbol, err := currency.GetSymbolByCurrencyName(origCurrency)
if err != nil {
log.Errorf(log.SyncMgr, "Failed to get original currency symbol for %s: %s\n",
origCurrency,
err)
}
return fmt.Sprintf("%s%.2f %s (%s%.2f %s)",
displaySymbol,
conv,
displayCurrency,
origSymbol,
origPrice,
origCurrency,
)
}
// PrintTickerSummary outputs the ticker results
func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string, err error) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return
}
if err != nil {
if err == common.ErrNotYetImplemented {
log.Warnf(log.SyncMgr, "Failed to get %s ticker. Error: %s\n",
protocol,
err)
return
}
log.Errorf(log.SyncMgr, "Failed to get %s ticker. Error: %s\n",
protocol,
err)
return
}
// ignoring error as not all tickers have volume populated and error is not actionable
_ = stats.Add(result.ExchangeName, result.Pair, result.AssetType, result.Last, result.Volume)
if result.Pair.Quote.IsFiatCurrency() &&
result.Pair.Quote != m.fiatDisplayCurrency &&
!m.fiatDisplayCurrency.IsEmpty() {
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",
result.ExchangeName,
protocol,
m.FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
printConvertCurrencyFormat(origCurrency, result.Last, m.fiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.Ask, m.fiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.Bid, m.fiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.High, m.fiatDisplayCurrency),
printConvertCurrencyFormat(origCurrency, result.Low, m.fiatDisplayCurrency),
result.Volume)
} else {
if result.Pair.Quote.IsFiatCurrency() &&
result.Pair.Quote == m.fiatDisplayCurrency &&
!m.fiatDisplayCurrency.IsEmpty() {
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n",
result.ExchangeName,
protocol,
m.FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
printCurrencyFormat(result.Last, m.fiatDisplayCurrency),
printCurrencyFormat(result.Ask, m.fiatDisplayCurrency),
printCurrencyFormat(result.Bid, m.fiatDisplayCurrency),
printCurrencyFormat(result.High, m.fiatDisplayCurrency),
printCurrencyFormat(result.Low, m.fiatDisplayCurrency),
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",
result.ExchangeName,
protocol,
m.FormatCurrency(result.Pair),
strings.ToUpper(result.AssetType.String()),
result.Last,
result.Ask,
result.Bid,
result.High,
result.Low,
result.Volume)
}
}
}
// FormatCurrency is a method that formats and returns a currency pair
// based on the user currency display preferences
func (m *syncManager) FormatCurrency(p currency.Pair) currency.Pair {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return p
}
return p.Format(m.delimiter, m.uppercase)
}
const (
book = "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n"
)
// PrintOrderbookSummary outputs orderbook results
func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol string, err error) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return
}
if err != nil {
if result == nil {
log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n",
protocol,
err)
return
}
if err == common.ErrNotYetImplemented {
log.Warnf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n",
protocol,
result.Exchange,
result.Pair,
result.Asset,
err)
return
}
log.Errorf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n",
protocol,
result.Exchange,
result.Pair,
result.Asset,
err)
return
}
bidsAmount, bidsValue := result.TotalBidsAmount()
asksAmount, asksValue := result.TotalAsksAmount()
var bidValueResult, askValueResult string
switch {
case result.Pair.Quote.IsFiatCurrency() && result.Pair.Quote != m.fiatDisplayCurrency && !m.fiatDisplayCurrency.IsEmpty():
origCurrency := result.Pair.Quote.Upper()
bidValueResult = printConvertCurrencyFormat(origCurrency, bidsValue, m.fiatDisplayCurrency)
askValueResult = printConvertCurrencyFormat(origCurrency, asksValue, m.fiatDisplayCurrency)
case result.Pair.Quote.IsFiatCurrency() && result.Pair.Quote == m.fiatDisplayCurrency && !m.fiatDisplayCurrency.IsEmpty():
bidValueResult = printCurrencyFormat(bidsValue, m.fiatDisplayCurrency)
askValueResult = printCurrencyFormat(asksValue, m.fiatDisplayCurrency)
default:
bidValueResult = strconv.FormatFloat(bidsValue, 'f', -1, 64)
askValueResult = strconv.FormatFloat(asksValue, 'f', -1, 64)
}
log.Infof(log.OrderBook, book,
result.Exchange,
protocol,
m.FormatCurrency(result.Pair),
strings.ToUpper(result.Asset.String()),
len(result.Bids),
bidsAmount,
result.Pair.Base,
bidValueResult,
len(result.Asks),
asksAmount,
result.Pair.Base,
askValueResult,
)
}
func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) {
evt := WebsocketEvent{
Data: result,
Event: event,
AssetType: assetType,
Exchange: exchangeName,
}
err := BroadcastWebsocketMessage(evt)
if !errors.Is(err, ErrWebsocketServiceNotRunning) {
log.Errorf(log.APIServerMgr, "Failed to broadcast websocket event %v. Error: %s\n",
event, err)
}
}

56
engine/sync_manager.md Normal file
View File

@@ -0,0 +1,56 @@
# GoCryptoTrader package Sync_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/sync_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This sync_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Sync_manager
+ The currency pair syncer subsystem is used to keep all trades, tickers and orderbooks up to date for all enabled exchange asset currency pairs
+ It can sync data via a websocket connection or REST and will switch between them if there has been no updates
+ In order to modify the behaviour of the currency pair syncer subsystem, you can change runtime parameters as detailed below:
| Config | Description | Example |
| ------ | ----------- | ------- |
| syncmanager | Determines whether the subsystem is enabled | `true` |
| tickersync | Enables ticker syncing for all enabled exchanges | `true`|
| orderbooksync | Enables orderbook syncing for all enabled exchanges | `true` |
| tradesync | Enables trade syncing for all enabled exchanges | `true` |
| syncworkers | The amount of workers (goroutines) to use for syncing exchange data | `15` |
| synccontinuously | Whether to sync exchange data continuously (ticker, orderbook and trades) | `true` |
| synctimeout | The amount of time in golang `time.Duration` format before the syncer will switch from one protocol to the other (e.g. from REST to websocket) | `15000000000` |
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

200
engine/sync_manager_test.go Normal file
View File

@@ -0,0 +1,200 @@
package engine
import (
"errors"
"sync/atomic"
"testing"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
func TestSetupSyncManager(t *testing.T) {
_, err := setupSyncManager(&Config{}, nil, nil, nil)
if !errors.Is(err, errNoSyncItemsEnabled) {
t.Errorf("error '%v', expected '%v'", err, errNoSyncItemsEnabled)
}
_, err = setupSyncManager(&Config{SyncTrades: true}, nil, nil, nil)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
_, err = setupSyncManager(&Config{SyncTrades: true}, &ExchangeManager{}, nil, nil)
if !errors.Is(err, errNilConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilConfig)
}
m, err := setupSyncManager(&Config{SyncTrades: true}, &ExchangeManager{}, nil, &config.RemoteControlConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expected manager")
}
}
func TestSyncManagerStart(t *testing.T) {
m, err := setupSyncManager(&Config{SyncTrades: true}, &ExchangeManager{}, nil, &config.RemoteControlConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
em.Add(exch)
m.exchangeManager = em
m.config.SyncContinuously = true
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
m = nil
err = m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestSyncManagerStop(t *testing.T) {
var m *syncManager
err := m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
em.Add(exch)
m, err = setupSyncManager(&Config{SyncTrades: true, SyncContinuously: true}, em, nil, &config.RemoteControlConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestPrintCurrencyFormat(t *testing.T) {
c := printCurrencyFormat(1337, currency.BTC)
if c == "" {
t.Error("expected formatted currency")
}
}
func TestPrintConvertCurrencyFormat(t *testing.T) {
c := printConvertCurrencyFormat(currency.BTC, 1337, currency.USD)
if c == "" {
t.Error("expected formatted currency")
}
}
func TestPrintTickerSummary(t *testing.T) {
var m *syncManager
m.PrintTickerSummary(&ticker.Price{}, "REST", nil)
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
em.Add(exch)
m, err = setupSyncManager(&Config{SyncTrades: true, SyncContinuously: true}, em, nil, &config.RemoteControlConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
atomic.StoreInt32(&m.started, 1)
m.PrintTickerSummary(&ticker.Price{
Pair: currency.NewPair(currency.BTC, currency.USDT),
}, "REST", nil)
m.fiatDisplayCurrency = currency.USD
m.PrintTickerSummary(&ticker.Price{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", nil)
m.fiatDisplayCurrency = currency.JPY
m.PrintTickerSummary(&ticker.Price{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", nil)
m.PrintTickerSummary(&ticker.Price{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", errors.New("test"))
m.PrintTickerSummary(&ticker.Price{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", common.ErrNotYetImplemented)
}
func TestPrintOrderbookSummary(t *testing.T) {
var m *syncManager
m.PrintOrderbookSummary(nil, "REST", nil)
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
em.Add(exch)
m, err = setupSyncManager(&Config{SyncTrades: true, SyncContinuously: true}, em, nil, &config.RemoteControlConfig{})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
atomic.StoreInt32(&m.started, 1)
m.PrintOrderbookSummary(&orderbook.Base{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", nil)
m.fiatDisplayCurrency = currency.USD
m.PrintOrderbookSummary(&orderbook.Base{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", nil)
m.fiatDisplayCurrency = currency.JPY
m.PrintOrderbookSummary(&orderbook.Base{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", nil)
m.PrintOrderbookSummary(&orderbook.Base{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", common.ErrNotYetImplemented)
m.PrintOrderbookSummary(&orderbook.Base{
Pair: currency.NewPair(currency.AUD, currency.USD),
}, "REST", errors.New("test"))
m.PrintOrderbookSummary(nil, "REST", errors.New("test"))
}
func TestRelayWebsocketEvent(t *testing.T) {
relayWebsocketEvent(nil, "", "", "")
}

View File

@@ -0,0 +1,64 @@
package engine
import (
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// syncBase stores information
type syncBase struct {
IsUsingWebsocket bool
IsUsingREST bool
IsProcessing bool
LastUpdated time.Time
HaveData bool
NumErrors int
}
// currencyPairSyncAgent stores the sync agent info
type currencyPairSyncAgent struct {
Created time.Time
Exchange string
AssetType asset.Item
Pair currency.Pair
Ticker syncBase
Orderbook syncBase
Trade syncBase
}
// Config stores the currency pair config
type Config struct {
SyncTicker bool
SyncOrderbook bool
SyncTrades bool
SyncContinuously bool
SyncTimeoutREST time.Duration
SyncTimeoutWebsocket time.Duration
NumWorkers int
Verbose bool
}
// syncManager stores the exchange currency pair syncer object
type syncManager struct {
initSyncCompleted int32
initSyncStarted int32
started int32
delimiter string
uppercase bool
initSyncStartTime time.Time
fiatDisplayCurrency currency.Code
mux sync.Mutex
initSyncWG sync.WaitGroup
currencyPairs []currencyPairSyncAgent
tickerBatchLastRequested map[string]time.Time
remoteConfig *config.RemoteControlConfig
config Config
exchangeManager iExchangeManager
websocketDataReceiver iWebsocketDataReceiver
}

View File

@@ -1,635 +0,0 @@
package engine
import (
"errors"
"strings"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/log"
)
// const holds the sync item types
const (
SyncItemTicker = iota
SyncItemOrderbook
SyncItemTrade
DefaultSyncerWorkers = 15
DefaultSyncerTimeoutREST = time.Second * 15
DefaultSyncerTimeoutWebsocket = time.Minute
)
var (
createdCounter = 0
removedCounter = 0
)
// NewCurrencyPairSyncer starts a new CurrencyPairSyncer
func NewCurrencyPairSyncer(c CurrencyPairSyncerConfig) (*ExchangeCurrencyPairSyncer, error) {
if !c.SyncOrderbook && !c.SyncTicker && !c.SyncTrades {
return nil, errors.New("no sync items enabled")
}
if c.NumWorkers <= 0 {
c.NumWorkers = DefaultSyncerWorkers
}
if c.SyncTimeoutREST <= time.Duration(0) {
c.SyncTimeoutREST = DefaultSyncerTimeoutREST
}
if c.SyncTimeoutWebsocket <= time.Duration(0) {
c.SyncTimeoutWebsocket = DefaultSyncerTimeoutWebsocket
}
s := ExchangeCurrencyPairSyncer{Cfg: c}
s.tickerBatchLastRequested = make(map[string]time.Time)
log.Debugf(log.SyncMgr,
"Exchange currency pair syncer config: continuous: %v ticker: %v"+
" orderbook: %v trades: %v workers: %v verbose: %v timeout REST: %v"+
" timeout Websocket: %v\n",
s.Cfg.SyncContinuously, s.Cfg.SyncTicker, s.Cfg.SyncOrderbook,
s.Cfg.SyncTrades, s.Cfg.NumWorkers, s.Cfg.Verbose, s.Cfg.SyncTimeoutREST,
s.Cfg.SyncTimeoutWebsocket)
return &s, nil
}
func (e *ExchangeCurrencyPairSyncer) get(exchangeName string, p currency.Pair, a asset.Item) (*CurrencyPairSyncAgent, error) {
e.mux.Lock()
defer e.mux.Unlock()
for x := range e.CurrencyPairs {
if e.CurrencyPairs[x].Exchange == exchangeName &&
e.CurrencyPairs[x].Pair.Equal(p) &&
e.CurrencyPairs[x].AssetType == a {
return &e.CurrencyPairs[x], nil
}
}
return nil, errors.New("exchange currency pair syncer not found")
}
func (e *ExchangeCurrencyPairSyncer) exists(exchangeName string, p currency.Pair, a asset.Item) bool {
e.mux.Lock()
defer e.mux.Unlock()
for x := range e.CurrencyPairs {
if e.CurrencyPairs[x].Exchange == exchangeName &&
e.CurrencyPairs[x].Pair.Equal(p) &&
e.CurrencyPairs[x].AssetType == a {
return true
}
}
return false
}
func (e *ExchangeCurrencyPairSyncer) add(c *CurrencyPairSyncAgent) {
e.mux.Lock()
defer e.mux.Unlock()
if e.Cfg.SyncTicker {
if e.Cfg.Verbose {
log.Debugf(log.SyncMgr,
"%s: Added ticker sync item %v: using websocket: %v using REST: %v\n",
c.Exchange, Bot.FormatCurrency(c.Pair).String(), c.Ticker.IsUsingWebsocket,
c.Ticker.IsUsingREST)
}
if atomic.LoadInt32(&e.initSyncCompleted) != 1 {
e.initSyncWG.Add(1)
createdCounter++
}
}
if e.Cfg.SyncOrderbook {
if e.Cfg.Verbose {
log.Debugf(log.SyncMgr,
"%s: Added orderbook sync item %v: using websocket: %v using REST: %v\n",
c.Exchange, Bot.FormatCurrency(c.Pair).String(), c.Orderbook.IsUsingWebsocket,
c.Orderbook.IsUsingREST)
}
if atomic.LoadInt32(&e.initSyncCompleted) != 1 {
e.initSyncWG.Add(1)
createdCounter++
}
}
if e.Cfg.SyncTrades {
if e.Cfg.Verbose {
log.Debugf(log.SyncMgr,
"%s: Added trade sync item %v: using websocket: %v using REST: %v\n",
c.Exchange, Bot.FormatCurrency(c.Pair).String(), c.Trade.IsUsingWebsocket,
c.Trade.IsUsingREST)
}
if atomic.LoadInt32(&e.initSyncCompleted) != 1 {
e.initSyncWG.Add(1)
createdCounter++
}
}
c.Created = time.Now()
e.CurrencyPairs = append(e.CurrencyPairs, *c)
}
func (e *ExchangeCurrencyPairSyncer) isProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int) bool {
e.mux.Lock()
defer e.mux.Unlock()
for x := range e.CurrencyPairs {
if e.CurrencyPairs[x].Exchange == exchangeName &&
e.CurrencyPairs[x].Pair.Equal(p) &&
e.CurrencyPairs[x].AssetType == a {
switch syncType {
case SyncItemTicker:
return e.CurrencyPairs[x].Ticker.IsProcessing
case SyncItemOrderbook:
return e.CurrencyPairs[x].Orderbook.IsProcessing
case SyncItemTrade:
return e.CurrencyPairs[x].Trade.IsProcessing
}
}
}
return false
}
func (e *ExchangeCurrencyPairSyncer) setProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int, processing bool) {
e.mux.Lock()
defer e.mux.Unlock()
for x := range e.CurrencyPairs {
if e.CurrencyPairs[x].Exchange == exchangeName &&
e.CurrencyPairs[x].Pair.Equal(p) &&
e.CurrencyPairs[x].AssetType == a {
switch syncType {
case SyncItemTicker:
e.CurrencyPairs[x].Ticker.IsProcessing = processing
case SyncItemOrderbook:
e.CurrencyPairs[x].Orderbook.IsProcessing = processing
case SyncItemTrade:
e.CurrencyPairs[x].Trade.IsProcessing = processing
}
}
}
}
func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair, a asset.Item, syncType int, err error) {
if atomic.LoadInt32(&e.initSyncStarted) != 1 {
return
}
switch syncType {
case SyncItemOrderbook:
if !e.Cfg.SyncOrderbook {
return
}
case SyncItemTicker:
if !e.Cfg.SyncTicker {
return
}
case SyncItemTrade:
if !e.Cfg.SyncTrades {
return
}
default:
log.Warnf(log.SyncMgr, "ExchangeCurrencyPairSyncer: unknown sync item %v\n", syncType)
return
}
e.mux.Lock()
defer e.mux.Unlock()
for x := range e.CurrencyPairs {
if e.CurrencyPairs[x].Exchange == exchangeName &&
e.CurrencyPairs[x].Pair.Equal(p) &&
e.CurrencyPairs[x].AssetType == a {
switch syncType {
case SyncItemTicker:
origHadData := e.CurrencyPairs[x].Ticker.HaveData
e.CurrencyPairs[x].Ticker.LastUpdated = time.Now()
if err != nil {
e.CurrencyPairs[x].Ticker.NumErrors++
}
e.CurrencyPairs[x].Ticker.HaveData = true
e.CurrencyPairs[x].Ticker.IsProcessing = false
if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s ticker sync complete %v [%d/%d].\n",
exchangeName,
Bot.FormatCurrency(p).String(),
removedCounter,
createdCounter)
e.initSyncWG.Done()
}
case SyncItemOrderbook:
origHadData := e.CurrencyPairs[x].Orderbook.HaveData
e.CurrencyPairs[x].Orderbook.LastUpdated = time.Now()
if err != nil {
e.CurrencyPairs[x].Orderbook.NumErrors++
}
e.CurrencyPairs[x].Orderbook.HaveData = true
e.CurrencyPairs[x].Orderbook.IsProcessing = false
if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s orderbook sync complete %v [%d/%d].\n",
exchangeName,
Bot.FormatCurrency(p).String(),
removedCounter,
createdCounter)
e.initSyncWG.Done()
}
case SyncItemTrade:
origHadData := e.CurrencyPairs[x].Trade.HaveData
e.CurrencyPairs[x].Trade.LastUpdated = time.Now()
if err != nil {
e.CurrencyPairs[x].Trade.NumErrors++
}
e.CurrencyPairs[x].Trade.HaveData = true
e.CurrencyPairs[x].Trade.IsProcessing = false
if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData {
removedCounter++
log.Debugf(log.SyncMgr, "%s trade sync complete %v [%d/%d].\n",
exchangeName,
Bot.FormatCurrency(p).String(),
removedCounter,
createdCounter)
e.initSyncWG.Done()
}
}
}
}
}
func (e *ExchangeCurrencyPairSyncer) worker() {
cleanup := func() {
log.Debugln(log.SyncMgr,
"Exchange CurrencyPairSyncer worker shutting down.")
}
defer cleanup()
for atomic.LoadInt32(&e.shutdown) != 1 {
exchanges := Bot.GetExchanges()
for x := range exchanges {
exchangeName := exchanges[x].GetName()
assetTypes := exchanges[x].GetAssetTypes()
supportsREST := exchanges[x].SupportsREST()
supportsRESTTickerBatching := exchanges[x].SupportsRESTTickerBatchUpdates()
var usingREST bool
var usingWebsocket bool
var switchedToRest bool
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)
usingREST = true
}
if ws.IsConnected() {
usingWebsocket = true
} else {
usingREST = true
}
} else if supportsREST {
usingREST = true
}
for y := range assetTypes {
if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil {
continue
}
wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y])
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
}
if !e.exists(exchangeName, enabledPairs[i], assetTypes[y]) {
c := CurrencyPairSyncAgent{
AssetType: assetTypes[y],
Exchange: exchangeName,
Pair: enabledPairs[i],
}
sBase := SyncBase{
IsUsingREST: usingREST || !wsAssetSupported,
IsUsingWebsocket: usingWebsocket && wsAssetSupported,
}
if e.Cfg.SyncTicker {
c.Ticker = sBase
}
if e.Cfg.SyncOrderbook {
c.Orderbook = sBase
}
if e.Cfg.SyncTrades {
c.Trade = sBase
}
e.add(&c)
}
c, err := e.get(exchangeName, enabledPairs[i], assetTypes[y])
if err != nil {
log.Errorf(log.SyncMgr, "failed to get item. Err: %s\n", err)
continue
}
if switchedToRest && usingWebsocket {
log.Warnf(log.SyncMgr,
"%s %s: Websocket re-enabled, switching from rest to websocket\n",
c.Exchange, Bot.FormatCurrency(enabledPairs[i]).String())
switchedToRest = false
}
if e.Cfg.SyncTicker {
if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTicker) {
if c.Ticker.LastUpdated.IsZero() ||
(time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeoutREST && c.Ticker.IsUsingREST) ||
(time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeoutWebsocket && c.Ticker.IsUsingWebsocket) {
if c.Ticker.IsUsingWebsocket {
if time.Since(c.Created) < e.Cfg.SyncTimeoutWebsocket {
continue
}
if supportsREST {
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true)
c.Ticker.IsUsingWebsocket = false
c.Ticker.IsUsingREST = true
log.Warnf(log.SyncMgr,
"%s %s %s: No ticker update after %s, switching from websocket to rest\n",
c.Exchange,
Bot.FormatCurrency(enabledPairs[i]).String(),
strings.ToUpper(c.AssetType.String()),
e.Cfg.SyncTimeoutWebsocket,
)
switchedToRest = true
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, false)
}
}
if c.Ticker.IsUsingREST {
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true)
var result *ticker.Price
var err error
if supportsRESTTickerBatching {
e.mux.Lock()
batchLastDone, ok := e.tickerBatchLastRequested[exchangeName]
if !ok {
e.tickerBatchLastRequested[exchangeName] = time.Time{}
}
e.mux.Unlock()
if batchLastDone.IsZero() || time.Since(batchLastDone) > e.Cfg.SyncTimeoutREST {
e.mux.Lock()
if e.Cfg.Verbose {
log.Debugf(log.SyncMgr, "%s Init'ing REST ticker batching\n", exchangeName)
}
result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType)
e.tickerBatchLastRequested[exchangeName] = time.Now()
e.mux.Unlock()
} else {
if e.Cfg.Verbose {
log.Debugf(log.SyncMgr, "%s Using recent batching cache\n", exchangeName)
}
result, err = exchanges[x].FetchTicker(c.Pair, c.AssetType)
}
} else {
result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType)
}
printTickerSummary(result, "REST", err)
if err == nil {
if Bot.Config.RemoteControl.WebsocketRPC.Enabled {
relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName)
}
}
e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, err)
}
} else {
time.Sleep(time.Millisecond * 50)
}
}
}
if e.Cfg.SyncOrderbook {
if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemOrderbook) {
if c.Orderbook.LastUpdated.IsZero() ||
(time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeoutREST && c.Orderbook.IsUsingREST) ||
(time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeoutWebsocket && c.Orderbook.IsUsingWebsocket) {
if c.Orderbook.IsUsingWebsocket {
if time.Since(c.Created) < e.Cfg.SyncTimeoutWebsocket {
continue
}
if supportsREST {
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true)
c.Orderbook.IsUsingWebsocket = false
c.Orderbook.IsUsingREST = true
log.Warnf(log.SyncMgr,
"%s %s %s: No orderbook update after %s, switching from websocket to rest\n",
c.Exchange,
Bot.FormatCurrency(c.Pair).String(),
strings.ToUpper(c.AssetType.String()),
e.Cfg.SyncTimeoutWebsocket,
)
switchedToRest = true
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, false)
}
}
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true)
result, err := exchanges[x].UpdateOrderbook(c.Pair, c.AssetType)
printOrderbookSummary(result, "REST", Bot, err)
if err == nil {
if Bot.Config.RemoteControl.WebsocketRPC.Enabled {
relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), exchangeName)
}
}
e.update(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, err)
} else {
time.Sleep(time.Millisecond * 50)
}
}
if e.Cfg.SyncTrades {
if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTrade) {
if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > e.Cfg.SyncTimeoutREST {
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, true)
e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, nil)
}
}
}
}
}
}
}
}
}
// Start starts an exchange currency pair syncer
func (e *ExchangeCurrencyPairSyncer) Start() {
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer started.")
exchanges := Bot.GetExchanges()
for x := range exchanges {
exchangeName := exchanges[x].GetName()
supportsWebsocket := exchanges[x].SupportsWebsocket()
assetTypes := exchanges[x].GetAssetTypes()
supportsREST := exchanges[x].SupportsREST()
if !supportsREST && !supportsWebsocket {
log.Warnf(log.SyncMgr,
"Loaded exchange %s does not support REST or Websocket.\n",
exchangeName)
continue
}
var usingWebsocket bool
var usingREST bool
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)
usingREST = true
}
if !ws.IsConnected() && !ws.IsConnecting() {
go Bot.WebsocketDataReceiver(ws)
err = ws.Connect()
if err == nil {
err = ws.FlushChannels()
}
if err != nil {
log.Errorf(log.SyncMgr,
"%s websocket failed to connect. Err: %s\n",
exchangeName,
err)
usingREST = true
} else {
usingWebsocket = true
}
} else {
usingWebsocket = true
}
} else if supportsREST {
usingREST = true
}
for y := range assetTypes {
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
}
wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y])
if !wsAssetSupported {
log.Warnf(log.SyncMgr,
"%s asset type %s websocket functionality is unsupported, REST fetching only.",
exchangeName,
assetTypes[y])
}
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
}
c := CurrencyPairSyncAgent{
AssetType: assetTypes[y],
Exchange: exchangeName,
Pair: enabledPairs[i],
}
sBase := SyncBase{
IsUsingREST: usingREST || !wsAssetSupported,
IsUsingWebsocket: usingWebsocket && wsAssetSupported,
}
if e.Cfg.SyncTicker {
c.Ticker = sBase
}
if e.Cfg.SyncOrderbook {
c.Orderbook = sBase
}
if e.Cfg.SyncTrades {
c.Trade = sBase
}
e.add(&c)
}
}
}
if atomic.CompareAndSwapInt32(&e.initSyncStarted, 0, 1) {
log.Debugf(log.SyncMgr,
"Exchange CurrencyPairSyncer initial sync started. %d items to process.\n",
createdCounter)
e.initSyncStartTime = time.Now()
}
go func() {
e.initSyncWG.Wait()
if atomic.CompareAndSwapInt32(&e.initSyncCompleted, 0, 1) {
log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync is complete.\n")
completedTime := time.Now()
log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync took %v [%v sync items].\n",
completedTime.Sub(e.initSyncStartTime), createdCounter)
if !e.Cfg.SyncContinuously {
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopping.")
e.Stop()
return
}
}
}()
if atomic.LoadInt32(&e.initSyncCompleted) == 1 && !e.Cfg.SyncContinuously {
return
}
for i := 0; i < e.Cfg.NumWorkers; i++ {
go e.worker()
}
}
// Stop shuts down the exchange currency pair syncer
func (e *ExchangeCurrencyPairSyncer) Stop() {
stopped := atomic.CompareAndSwapInt32(&e.shutdown, 0, 1)
if stopped {
log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopped.")
}
}

View File

@@ -1,43 +0,0 @@
package engine
import (
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
)
func TestNewCurrencyPairSyncer(t *testing.T) {
t.Skip()
if Bot == nil {
Bot = new(Engine)
}
Bot.Config = &config.Cfg
err := Bot.Config.LoadConfig("", true)
if err != nil {
t.Fatalf("TestNewExchangeSyncer: Failed to load config: %s", err)
}
Bot.Settings.DisableExchangeAutoPairUpdates = true
Bot.Settings.EnableExchangeWebsocketSupport = true
err = Bot.SetupExchanges()
if err != nil {
t.Log(err)
}
Bot.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(CurrencyPairSyncerConfig{
SyncTicker: true,
SyncOrderbook: false,
SyncTrades: false,
SyncContinuously: false,
})
if err != nil {
t.Errorf("NewCurrencyPairSyncer failed: err %s", err)
}
Bot.ExchangeCurrencyPairManager.Start()
time.Sleep(time.Second * 15)
Bot.ExchangeCurrencyPairManager.Stop()
}

View File

@@ -1,62 +0,0 @@
package engine
import (
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// CurrencyPairSyncerConfig stores the currency pair config
type CurrencyPairSyncerConfig struct {
SyncTicker bool
SyncOrderbook bool
SyncTrades bool
SyncContinuously bool
SyncTimeoutREST time.Duration
SyncTimeoutWebsocket time.Duration
NumWorkers int
Verbose bool
}
// ExchangeSyncerConfig stores the exchange syncer config
type ExchangeSyncerConfig struct {
SyncDepositAddresses bool
SyncOrders bool
}
// ExchangeCurrencyPairSyncer stores the exchange currency pair syncer object
type ExchangeCurrencyPairSyncer struct {
Cfg CurrencyPairSyncerConfig
CurrencyPairs []CurrencyPairSyncAgent
tickerBatchLastRequested map[string]time.Time
mux sync.Mutex
initSyncWG sync.WaitGroup
initSyncCompleted int32
initSyncStarted int32
initSyncStartTime time.Time
shutdown int32
}
// SyncBase stores information
type SyncBase struct {
IsUsingWebsocket bool
IsUsingREST bool
IsProcessing bool
LastUpdated time.Time
HaveData bool
NumErrors int
}
// CurrencyPairSyncAgent stores the sync agent info
type CurrencyPairSyncAgent struct {
Created time.Time
Exchange string
AssetType asset.Item
Pair currency.Pair
Ticker SyncBase
Orderbook SyncBase
Trade SyncBase
}

View File

@@ -1,135 +0,0 @@
package engine
import (
"errors"
"fmt"
"os"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/engine/subsystem"
"github.com/thrasher-corp/gocryptotrader/log"
ntpclient "github.com/thrasher-corp/gocryptotrader/ntpclient"
)
// vars related to the NTP manager
var (
NTPCheckInterval = time.Second * 30
NTPRetryLimit = 3
errNTPDisabled = errors.New("ntp client disabled")
)
// ntpManager starts the NTP manager
type ntpManager struct {
started int32
initialCheck bool
shutdown chan struct{}
}
func (n *ntpManager) Started() bool {
return atomic.LoadInt32(&n.started) == 1
}
func (n *ntpManager) Start() error {
if !atomic.CompareAndSwapInt32(&n.started, 0, 1) {
return fmt.Errorf("NTP manager %w", subsystem.ErrSubSystemAlreadyStarted)
}
if Bot.Config.NTPClient.Level == -1 {
atomic.CompareAndSwapInt32(&n.started, 1, 0)
return errors.New("NTP client disabled")
}
log.Debugln(log.TimeMgr, "NTP manager starting...")
if Bot.Config.NTPClient.Level == 0 && *Bot.Config.Logging.Enabled {
// Initial NTP check (prompts user on how we should proceed)
n.initialCheck = true
// Sometimes the NTP client can have transient issues due to UDP, try
// the default retry limits before giving up
check:
for i := 0; i < NTPRetryLimit; i++ {
err := n.processTime()
switch err {
case nil:
break check
case errNTPDisabled:
log.Debugln(log.TimeMgr, "NTP manager: User disabled NTP prompts. Exiting.")
atomic.CompareAndSwapInt32(&n.started, 1, 0)
return nil
default:
if i == NTPRetryLimit-1 {
return err
}
}
}
}
n.shutdown = make(chan struct{})
go n.run()
log.Debugln(log.TimeMgr, "NTP manager started.")
return nil
}
func (n *ntpManager) Stop() error {
if atomic.LoadInt32(&n.started) == 0 {
return fmt.Errorf("NTP manager %w", subsystem.ErrSubSystemNotStarted)
}
defer func() {
atomic.CompareAndSwapInt32(&n.started, 1, 0)
}()
log.Debugln(log.TimeMgr, "NTP manager shutting down...")
close(n.shutdown)
return nil
}
func (n *ntpManager) run() {
t := time.NewTicker(NTPCheckInterval)
defer func() {
t.Stop()
log.Debugln(log.TimeMgr, "NTP manager shutdown.")
}()
for {
select {
case <-n.shutdown:
return
case <-t.C:
err := n.processTime()
if err != nil {
log.Error(log.TimeMgr, err)
}
}
}
}
func (n *ntpManager) FetchNTPTime() time.Time {
return ntpclient.NTPClient(Bot.Config.NTPClient.Pool)
}
func (n *ntpManager) processTime() error {
NTPTime := n.FetchNTPTime()
currentTime := time.Now()
diff := NTPTime.Sub(currentTime)
configNTPTime := *Bot.Config.NTPClient.AllowedDifference
negDiff := *Bot.Config.NTPClient.AllowedNegativeDifference
configNTPNegativeTime := -negDiff
if diff > configNTPTime || diff < configNTPNegativeTime {
log.Warnf(log.TimeMgr, "NTP manager: Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v\n",
NTPTime,
currentTime,
diff,
configNTPTime,
configNTPNegativeTime)
if n.initialCheck {
n.initialCheck = false
disable, err := Bot.Config.DisableNTPCheck(os.Stdin)
if err != nil {
return fmt.Errorf("unable to disable NTP check: %s", err)
}
log.Infoln(log.TimeMgr, disable)
if Bot.Config.NTPClient.Level == -1 {
return errNTPDisabled
}
}
}
return nil
}

View File

@@ -1,434 +0,0 @@
package engine
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
// Const vars for websocket
const (
WebsocketResponseSuccess = "OK"
)
var (
wsHub *WebsocketHub
wsHubStarted bool
)
type wsCommandHandler struct {
authRequired bool
handler func(client *WebsocketClient, data interface{}) error
}
var wsHandlers = map[string]wsCommandHandler{
"auth": {authRequired: false, handler: wsAuth},
"getconfig": {authRequired: true, handler: wsGetConfig},
"saveconfig": {authRequired: true, handler: wsSaveConfig},
"getaccountinfo": {authRequired: true, handler: wsGetAccountInfo},
"gettickers": {authRequired: false, handler: wsGetTickers},
"getticker": {authRequired: false, handler: wsGetTicker},
"getorderbooks": {authRequired: false, handler: wsGetOrderbooks},
"getorderbook": {authRequired: false, handler: wsGetOrderbook},
"getexchangerates": {authRequired: false, handler: wsGetExchangeRates},
"getportfolio": {authRequired: true, handler: wsGetPortfolio},
}
// NewWebsocketHub Creates a new websocket hub
func NewWebsocketHub() *WebsocketHub {
return &WebsocketHub{
Broadcast: make(chan []byte),
Register: make(chan *WebsocketClient),
Unregister: make(chan *WebsocketClient),
Clients: make(map[*WebsocketClient]bool),
}
}
func (h *WebsocketHub) run() {
for {
select {
case client := <-h.Register:
h.Clients[client] = true
case client := <-h.Unregister:
if _, ok := h.Clients[client]; ok {
log.Debugln(log.WebsocketMgr, "websocket: disconnected client")
delete(h.Clients, client)
close(client.Send)
}
case message := <-h.Broadcast:
for client := range h.Clients {
select {
case client.Send <- message:
default:
log.Debugln(log.WebsocketMgr, "websocket: disconnected client")
close(client.Send)
delete(h.Clients, client)
}
}
}
}
}
// SendWebsocketMessage sends a websocket event to the client
func (c *WebsocketClient) SendWebsocketMessage(evt interface{}) error {
data, err := json.Marshal(evt)
if err != nil {
log.Errorf(log.WebsocketMgr, "websocket: failed to send message: %s\n", err)
return err
}
c.Send <- data
return nil
}
func (c *WebsocketClient) read() {
defer func() {
c.Hub.Unregister <- c
c.Conn.Close()
}()
for {
msgType, message, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Errorf(log.WebsocketMgr, "websocket: client disconnected, err: %s\n", err)
}
break
}
if msgType == websocket.TextMessage {
var evt WebsocketEvent
err := json.Unmarshal(message, &evt)
if err != nil {
log.Errorf(log.WebsocketMgr, "websocket: failed to decode JSON sent from client %s\n", err)
continue
}
if evt.Event == "" {
log.Warnln(log.WebsocketMgr, "websocket: client sent a blank event, disconnecting")
continue
}
dataJSON, err := json.Marshal(evt.Data)
if err != nil {
log.Errorln(log.WebsocketMgr, "websocket: client sent data we couldn't JSON decode")
break
}
req := strings.ToLower(evt.Event)
log.Debugf(log.WebsocketMgr, "websocket: request received: %s\n", req)
result, ok := wsHandlers[req]
if !ok {
log.Debugln(log.WebsocketMgr, "websocket: unsupported event")
continue
}
if result.authRequired && !c.Authenticated {
log.Warnf(log.WebsocketMgr, "Websocket: request %s failed due to unauthenticated request on an authenticated API\n", evt.Event)
c.SendWebsocketMessage(WebsocketEventResponse{Event: evt.Event, Error: "unauthorised request on authenticated API"})
continue
}
err = result.handler(c, dataJSON)
if err != nil {
log.Errorf(log.WebsocketMgr, "websocket: request %s failed. Error %s\n", evt.Event, err)
continue
}
}
}
}
func (c *WebsocketClient) write() {
defer func() {
c.Conn.Close()
}()
for { // nolint // ws client write routine loop
select {
case message, ok := <-c.Send:
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
log.Debugln(log.WebsocketMgr, "websocket: hub closed the channel")
return
}
w, err := c.Conn.NextWriter(websocket.TextMessage)
if err != nil {
log.Errorf(log.WebsocketMgr, "websocket: failed to create new io.writeCloser: %s\n", err)
return
}
w.Write(message)
// Add queued chat messages to the current websocket message
n := len(c.Send)
for i := 0; i < n; i++ {
w.Write(<-c.Send)
}
if err := w.Close(); err != nil {
log.Errorf(log.WebsocketMgr, "websocket: failed to close io.WriteCloser: %s\n", err)
return
}
}
}
}
// StartWebsocketHandler starts the websocket hub and routine which
// handles clients
func StartWebsocketHandler() {
if !wsHubStarted {
wsHubStarted = true
wsHub = NewWebsocketHub()
go wsHub.run()
}
}
// BroadcastWebsocketMessage meow
func BroadcastWebsocketMessage(evt WebsocketEvent) error {
if !wsHubStarted {
return errors.New("websocket service not started")
}
data, err := json.Marshal(evt)
if err != nil {
return err
}
wsHub.Broadcast <- data
return nil
}
// WebsocketClientHandler upgrades the HTTP connection to a websocket
// compatible one
func WebsocketClientHandler(w http.ResponseWriter, r *http.Request) {
if !wsHubStarted {
StartWebsocketHandler()
}
connectionLimit := Bot.Config.RemoteControl.WebsocketRPC.ConnectionLimit
numClients := len(wsHub.Clients)
if numClients >= connectionLimit {
log.Warnf(log.WebsocketMgr,
"websocket: client rejected due to websocket client limit reached. Number of clients %d. Limit %d.\n",
numClients, connectionLimit)
w.WriteHeader(http.StatusForbidden)
return
}
upgrader := websocket.Upgrader{
WriteBufferSize: 1024,
ReadBufferSize: 1024,
}
// Allow insecure origin if the Origin request header is present and not
// equal to the Host request header. Default to false
if Bot.Config.RemoteControl.WebsocketRPC.AllowInsecureOrigin {
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error(log.WebsocketMgr, err)
return
}
client := &WebsocketClient{Hub: wsHub, Conn: conn, Send: make(chan []byte, 1024)}
client.Hub.Register <- client
log.Debugf(log.WebsocketMgr,
"websocket: client connected. Connected clients: %d. Limit %d.\n",
numClients+1, connectionLimit)
go client.read()
go client.write()
}
func wsAuth(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "auth",
}
var auth WebsocketAuth
err := json.Unmarshal(data.([]byte), &auth)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
hashPW := crypto.HexEncodeToString(crypto.GetSHA256([]byte(Bot.Config.RemoteControl.Password)))
if auth.Username == Bot.Config.RemoteControl.Username && auth.Password == hashPW {
client.Authenticated = true
wsResp.Data = WebsocketResponseSuccess
log.Debugln(log.WebsocketMgr,
"websocket: client authenticated successfully")
return client.SendWebsocketMessage(wsResp)
}
wsResp.Error = "invalid username/password"
client.authFailures++
client.SendWebsocketMessage(wsResp)
if client.authFailures >= Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures {
log.Debugf(log.WebsocketMgr,
"websocket: disconnecting client, maximum auth failures threshold reached (failures: %d limit: %d)\n",
client.authFailures, Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures)
wsHub.Unregister <- client
return nil
}
log.Debugf(log.WebsocketMgr,
"websocket: client sent wrong username/password (failures: %d limit: %d)\n",
client.authFailures, Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures)
return nil
}
func wsGetConfig(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetConfig",
Data: Bot.Config,
}
return client.SendWebsocketMessage(wsResp)
}
func wsSaveConfig(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "SaveConfig",
}
var cfg config.Config
err := json.Unmarshal(data.([]byte), &cfg)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
err = Bot.Config.UpdateConfig(Bot.Settings.ConfigFile, &cfg, Bot.Settings.EnableDryRun)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
Bot.SetupExchanges()
wsResp.Data = WebsocketResponseSuccess
return client.SendWebsocketMessage(wsResp)
}
func wsGetAccountInfo(client *WebsocketClient, data interface{}) error {
accountInfo := Bot.GetAllEnabledExchangeAccountInfo()
wsResp := WebsocketEventResponse{
Event: "GetAccountInfo",
Data: accountInfo,
}
return client.SendWebsocketMessage(wsResp)
}
func wsGetTickers(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetTickers",
}
wsResp.Data = Bot.GetAllActiveTickers()
return client.SendWebsocketMessage(wsResp)
}
func wsGetTicker(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetTicker",
}
var tickerReq WebsocketOrderbookTickerRequest
err := json.Unmarshal(data.([]byte), &tickerReq)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
p, err := currency.NewPairFromString(tickerReq.Currency)
if err != nil {
return err
}
a, err := asset.New(tickerReq.AssetType)
if err != nil {
return err
}
result, err := Bot.GetSpecificTicker(p, tickerReq.Exchange, a)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
wsResp.Data = result
return client.SendWebsocketMessage(wsResp)
}
func wsGetOrderbooks(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetOrderbooks",
}
wsResp.Data = GetAllActiveOrderbooks()
return client.SendWebsocketMessage(wsResp)
}
func wsGetOrderbook(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetOrderbook",
}
var orderbookReq WebsocketOrderbookTickerRequest
err := json.Unmarshal(data.([]byte), &orderbookReq)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
p, err := currency.NewPairFromString(orderbookReq.Currency)
if err != nil {
return err
}
a, err := asset.New(orderbookReq.AssetType)
if err != nil {
return err
}
result, err := Bot.GetSpecificOrderbook(p, orderbookReq.Exchange, a)
if err != nil {
wsResp.Error = err.Error()
client.SendWebsocketMessage(wsResp)
return err
}
wsResp.Data = result
return client.SendWebsocketMessage(wsResp)
}
func wsGetExchangeRates(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetExchangeRates",
}
var err error
wsResp.Data, err = currency.GetExchangeRates()
if err != nil {
return err
}
return client.SendWebsocketMessage(wsResp)
}
func wsGetPortfolio(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetPortfolio",
}
wsResp.Data = Bot.Portfolio.GetPortfolioSummary()
return client.SendWebsocketMessage(wsResp)
}

View File

@@ -1,49 +0,0 @@
package engine
import "github.com/gorilla/websocket"
// WebsocketClient stores information related to the websocket client
type WebsocketClient struct {
Hub *WebsocketHub
Conn *websocket.Conn
Authenticated bool
authFailures int
Send chan []byte
}
// WebsocketHub stores the data for managing websocket clients
type WebsocketHub struct {
Clients map[*WebsocketClient]bool
Broadcast chan []byte
Register chan *WebsocketClient
Unregister chan *WebsocketClient
}
// WebsocketEvent is the struct used for websocket events
type WebsocketEvent struct {
Exchange string `json:"exchange,omitempty"`
AssetType string `json:"assetType,omitempty"`
Event string
Data interface{}
}
// WebsocketEventResponse is the struct used for websocket event responses
type WebsocketEventResponse struct {
Event string `json:"event"`
Data interface{} `json:"data"`
Error string `json:"error"`
}
// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook
// requests
type WebsocketOrderbookTickerRequest struct {
Exchange string `json:"exchangeName"`
Currency string `json:"currency"`
AssetType string `json:"assetType"`
}
// WebsocketAuth is a struct used for
type WebsocketAuth struct {
Username string `json:"username"`
Password string `json:"password"`
}

View File

@@ -0,0 +1,332 @@
package engine
import (
"fmt"
"sync/atomic"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"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/log"
)
// setupWebsocketRoutineManager creates a new websocket routine manager
func setupWebsocketRoutineManager(exchangeManager iExchangeManager, orderManager iOrderManager, syncer iCurrencyPairSyncer, cfg *config.CurrencyConfig, verbose bool) (*websocketRoutineManager, error) {
if exchangeManager == nil {
return nil, errNilExchangeManager
}
if orderManager == nil {
return nil, errNilOrderManager
}
if syncer == nil {
return nil, errNilCurrencyPairSyncer
}
if cfg == nil {
return nil, errNilCurrencyConfig
}
if cfg.CurrencyPairFormat == nil && verbose {
return nil, errNilCurrencyPairFormat
}
return &websocketRoutineManager{
verbose: verbose,
exchangeManager: exchangeManager,
orderManager: orderManager,
syncer: syncer,
currencyConfig: cfg,
shutdown: make(chan struct{}),
}, nil
}
// Start runs the subsystem
func (m *websocketRoutineManager) Start() error {
if m == nil {
return fmt.Errorf("websocket routine manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
return ErrSubSystemAlreadyStarted
}
m.shutdown = make(chan struct{})
go m.websocketRoutine()
return nil
}
// IsRunning safely checks whether the subsystem is running
func (m *websocketRoutineManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
}
// Stop attempts to shutdown the subsystem
func (m *websocketRoutineManager) Stop() error {
if m == nil {
return fmt.Errorf("websocket routine manager %w", ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&m.started, 1, 0) {
return fmt.Errorf("websocket routine manager %w", ErrSubSystemNotStarted)
}
close(m.shutdown)
m.wg.Wait()
return nil
}
// websocketRoutine Initial routine management system for websocket
func (m *websocketRoutineManager) websocketRoutine() {
if m.verbose {
log.Debugln(log.WebsocketMgr, "Connecting exchange websocket services...")
}
exchanges := m.exchangeManager.GetExchanges()
for i := range exchanges {
go func(i int) {
if exchanges[i].SupportsWebsocket() {
if m.verbose {
log.Debugf(log.WebsocketMgr,
"Exchange %s websocket support: Yes Enabled: %v\n",
exchanges[i].GetName(),
common.IsEnabled(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
}
// 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 m.WebsocketDataReceiver(ws)
if ws.IsEnabled() {
err = ws.Connect()
if err != nil {
log.Errorf(log.WebsocketMgr, "%v\n", err)
}
err = ws.FlushChannels()
if err != nil {
log.Errorf(log.WebsocketMgr, "Failed to subscribe: %v\n", err)
}
}
} else if m.verbose {
log.Debugf(log.WebsocketMgr,
"Exchange %s websocket support: No\n",
exchanges[i].GetName(),
)
}
}(i)
}
}
// WebsocketDataReceiver handles websocket data coming from a websocket feed
// associated with an exchange
func (m *websocketRoutineManager) WebsocketDataReceiver(ws *stream.Websocket) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return
}
m.wg.Add(1)
defer m.wg.Done()
for {
select {
case <-m.shutdown:
return
case data := <-ws.ToRoutine:
err := m.WebsocketDataHandler(ws.GetName(), data)
if err != nil {
log.Error(log.WebsocketMgr, err)
}
}
}
}
// WebsocketDataHandler is a central point for exchange websocket implementations to send
// processed data. WebsocketDataHandler will then pass that to an appropriate handler
func (m *websocketRoutineManager) WebsocketDataHandler(exchName string, data interface{}) error {
if data == nil {
return fmt.Errorf("exchange %s nil data sent to websocket",
exchName)
}
switch d := data.(type) {
case string:
log.Info(log.WebsocketMgr, d)
case error:
return fmt.Errorf("exchange %s websocket error - %s", exchName, data)
case stream.FundingData:
if m.verbose {
log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v",
exchName,
m.FormatCurrency(d.CurrencyPair),
d.AssetType,
d)
}
case *ticker.Price:
if m.syncer.IsRunning() {
err := m.syncer.Update(exchName,
d.Pair,
d.AssetType,
SyncItemTicker,
nil)
if err != nil {
return err
}
}
err := ticker.ProcessTicker(d)
if err != nil {
return err
}
m.syncer.PrintTickerSummary(d, "websocket", err)
case stream.KlineData:
if m.verbose {
log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v",
exchName,
m.FormatCurrency(d.Pair),
d.AssetType,
d)
}
case *orderbook.Base:
if m.syncer.IsRunning() {
err := m.syncer.Update(exchName,
d.Pair,
d.Asset,
SyncItemOrderbook,
nil)
if err != nil {
return err
}
}
m.syncer.PrintOrderbookSummary(d, "websocket", nil)
case *order.Detail:
m.printOrderSummary(d)
if !m.orderManager.Exists(d) {
err := m.orderManager.Add(d)
if err != nil {
return err
}
} else {
od, err := m.orderManager.GetByExchangeAndID(d.Exchange, d.ID)
if err != nil {
return err
}
od.UpdateOrderFromDetail(d)
err = m.orderManager.UpdateExistingOrder(od)
if err != nil {
return err
}
}
case *order.Modify:
m.printOrderChangeSummary(d)
od, err := m.orderManager.GetByExchangeAndID(d.Exchange, d.ID)
if err != nil {
return err
}
od.UpdateOrderFromModify(d)
err = m.orderManager.UpdateExistingOrder(od)
if err != nil {
return err
}
case order.ClassificationError:
return fmt.Errorf("%w %s", d.Err, d.Error())
case stream.UnhandledMessageWarning:
log.Warn(log.WebsocketMgr, d.Message)
case account.Change:
if m.verbose {
m.printAccountHoldingsChangeSummary(d)
}
default:
if m.verbose {
log.Warnf(log.WebsocketMgr,
"%s websocket Unknown type: %+v",
exchName,
d)
}
}
return nil
}
// FormatCurrency is a method that formats and returns a currency pair
// based on the user currency display preferences
func (m *websocketRoutineManager) FormatCurrency(p currency.Pair) currency.Pair {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return p
}
return p.Format(m.currencyConfig.CurrencyPairFormat.Delimiter,
m.currencyConfig.CurrencyPairFormat.Uppercase)
}
// printOrderChangeSummary this function will be deprecated when a order manager
// update is done.
func (m *websocketRoutineManager) printOrderChangeSummary(o *order.Modify) {
if m == nil || atomic.LoadInt32(&m.started) == 0 || o == nil {
return
}
log.Debugf(log.WebsocketMgr,
"Order Change: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f",
o.Exchange,
o.AssetType,
o.Pair,
o.Status,
o.Type,
o.Side,
o.ID,
o.ClientOrderID,
o.Price,
o.Amount,
o.ExecutedAmount,
o.RemainingAmount)
}
// printOrderSummary this function will be deprecated when a order manager
// update is done.
func (m *websocketRoutineManager) printOrderSummary(o *order.Detail) {
if m == nil || atomic.LoadInt32(&m.started) == 0 || o == nil {
return
}
log.Debugf(log.WebsocketMgr,
"New Order: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f",
o.Exchange,
o.AssetType,
o.Pair,
o.Status,
o.Type,
o.Side,
o.ID,
o.ClientOrderID,
o.Price,
o.Amount,
o.ExecutedAmount,
o.RemainingAmount)
}
// printAccountHoldingsChangeSummary this function will be deprecated when a
// account holdings update is done.
func (m *websocketRoutineManager) printAccountHoldingsChangeSummary(o account.Change) {
if m == nil || atomic.LoadInt32(&m.started) == 0 {
return
}
log.Debugf(log.WebsocketMgr,
"Account Holdings Balance Changed: %s %s %s has changed balance by %f for account: %s",
o.Exchange,
o.Asset,
o.Currency,
o.Amount,
o.Account)
}

View File

@@ -0,0 +1,48 @@
# GoCryptoTrader package Websocketroutine_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/websocketroutine_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This websocketroutine_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Websocketroutine_manager
+ The websocket routine manager subsystem is used process websocket data in a unified manner across enabled exchanges with websocket support
+ It can help process orders to the order manager subsystem when it receives new data
+ Logs output of ticker and orderbook updates
+ The websocket routine manager subsystem can be enabled or disabled via runtime command `-websocketroutine=false` defaulting to true
+ Logs can be customised to display values the config value `fiatDisplayCurrency` under `currencyConfig`
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,260 @@
package engine
import (
"errors"
"sync"
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
"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/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
func TestWebsocketRoutineManagerSetup(t *testing.T) {
_, err := setupWebsocketRoutineManager(nil, nil, nil, nil, false)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
_, err = setupWebsocketRoutineManager(SetupExchangeManager(), nil, nil, nil, false)
if !errors.Is(err, errNilOrderManager) {
t.Errorf("error '%v', expected '%v'", err, errNilOrderManager)
}
_, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, nil, nil, false)
if !errors.Is(err, errNilCurrencyPairSyncer) {
t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairSyncer)
}
_, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, nil, false)
if !errors.Is(err, errNilCurrencyConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilCurrencyConfig)
}
_, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, true)
if !errors.Is(err, errNilCurrencyPairFormat) {
t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairFormat)
}
m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expecting manager")
}
}
func TestWebsocketRoutineManagerStart(t *testing.T) {
var m *websocketRoutineManager
err := m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
cfg := &config.CurrencyConfig{CurrencyPairFormat: &config.CurrencyPairFormatConfig{
Uppercase: false,
Delimiter: "-",
}}
m, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
}
func TestWebsocketRoutineManagerIsRunning(t *testing.T) {
var m *websocketRoutineManager
if m.IsRunning() {
t.Error("expected false")
}
m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.IsRunning() {
t.Error("expected false")
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if !m.IsRunning() {
t.Error("expected true")
}
}
func TestWebsocketRoutineManagerStop(t *testing.T) {
var m *websocketRoutineManager
err := m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
m, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestWebsocketRoutineManagerHandleData(t *testing.T) {
var exchName = "Bitstamp"
var wg sync.WaitGroup
em := SetupExchangeManager()
exch, err := em.NewExchangeByName(exchName)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
exch.SetDefaults()
em.Add(exch)
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = om.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
cfg := &config.CurrencyConfig{CurrencyPairFormat: &config.CurrencyPairFormatConfig{
Uppercase: false,
Delimiter: "-",
}}
m, err := setupWebsocketRoutineManager(em, om, &syncManager{}, cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
var orderID = "1337"
err = m.WebsocketDataHandler(exchName, errors.New("error"))
if err == nil {
t.Error("Error not handled correctly")
}
err = m.WebsocketDataHandler(exchName, nil)
if err == nil {
t.Error("Expected nil data error")
}
err = m.WebsocketDataHandler(exchName, stream.FundingData{})
if err != nil {
t.Error(err)
}
err = m.WebsocketDataHandler(exchName, &ticker.Price{
ExchangeName: exchName,
Pair: currency.NewPair(currency.BTC, currency.USDC),
AssetType: asset.Spot,
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.WebsocketDataHandler(exchName, stream.KlineData{})
if err != nil {
t.Error(err)
}
origOrder := &order.Detail{
Exchange: exchName,
ID: orderID,
Amount: 1337,
Price: 1337,
}
err = m.WebsocketDataHandler(exchName, origOrder)
if err != nil {
t.Error(err)
}
// Send it again since it exists now
err = m.WebsocketDataHandler(exchName, &order.Detail{
Exchange: exchName,
ID: orderID,
Amount: 1338,
})
if err != nil {
t.Error(err)
}
updated, err := m.orderManager.GetByExchangeAndID(origOrder.Exchange, origOrder.ID)
if err != nil {
t.Error(err)
}
if updated.Amount != 1338 {
t.Error("Bad pipeline")
}
err = m.WebsocketDataHandler(exchName, &order.Modify{
Exchange: "Bitstamp",
ID: orderID,
Status: order.Active,
})
if err != nil {
t.Error(err)
}
updated, err = m.orderManager.GetByExchangeAndID(origOrder.Exchange, origOrder.ID)
if err != nil {
t.Error(err)
}
if updated.Status != order.Active {
t.Error("Expected order to be modified to Active")
}
// Send some gibberish
err = m.WebsocketDataHandler(exchName, order.Stop)
if err != nil {
t.Error(err)
}
err = m.WebsocketDataHandler(exchName, stream.UnhandledMessageWarning{
Message: "there's an issue here's a tissue"},
)
if err != nil {
t.Error(err)
}
classificationError := order.ClassificationError{
Exchange: "test",
OrderID: "one",
Err: errors.New("lol"),
}
err = m.WebsocketDataHandler(exchName, classificationError)
if err == nil {
t.Error("Expected error")
}
if !errors.Is(err, classificationError.Err) {
t.Errorf("error '%v', expected '%v'", err, classificationError.Err)
}
err = m.WebsocketDataHandler(exchName, &orderbook.Base{
Exchange: "Bitstamp",
Pair: currency.NewPair(currency.BTC, currency.USD),
})
if err != nil {
t.Error(err)
}
err = m.WebsocketDataHandler(exchName, "this is a test string")
if err != nil {
t.Error(err)
}
}

View File

@@ -0,0 +1,27 @@
package engine
import (
"errors"
"sync"
"github.com/thrasher-corp/gocryptotrader/config"
)
// websocketRoutineManager is used to process websocket updates from a unified location
type websocketRoutineManager struct {
started int32
verbose bool
exchangeManager iExchangeManager
orderManager iOrderManager
syncer iCurrencyPairSyncer
currencyConfig *config.CurrencyConfig
shutdown chan struct{}
wg sync.WaitGroup
}
var (
errNilOrderManager = errors.New("nil order manager received")
errNilCurrencyPairSyncer = errors.New("nil currency pair syncer received")
errNilCurrencyConfig = errors.New("nil currency config received")
errNilCurrencyPairFormat = errors.New("nil currency pair format received")
)

View File

@@ -1,245 +0,0 @@
package engine
import (
"fmt"
"time"
withdrawDataStore "github.com/thrasher-corp/gocryptotrader/database/repository/withdraw"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// ErrWithdrawRequestNotFound message to display when no record is found
ErrWithdrawRequestNotFound = "%v not found"
// ErrRequestCannotbeNil message to display when request is nil
ErrRequestCannotbeNil = "request cannot be nil"
// StatusError const for for "error" string
StatusError = "error"
)
// SubmitWithdrawal performs validation and submits a new withdraw request to
// exchange
func (bot *Engine) SubmitWithdrawal(req *withdraw.Request) (*withdraw.Response, error) {
if req == nil {
return nil, withdraw.ErrRequestCannotBeNil
}
exch := bot.GetExchangeByName(req.Exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
resp := &withdraw.Response{
Exchange: withdraw.ExchangeResponse{
Name: req.Exchange,
},
RequestDetails: *req,
}
var err error
if bot.Settings.EnableDryRun {
log.Warnln(log.Global, "Dry run enabled, no withdrawal request will be submitted or have an event created")
resp.ID = withdraw.DryRunID
resp.Exchange.Status = "dryrun"
resp.Exchange.ID = withdraw.DryRunID.String()
} else {
var ret *withdraw.ExchangeResponse
if req.Type == withdraw.Fiat {
ret, err = exch.WithdrawFiatFunds(req)
if err != nil {
resp.Exchange.ID = StatusError
resp.Exchange.Status = err.Error()
} else {
resp.Exchange.Status = ret.Status
resp.Exchange.ID = ret.ID
}
} else if req.Type == withdraw.Crypto {
ret, err = exch.WithdrawCryptocurrencyFunds(req)
if err != nil {
resp.Exchange.ID = StatusError
resp.Exchange.Status = err.Error()
} else {
resp.Exchange.Status = ret.Status
resp.Exchange.ID = ret.ID
}
}
withdrawDataStore.Event(resp)
}
if err == nil {
withdraw.Cache.Add(resp.ID, resp)
}
return resp, nil
}
// WithdrawalEventByID returns a withdrawal request by ID
func WithdrawalEventByID(id string) (*withdraw.Response, error) {
v := withdraw.Cache.Get(id)
if v != nil {
return v.(*withdraw.Response), nil
}
l, err := withdrawDataStore.GetEventByUUID(id)
if err != nil {
return nil, fmt.Errorf(ErrWithdrawRequestNotFound, id)
}
withdraw.Cache.Add(id, l)
return l, nil
}
// WithdrawalEventByExchange returns a withdrawal request by ID
func WithdrawalEventByExchange(exchange string, limit int) ([]*withdraw.Response, error) {
return withdrawDataStore.GetEventsByExchange(exchange, limit)
}
// WithdrawEventByDate returns a withdrawal request by ID
func WithdrawEventByDate(exchange string, start, end time.Time, limit int) ([]*withdraw.Response, error) {
return withdrawDataStore.GetEventsByDate(exchange, start, end, limit)
}
// WithdrawalEventByExchangeID returns a withdrawal request by Exchange ID
func WithdrawalEventByExchangeID(exchange, id string) (*withdraw.Response, error) {
return withdrawDataStore.GetEventByExchangeID(exchange, id)
}
func parseMultipleEvents(ret []*withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse {
v := &gctrpc.WithdrawalEventsByExchangeResponse{}
for x := range ret {
tempEvent := &gctrpc.WithdrawalEventResponse{
Id: ret[x].ID.String(),
Exchange: &gctrpc.WithdrawlExchangeEvent{
Name: ret[x].Exchange.Name,
Id: ret[x].Exchange.ID,
Status: ret[x].Exchange.Status,
},
Request: &gctrpc.WithdrawalRequestEvent{
Currency: ret[x].RequestDetails.Currency.String(),
Description: ret[x].RequestDetails.Description,
Amount: ret[x].RequestDetails.Amount,
Type: int32(ret[x].RequestDetails.Type),
},
}
tempEvent.CreatedAt = timestamppb.New(ret[x].CreatedAt)
if err := tempEvent.CreatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseMultipleEvents CreatedAt: %s", err)
}
tempEvent.UpdatedAt = timestamppb.New(ret[x].UpdatedAt)
if err := tempEvent.UpdatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseMultipleEvents UpdatedAt: %s", err)
}
if ret[x].RequestDetails.Type == withdraw.Crypto {
tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent)
tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{
Address: ret[x].RequestDetails.Crypto.Address,
AddressTag: ret[x].RequestDetails.Crypto.AddressTag,
Fee: ret[x].RequestDetails.Crypto.FeeAmount,
}
} else if ret[x].RequestDetails.Type == withdraw.Fiat {
if ret[x].RequestDetails.Fiat != (withdraw.FiatRequest{}) {
tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent)
tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{
BankName: ret[x].RequestDetails.Fiat.Bank.BankName,
AccountName: ret[x].RequestDetails.Fiat.Bank.AccountName,
AccountNumber: ret[x].RequestDetails.Fiat.Bank.AccountNumber,
Bsb: ret[x].RequestDetails.Fiat.Bank.BSBNumber,
Swift: ret[x].RequestDetails.Fiat.Bank.SWIFTCode,
Iban: ret[x].RequestDetails.Fiat.Bank.IBAN,
}
}
}
v.Event = append(v.Event, tempEvent)
}
return v
}
func parseWithdrawalsHistory(ret []exchange.WithdrawalHistory, exchName string, limit int) *gctrpc.WithdrawalEventsByExchangeResponse {
v := &gctrpc.WithdrawalEventsByExchangeResponse{}
for x := range ret {
if limit > 0 && x >= limit {
return v
}
tempEvent := &gctrpc.WithdrawalEventResponse{
Id: ret[x].TransferID,
Exchange: &gctrpc.WithdrawlExchangeEvent{
Name: exchName,
Status: ret[x].Status,
},
Request: &gctrpc.WithdrawalRequestEvent{
Currency: ret[x].Currency,
Description: ret[x].Description,
Amount: ret[x].Amount,
},
}
tempEvent.UpdatedAt = timestamppb.New(ret[x].Timestamp)
if err := tempEvent.UpdatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseWithdrawalsHistory UpdatedAt: %s", err)
}
tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{
Address: ret[x].CryptoToAddress,
Fee: ret[x].Fee,
TxId: ret[x].CryptoTxID,
}
v.Event = append(v.Event, tempEvent)
}
return v
}
func parseSingleEvents(ret *withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse {
tempEvent := &gctrpc.WithdrawalEventResponse{
Id: ret.ID.String(),
Exchange: &gctrpc.WithdrawlExchangeEvent{
Name: ret.Exchange.Name,
Id: ret.Exchange.Name,
Status: ret.Exchange.Status,
},
Request: &gctrpc.WithdrawalRequestEvent{
Currency: ret.RequestDetails.Currency.String(),
Description: ret.RequestDetails.Description,
Amount: ret.RequestDetails.Amount,
Type: int32(ret.RequestDetails.Type),
},
}
tempEvent.CreatedAt = timestamppb.New(ret.CreatedAt)
if err := tempEvent.CreatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseSingleEvents CreatedAt %s", err)
}
tempEvent.UpdatedAt = timestamppb.New(ret.UpdatedAt)
if err := tempEvent.UpdatedAt.CheckValid(); err != nil {
log.Errorf(log.Global, "withdrawal parseSingleEvents UpdatedAt: %s", err)
}
if ret.RequestDetails.Type == withdraw.Crypto {
tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent)
tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{
Address: ret.RequestDetails.Crypto.Address,
AddressTag: ret.RequestDetails.Crypto.AddressTag,
Fee: ret.RequestDetails.Crypto.FeeAmount,
}
} else if ret.RequestDetails.Type == withdraw.Fiat {
if ret.RequestDetails.Fiat != (withdraw.FiatRequest{}) {
tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent)
tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{
BankName: ret.RequestDetails.Fiat.Bank.BankName,
AccountName: ret.RequestDetails.Fiat.Bank.AccountName,
AccountNumber: ret.RequestDetails.Fiat.Bank.AccountNumber,
Bsb: ret.RequestDetails.Fiat.Bank.BSBNumber,
Swift: ret.RequestDetails.Fiat.Bank.SWIFTCode,
Iban: ret.RequestDetails.Fiat.Bank.IBAN,
}
}
}
return &gctrpc.WithdrawalEventsByExchangeResponse{
Event: []*gctrpc.WithdrawalEventResponse{tempEvent},
}
}

143
engine/withdraw_manager.go Normal file
View File

@@ -0,0 +1,143 @@
package engine
import (
"errors"
"fmt"
"time"
dbwithdraw "github.com/thrasher-corp/gocryptotrader/database/repository/withdraw"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// SetupWithdrawManager creates a new withdraw manager
func SetupWithdrawManager(em iExchangeManager, pm iPortfolioManager, isDryRun bool) (*WithdrawManager, error) {
if em == nil {
return nil, errors.New("nil manager")
}
return &WithdrawManager{
exchangeManager: em,
portfolioManager: pm,
isDryRun: isDryRun,
}, nil
}
// SubmitWithdrawal performs validation and submits a new withdraw request to
// exchange
func (m *WithdrawManager) SubmitWithdrawal(req *withdraw.Request) (*withdraw.Response, error) {
if m == nil {
return nil, ErrNilSubsystem
}
if req == nil {
return nil, withdraw.ErrRequestCannotBeNil
}
exch := m.exchangeManager.GetExchangeByName(req.Exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
resp := &withdraw.Response{
Exchange: withdraw.ExchangeResponse{
Name: req.Exchange,
},
RequestDetails: *req,
}
var err error
if m.isDryRun {
log.Warnln(log.Global, "Dry run enabled, no withdrawal request will be submitted or have an event created")
resp.ID = withdraw.DryRunID
resp.Exchange.Status = "dryrun"
resp.Exchange.ID = withdraw.DryRunID.String()
} else {
var ret *withdraw.ExchangeResponse
if req.Type == withdraw.Crypto {
if !m.portfolioManager.IsWhiteListed(req.Crypto.Address) {
return nil, withdraw.ErrStrAddressNotWhiteListed
}
if !m.portfolioManager.IsExchangeSupported(req.Exchange, req.Crypto.Address) {
return nil, withdraw.ErrStrExchangeNotSupportedByAddress
}
}
if req.Type == withdraw.Fiat {
ret, err = exch.WithdrawFiatFunds(req)
if err != nil {
resp.Exchange.Status = err.Error()
} else {
resp.Exchange.Status = ret.Status
resp.Exchange.ID = ret.ID
}
} else if req.Type == withdraw.Crypto {
ret, err = exch.WithdrawCryptocurrencyFunds(req)
if err != nil {
resp.Exchange.Status = err.Error()
} else {
resp.Exchange.Status = ret.Status
resp.Exchange.ID = ret.ID
}
}
}
if err == nil {
withdraw.Cache.Add(resp.ID, resp)
}
dbwithdraw.Event(resp)
return resp, err
}
// WithdrawalEventByID returns a withdrawal request by ID
func (m *WithdrawManager) WithdrawalEventByID(id string) (*withdraw.Response, error) {
if m == nil {
return nil, ErrNilSubsystem
}
v := withdraw.Cache.Get(id)
if v != nil {
return v.(*withdraw.Response), nil
}
l, err := dbwithdraw.GetEventByUUID(id)
if err != nil {
return nil, fmt.Errorf("%w %v", ErrWithdrawRequestNotFound, id)
}
withdraw.Cache.Add(id, l)
return l, nil
}
// WithdrawalEventByExchange returns a withdrawal request by ID
func (m *WithdrawManager) WithdrawalEventByExchange(exchange string, limit int) ([]*withdraw.Response, error) {
if m == nil {
return nil, ErrNilSubsystem
}
exch := m.exchangeManager.GetExchangeByName(exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
return dbwithdraw.GetEventsByExchange(exchange, limit)
}
// WithdrawEventByDate returns a withdrawal request by ID
func (m *WithdrawManager) WithdrawEventByDate(exchange string, start, end time.Time, limit int) ([]*withdraw.Response, error) {
if m == nil {
return nil, ErrNilSubsystem
}
exch := m.exchangeManager.GetExchangeByName(exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
return dbwithdraw.GetEventsByDate(exchange, start, end, limit)
}
// WithdrawalEventByExchangeID returns a withdrawal request by Exchange ID
func (m *WithdrawManager) WithdrawalEventByExchangeID(exchange, id string) (*withdraw.Response, error) {
if m == nil {
return nil, ErrNilSubsystem
}
exch := m.exchangeManager.GetExchangeByName(exchange)
if exch == nil {
return nil, ErrExchangeNotFound
}
return dbwithdraw.GetEventByExchangeID(exchange, id)
}

View File

@@ -0,0 +1,49 @@
# GoCryptoTrader package Withdraw_manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/withdraw_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This withdraw_manager package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for Withdraw_manager
+ The withdraw manager subsystem is responsible for the processing of withdrawal requests and submitting them to exchanges
+ The withdraw manager can be interacted with via GRPC commands such as `WithdrawFiatRequest` and `WithdrawCryptoRequest`
+ Supports caching of responses to allow for quick viewing of withdrawal events via GRPC
+ If the database is enabled, withdrawal events are stored to the database for later viewing
+ Will not process withdrawal events if `dryrun` is true
+ The withdraw manager subsystem is always enabled
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,187 @@
package engine
import (
"errors"
"sync"
"testing"
"time"
"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/portfolio"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
const (
exchangeName = "Binance"
)
func withdrawManagerTestHelper(t *testing.T) (*ExchangeManager, *portfolioManager) {
t.Helper()
em := SetupExchangeManager()
b := new(binance.Binance)
b.SetDefaults()
em.Add(b)
pm, err := setupPortfolioManager(em, 0, &portfolio.Base{Addresses: []portfolio.Address{}})
if err != nil {
t.Fatal(err)
}
return em, pm
}
func TestSubmitWithdrawal(t *testing.T) {
t.Parallel()
em, pm := withdrawManagerTestHelper(t)
m, err := SetupWithdrawManager(em, pm, false)
if err != nil {
t.Fatal(err)
}
banking.Accounts = append(banking.Accounts,
banking.Account{
Enabled: true,
ID: "test-bank-01",
BankName: "Test Bank",
BankAddress: "42 Bank Street",
BankPostalCode: "13337",
BankPostalCity: "Satoshiville",
BankCountry: "Japan",
AccountName: "Satoshi Nakamoto",
AccountNumber: "0234",
BSBNumber: "123456",
SWIFTCode: "91272837",
IBAN: "98218738671897",
SupportedCurrencies: "AUD,USD",
SupportedExchanges: "Binance",
},
)
bank, err := banking.GetBankAccountByID("test-bank-01")
if err != nil {
t.Error(err)
}
req := &withdraw.Request{
Exchange: exchangeName,
Currency: currency.AUD,
Description: exchangeName,
Amount: 1.0,
Type: withdraw.Fiat,
Fiat: withdraw.FiatRequest{
Bank: *bank,
},
}
_, err = m.SubmitWithdrawal(req)
if !errors.Is(err, common.ErrFunctionNotSupported) {
t.Errorf("received %v, expected %v", err, common.ErrFunctionNotSupported)
}
req.Type = withdraw.Crypto
req.Currency = currency.BTC
req.Crypto.Address = "1337"
_, err = m.SubmitWithdrawal(req)
if !errors.Is(err, withdraw.ErrStrAddressNotWhiteListed) {
t.Errorf("received %v, expected %v", err, withdraw.ErrStrAddressNotWhiteListed)
}
var wg sync.WaitGroup
err = pm.Start(&wg)
if err != nil {
t.Error(err)
}
err = pm.AddAddress("1337", "", req.Currency, 1337)
if err != nil {
t.Error(err)
}
adds := pm.GetAddresses()
adds[0].WhiteListed = true
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)
}
_, err = m.SubmitWithdrawal(req)
if !errors.Is(err, withdraw.ErrStrExchangeNotSupportedByAddress) {
t.Errorf("received %v, expected %v", err, withdraw.ErrStrExchangeNotSupportedByAddress)
}
adds[0].SupportedExchanges = exchangeName
_, err = m.SubmitWithdrawal(req)
if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) {
t.Errorf("received %v, expected %v", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
_, err = m.SubmitWithdrawal(nil)
if !errors.Is(err, withdraw.ErrRequestCannotBeNil) {
t.Errorf("received %v, expected %v", err, withdraw.ErrRequestCannotBeNil)
}
m.isDryRun = true
_, err = m.SubmitWithdrawal(req)
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)
}
}
func TestWithdrawEventByID(t *testing.T) {
t.Parallel()
em, pm := withdrawManagerTestHelper(t)
m, err := SetupWithdrawManager(em, pm, false)
if err != nil {
t.Fatal(err)
}
tempResp := &withdraw.Response{
ID: withdraw.DryRunID,
}
_, err = m.WithdrawalEventByID(withdraw.DryRunID.String())
if !errors.Is(err, ErrWithdrawRequestNotFound) {
t.Errorf("received %v, expected %v", err, ErrWithdrawRequestNotFound)
}
withdraw.Cache.Add(withdraw.DryRunID.String(), tempResp)
v, err := m.WithdrawalEventByID(withdraw.DryRunID.String())
if !errors.Is(err, nil) {
t.Errorf("expected %v, received %v", nil, err)
}
if v == nil {
t.Error("expected WithdrawalEventByID() to return data from cache")
}
}
func TestWithdrawalEventByExchange(t *testing.T) {
t.Parallel()
em, pm := withdrawManagerTestHelper(t)
m, err := SetupWithdrawManager(em, pm, false)
if err != nil {
t.Fatal(err)
}
_, err = m.WithdrawalEventByExchange(exchangeName, 1)
if err == nil {
t.Error(err)
}
}
func TestWithdrawEventByDate(t *testing.T) {
t.Parallel()
em, pm := withdrawManagerTestHelper(t)
m, err := SetupWithdrawManager(em, pm, false)
if err != nil {
t.Fatal(err)
}
_, err = m.WithdrawEventByDate(exchangeName, time.Now(), time.Now(), 1)
if err == nil {
t.Error(err)
}
}
func TestWithdrawalEventByExchangeID(t *testing.T) {
t.Parallel()
em, _ := withdrawManagerTestHelper(t)
m, err := SetupWithdrawManager(em, nil, false)
if err != nil {
t.Fatal(err)
}
_, err = m.WithdrawalEventByExchangeID(exchangeName, exchangeName)
if err == nil {
t.Error(err)
}
}

View File

@@ -0,0 +1,18 @@
package engine
import (
"errors"
)
var (
// ErrWithdrawRequestNotFound message to display when no record is found
ErrWithdrawRequestNotFound = errors.New("request not found")
)
// WithdrawManager is responsible for performing withdrawal requests and
// saving them to the database
type WithdrawManager struct {
exchangeManager iExchangeManager
portfolioManager iPortfolioManager
isDryRun bool
}

View File

@@ -1,191 +0,0 @@
package engine
import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
const (
bankAccountID = "test-bank-01"
)
var (
settings = Settings{
ConfigFile: filepath.Join("..", "testdata", "configtest.json"),
EnableDryRun: true,
DataDir: filepath.Join("..", "testdata", "gocryptotrader"),
Verbose: false,
EnableGRPC: false,
EnableDeprecatedRPC: false,
EnableWebsocketRPC: false,
}
)
func cleanup() {
err := os.RemoveAll(settings.DataDir)
if err != nil {
fmt.Printf("Clean up failed to remove file: %v manual removal may be required", err)
}
}
func TestSubmitWithdrawal(t *testing.T) {
bot := CreateTestBot(t)
if config.Cfg.Name == "" {
config.Cfg = *bot.Config
}
banking.Accounts = append(banking.Accounts,
banking.Account{
Enabled: true,
ID: "test-bank-01",
BankName: "Test Bank",
BankAddress: "42 Bank Street",
BankPostalCode: "13337",
BankPostalCity: "Satoshiville",
BankCountry: "Japan",
AccountName: "Satoshi Nakamoto",
AccountNumber: "0234",
BSBNumber: "123456",
SWIFTCode: "91272837",
IBAN: "98218738671897",
SupportedCurrencies: "AUD,USD",
SupportedExchanges: testExchange,
},
)
bank, err := banking.GetBankAccountByID(bankAccountID)
if err != nil {
t.Fatal(err)
}
req := &withdraw.Request{
Exchange: testExchange,
Currency: currency.AUD,
Description: testExchange,
Amount: 1.0,
Type: 1,
Fiat: withdraw.FiatRequest{
Bank: *bank,
},
}
_, err = bot.SubmitWithdrawal(req)
if err != nil {
t.Fatal(err)
}
_, err = bot.SubmitWithdrawal(nil)
if err != nil {
if err.Error() != withdraw.ErrRequestCannotBeNil.Error() {
t.Fatal(err)
}
}
cleanup()
}
func TestWithdrawEventByID(t *testing.T) {
tempResp := &withdraw.Response{
ID: withdraw.DryRunID,
}
_, err := WithdrawalEventByID(withdraw.DryRunID.String())
if err != nil {
if err.Error() != fmt.Errorf(ErrWithdrawRequestNotFound, withdraw.DryRunID.String()).Error() {
t.Fatal(err)
}
}
withdraw.Cache.Add(withdraw.DryRunID.String(), tempResp)
v, err := WithdrawalEventByID(withdraw.DryRunID.String())
if err != nil {
if err != fmt.Errorf(ErrWithdrawRequestNotFound, withdraw.DryRunID.String()) {
t.Fatal(err)
}
}
if v == nil {
t.Fatal("expected WithdrawalEventByID() to return data from cache")
}
}
func TestWithdrawalEventByExchange(t *testing.T) {
_, err := WithdrawalEventByExchange(testExchange, 1)
if err == nil {
t.Fatal(err)
}
}
func TestWithdrawEventByDate(t *testing.T) {
_, err := WithdrawEventByDate(testExchange, time.Now(), time.Now(), 1)
if err == nil {
t.Fatal(err)
}
}
func TestWithdrawalEventByExchangeID(t *testing.T) {
_, err := WithdrawalEventByExchangeID(testExchange, testExchange)
if err == nil {
t.Fatal(err)
}
}
func TestParseEvents(t *testing.T) {
var testData []*withdraw.Response
for x := 0; x < 5; x++ {
test := fmt.Sprintf("test-%v", x)
resp := &withdraw.Response{
ID: withdraw.DryRunID,
Exchange: withdraw.ExchangeResponse{
Name: test,
ID: test,
Status: test,
},
RequestDetails: withdraw.Request{
Exchange: test,
Description: test,
Amount: 1.0,
},
}
if x%2 == 0 {
resp.RequestDetails.Currency = currency.AUD
resp.RequestDetails.Type = 1
resp.RequestDetails.Fiat = withdraw.FiatRequest{
Bank: banking.Account{
Enabled: false,
ID: fmt.Sprintf("test-%v", x),
BankName: fmt.Sprintf("test-%v-bank", x),
AccountName: "hello",
AccountNumber: fmt.Sprintf("test-%v", x),
BSBNumber: "123456",
SupportedCurrencies: "BTC-AUD",
SupportedExchanges: testExchange,
},
}
} else {
resp.RequestDetails.Currency = currency.BTC
resp.RequestDetails.Type = 0
resp.RequestDetails.Crypto.Address = test
resp.RequestDetails.Crypto.FeeAmount = 0
resp.RequestDetails.Crypto.AddressTag = test
}
testData = append(testData, resp)
}
v := parseMultipleEvents(testData)
if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" {
t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse")
}
v = parseSingleEvents(testData[0])
if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" {
t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse")
}
v = parseSingleEvents(testData[1])
if v.Event[0].Request.Type != 0 {
t.Fatal("Expected second entry in slice to return a Request.Type of Crypto")
}
}