mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-29 15:10:37 +00:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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
906
engine/apiserver.go
Normal 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
62
engine/apiserver.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# GoCryptoTrader package Apiserver
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/apiserver)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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
288
engine/apiserver_test.go
Normal 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
169
engine/apiserver_types.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
117
engine/communication_manager.go
Normal file
117
engine/communication_manager.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
90
engine/communication_manager.md
Normal file
90
engine/communication_manager.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# GoCryptoTrader package Communication_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/communication_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
158
engine/communication_manager_test.go
Normal file
158
engine/communication_manager_test.go
Normal 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{})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
100
engine/connection_manager.go
Normal file
100
engine/connection_manager.go
Normal 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()
|
||||
}
|
||||
53
engine/connection_manager.md
Normal file
53
engine/connection_manager.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# GoCryptoTrader package Connection_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/connection_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
122
engine/connection_manager_test.go
Normal file
122
engine/connection_manager_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
191
engine/database_connection.go
Normal file
191
engine/database_connection.go
Normal 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
|
||||
}
|
||||
64
engine/database_connection.md
Normal file
64
engine/database_connection.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# GoCryptoTrader package Database_connection
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/database_connection)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
241
engine/database_connection_test.go
Normal file
241
engine/database_connection_test.go
Normal 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
91
engine/depositaddress.go
Normal 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
45
engine/depositaddress.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# GoCryptoTrader package Depositaddress
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/depositaddress)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
96
engine/depositaddress_test.go
Normal file
96
engine/depositaddress_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
509
engine/engine.go
509
engine/engine.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
334
engine/event_manager.go
Normal 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
52
engine/event_manager.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# GoCryptoTrader package Event_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/event_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
321
engine/event_manager_test.go
Normal file
321
engine/event_manager_test.go
Normal 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()
|
||||
}
|
||||
74
engine/event_manager_types.go
Normal file
74
engine/event_manager_types.go
Normal 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
|
||||
}
|
||||
347
engine/events.go
347
engine/events.go
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
193
engine/exchange_manager.go
Normal 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
|
||||
}
|
||||
45
engine/exchange_manager.md
Normal file
45
engine/exchange_manager.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# GoCryptoTrader package Exchange_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/exchange_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
81
engine/exchange_manager_test.go
Normal file
81
engine/exchange_manager_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
205
engine/ntp_manager.go
Normal 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
56
engine/ntp_manager.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# GoCryptoTrader package Ntp_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/ntp_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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
206
engine/ntp_manager_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
49
engine/ntp_manager_types.go
Normal file
49
engine/ntp_manager_types.go
Normal 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
738
engine/order_manager.go
Normal 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
45
engine/order_manager.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# GoCryptoTrader package Order_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/order_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
561
engine/order_manager_test.go
Normal file
561
engine/order_manager_test.go
Normal 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()
|
||||
}
|
||||
59
engine/order_manager_types.go
Normal file
59
engine/order_manager_types.go
Normal 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
|
||||
}
|
||||
573
engine/orders.go
573
engine/orders.go
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
327
engine/portfolio_manager.go
Normal 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)
|
||||
}
|
||||
66
engine/portfolio_manager.md
Normal file
66
engine/portfolio_manager.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# GoCryptoTrader package Portfolio_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/portfolio_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
117
engine/portfolio_manager_test.go
Normal file
117
engine/portfolio_manager_test.go
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
85
engine/subsystem_types.go
Normal 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
47
engine/subsystem_types.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# GoCryptoTrader package Subsystem_types
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/subsystem_types)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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
893
engine/sync_manager.go
Normal 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
56
engine/sync_manager.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# GoCryptoTrader package Sync_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/sync_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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
200
engine/sync_manager_test.go
Normal 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, "", "", "")
|
||||
}
|
||||
64
engine/sync_manager_types.go
Normal file
64
engine/sync_manager_types.go
Normal 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
|
||||
}
|
||||
635
engine/syncer.go
635
engine/syncer.go
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
332
engine/websocketroutine_manager.go
Normal file
332
engine/websocketroutine_manager.go
Normal 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)
|
||||
}
|
||||
48
engine/websocketroutine_manager.md
Normal file
48
engine/websocketroutine_manager.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# GoCryptoTrader package Websocketroutine_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/websocketroutine_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
260
engine/websocketroutine_manager_test.go
Normal file
260
engine/websocketroutine_manager_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
27
engine/websocketroutine_manager_types.go
Normal file
27
engine/websocketroutine_manager_types.go
Normal 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")
|
||||
)
|
||||
@@ -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
143
engine/withdraw_manager.go
Normal 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)
|
||||
}
|
||||
49
engine/withdraw_manager.md
Normal file
49
engine/withdraw_manager.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# GoCryptoTrader package Withdraw_manager
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/withdraw_manager)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](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***
|
||||
187
engine/withdraw_manager_test.go
Normal file
187
engine/withdraw_manager_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
18
engine/withdraw_manager_types.go
Normal file
18
engine/withdraw_manager_types.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user