From fd600972ba8ddef1b3bc656eb42939aa008fa134 Mon Sep 17 00:00:00 2001 From: TaltaM <68383301+TaltaM@users.noreply.github.com> Date: Wed, 15 Sep 2021 01:28:31 +0200 Subject: [PATCH] engine/order manager: Initial REST managed order updating (resolves #772) (#775) * Initial REST managed order updating * Apply gloriousCode's changes.go patch * Update internal order ID handling * Check error * Replace string with string pointer * Avoid nil pointers in upsert * Update test for UpdateOrderFromDetail() * Add tests for orders.go * Remove unnecessary newline * Address comments * Add missing nil check * Add tests for new functions in order_manager.go * Remove empty line * Change log level for updates from Info to Debug (keep added orders at Info) * Initialize orders before running the timer * [TEMP] Add verbosity for debugging * Nil checking on exchangeManager in GetExchanges() - exchangeManager.GetExchanges() and iExchangeManager.GetExchanges() return an error on nil - bot.GetExchanges() wraps exchangeManager.GetExchanges() and returns an empty slice * Revert https://github.com/thrasher-corp/gocryptotrader/pull/775/commits/b5afe1a46b95afe6c622d2c13972dbb2137735ff * Do not start the order manager runner thread Instead, mark the order manager as running * Remove redundant error.Is() and remove print wrapper on msg * Add atomic blocker and waitgroup on processOrders() * Disable unnecessary orderManager runner thread for rpcserver_test * Remove redundant err from orderStore.getActiveOrders() * [FIX] Populate requiresProcessing using UpsertResponse data instead of REST return data .. because the data returned by the REST calls do not include the internal user ID's * [TEST] Verify that processOrders() actually processes queried order data * Remove leftover warning and add nil check on wg.Done() * Apply suggestions from code review Log category changes - as suggested Co-authored-by: Adrian Gallagher * Return when no exchanges available Co-authored-by: Adrian Gallagher --- engine/apiserver.go | 15 +- engine/engine.go | 9 +- engine/exchange_manager.go | 7 +- engine/exchange_manager_test.go | 18 +- engine/helpers.go | 2 +- engine/order_manager.go | 235 +++++++++++++--- engine/order_manager_test.go | 382 ++++++++++++++++++++++++++- engine/order_manager_types.go | 19 +- engine/portfolio_manager.go | 6 +- engine/rpcserver.go | 5 +- engine/rpcserver_test.go | 12 +- engine/subsystem_types.go | 2 +- engine/sync_manager.go | 10 +- engine/websocketroutine_manager.go | 5 +- exchanges/bittrex/bittrex_wrapper.go | 1 + exchanges/order/order_test.go | 146 +++++++++- exchanges/order/orders.go | 38 +++ 17 files changed, 833 insertions(+), 79 deletions(-) diff --git a/engine/apiserver.go b/engine/apiserver.go index 9e4e539aeaf..ad43757f24c 100644 --- a/engine/apiserver.go +++ b/engine/apiserver.go @@ -303,7 +303,10 @@ func (m *apiServerManager) getIndex(w http.ResponseWriter, _ *http.Request) { // getAllActiveOrderbooks returns all enabled exchanges orderbooks func getAllActiveOrderbooks(m iExchangeManager) []EnabledExchangeOrderbooks { var orderbookData []EnabledExchangeOrderbooks - exchanges := m.GetExchanges() + exchanges, err := m.GetExchanges() + if err != nil { + log.Errorf(log.APIServerMgr, "Cannot get exchanges: %v", err) + } for x := range exchanges { assets := exchanges[x].GetAssetTypes(true) exchName := exchanges[x].GetName() @@ -340,7 +343,10 @@ func getAllActiveOrderbooks(m iExchangeManager) []EnabledExchangeOrderbooks { // getAllActiveTickers returns all enabled exchanges tickers func getAllActiveTickers(m iExchangeManager) []EnabledExchangeCurrencies { var tickers []EnabledExchangeCurrencies - exchanges := m.GetExchanges() + exchanges, err := m.GetExchanges() + if err != nil { + log.Errorf(log.APIServerMgr, "Cannot get exchanges: %v", err) + } for x := range exchanges { assets := exchanges[x].GetAssetTypes(true) exchName := exchanges[x].GetName() @@ -377,7 +383,10 @@ func getAllActiveTickers(m iExchangeManager) []EnabledExchangeCurrencies { // getAllActiveAccounts returns all enabled exchanges accounts func getAllActiveAccounts(m iExchangeManager) []AllEnabledExchangeAccounts { var accounts []AllEnabledExchangeAccounts - exchanges := m.GetExchanges() + exchanges, err := m.GetExchanges() + if err != nil { + log.Errorf(log.APIServerMgr, "Cannot get exchanges: %v", err) + } for x := range exchanges { assets := exchanges[x].GetAssetTypes(true) exchName := exchanges[x].GetName() diff --git a/engine/engine.go b/engine/engine.go index 1ef68651e18..8b0960b285f 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -716,7 +716,12 @@ func (bot *Engine) UnloadExchange(exchName string) error { // GetExchanges retrieves the loaded exchanges func (bot *Engine) GetExchanges() []exchange.IBotExchange { - return bot.ExchangeManager.GetExchanges() + exch, err := bot.ExchangeManager.GetExchanges() + if err != nil { + gctlog.Warnf(gctlog.ExchangeSys, "Cannot get exchanges: %v", err) + return []exchange.IBotExchange{} + } + return exch } // LoadExchange loads an exchange by name. Optional wait group can be added for @@ -917,7 +922,7 @@ func (bot *Engine) SetupExchanges() error { }(configs[x]) } wg.Wait() - if len(bot.ExchangeManager.GetExchanges()) == 0 { + if len(bot.GetExchanges()) == 0 { return ErrNoExchangesLoaded } return nil diff --git a/engine/exchange_manager.go b/engine/exchange_manager.go index 1386a117cf2..fcac392136e 100644 --- a/engine/exchange_manager.go +++ b/engine/exchange_manager.go @@ -77,14 +77,17 @@ func (m *ExchangeManager) Add(exch exchange.IBotExchange) { } // GetExchanges returns all stored exchanges -func (m *ExchangeManager) GetExchanges() []exchange.IBotExchange { +func (m *ExchangeManager) GetExchanges() ([]exchange.IBotExchange, error) { + if m == nil { + return nil, fmt.Errorf("exchange manager: %w", ErrNilSubsystem) + } m.m.Lock() defer m.m.Unlock() var exchs []exchange.IBotExchange for _, x := range m.exchanges { exchs = append(exchs, x) } - return exchs + return exchs, nil } // RemoveExchange removes an exchange from the manager diff --git a/engine/exchange_manager_test.go b/engine/exchange_manager_test.go index b951f441a1a..76fd27f1eeb 100644 --- a/engine/exchange_manager_test.go +++ b/engine/exchange_manager_test.go @@ -28,7 +28,11 @@ func TestExchangeManagerAdd(t *testing.T) { b := new(bitfinex.Bitfinex) b.SetDefaults() m.Add(b) - if exch := m.GetExchanges(); exch[0].GetName() != "Bitfinex" { + exchanges, err := m.GetExchanges() + if err != nil { + t.Error("no exchange manager found") + } + if exchanges[0].GetName() != "Bitfinex" { t.Error("unexpected exchange name") } } @@ -36,13 +40,21 @@ func TestExchangeManagerAdd(t *testing.T) { func TestExchangeManagerGetExchanges(t *testing.T) { t.Parallel() m := SetupExchangeManager() - if exchanges := m.GetExchanges(); exchanges != nil { + exchanges, err := m.GetExchanges() + if err != nil { + t.Error("no exchange manager found") + } + if exchanges != nil { t.Error("unexpected value") } b := new(bitfinex.Bitfinex) b.SetDefaults() m.Add(b) - if exch := m.GetExchanges(); exch[0].GetName() != "Bitfinex" { + exchanges, err = m.GetExchanges() + if err != nil { + t.Error("no exchange manager found") + } + if exchanges[0].GetName() != "Bitfinex" { t.Error("unexpected exchange name") } } diff --git a/engine/helpers.go b/engine/helpers.go index 83bb0a58ef9..38d3680ed49 100644 --- a/engine/helpers.go +++ b/engine/helpers.go @@ -719,7 +719,7 @@ func (bot *Engine) GetExchangeCryptocurrencyDepositAddresses() map[string]map[st // GetExchangeNames returns a list of enabled or disabled exchanges func (bot *Engine) GetExchangeNames(enabledOnly bool) []string { - exchanges := bot.ExchangeManager.GetExchanges() + exchanges := bot.GetExchanges() var response []string for i := range exchanges { if !enabledOnly || (enabledOnly && exchanges[i].IsEnabled()) { diff --git a/engine/order_manager.go b/engine/order_manager.go index 2aa5cd79ba5..96a5fa743a7 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -87,14 +87,19 @@ func (m *OrderManager) Stop() error { func (m *OrderManager) gracefulShutdown() { if m.cfg.CancelOrdersOnShutdown { log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...") - m.CancelAllOrders(context.TODO(), - m.orderStore.exchangeManager.GetExchanges()) + exchanges, err := m.orderStore.exchangeManager.GetExchanges() + if err != nil { + log.Errorf(log.OrderMgr, "Order manager cannot get exchanges: %v", err) + return + } + m.CancelAllOrders(context.TODO(), exchanges) } } // run will periodically process orders func (m *OrderManager) run() { log.Debugln(log.OrderMgr, "Order manager started.") + m.processOrders() tick := time.NewTicker(orderManagerDelay) m.orderStore.wg.Add(1) defer func() { @@ -242,12 +247,12 @@ func (m *OrderManager) GetOrderInfo(ctx context.Context, exchangeName, orderID s return order.Detail{}, err } - err = m.orderStore.add(&result) - if err != nil && err != ErrOrdersAlreadyExists { + upsertResponse, err := m.orderStore.upsert(&result) + if err != nil { return order.Detail{}, err } - return result, nil + return upsertResponse.OrderDetails, nil } // validate ensures a submitted order is valid before adding to the manager @@ -473,6 +478,18 @@ func (m *OrderManager) GetOrdersFiltered(f *order.Filter) ([]order.Detail, error return m.orderStore.getFilteredOrders(f) } +// GetOrdersActive returns a snapshot of all orders in the order store +// that have a status that indicates it's currently tradable +func (m *OrderManager) GetOrdersActive(f *order.Filter) ([]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) + } + return m.orderStore.getActiveOrders(f), nil +} + // processSubmittedOrder adds a new order to the manager func (m *OrderManager) processSubmittedOrder(newOrder *order.Submit, result order.SubmitResponse) (*OrderSubmitResponse, error) { if !result.IsOrderPlaced { @@ -554,7 +571,18 @@ func (m *OrderManager) processSubmittedOrder(newOrder *order.Submit, result orde // 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() + if !atomic.CompareAndSwapInt32(&m.processingOrders, 0, 1) { + return + } + defer func() { + atomic.StoreInt32(&m.processingOrders, 0) + }() + exchanges, err := m.orderStore.exchangeManager.GetExchanges() + if err != nil { + log.Errorf(log.OrderMgr, "Order manager cannot get exchanges: %v", err) + return + } + var wg sync.WaitGroup for i := range exchanges { if !exchanges[i].GetAuthenticatedAPISupport(exchange.RestAuthentication) { continue @@ -585,6 +613,16 @@ func (m *OrderManager) processOrders() { continue } + filter := &order.Filter{ + Exchange: exchanges[i].GetName(), + } + orders := m.orderStore.getActiveOrders(filter) + order.FilterOrdersByCurrencies(&orders, pairs) + requiresProcessing := make(map[string]bool, len(orders)) + for x := range orders { + requiresProcessing[orders[x].InternalOrderID] = true + } + req := order.GetOrdersRequest{ Side: order.AnySide, Type: order.AnyType, @@ -593,30 +631,66 @@ func (m *OrderManager) processOrders() { } result, err := exchanges[i].GetActiveOrders(context.TODO(), &req) if err != nil { - log.Warnf(log.OrderMgr, + log.Errorf(log.OrderMgr, "Order manager: Unable to get active orders for %s and asset type %s: %s", exchanges[i].GetName(), supportedAssets[y], err) continue } + if len(orders) == 0 && len(result) == 0 { + 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 + upsertResponse, err := m.UpsertOrder(&result[z]) + if err != nil { + log.Error(log.OrderMgr, err) } + requiresProcessing[upsertResponse.OrderDetails.InternalOrderID] = false } + if !exchanges[i].GetBase().GetSupportedFeatures().RESTCapabilities.GetOrder { + continue + } + wg.Add(1) + go m.processMatchingOrders(exchanges[i], orders, requiresProcessing, &wg) } } + wg.Wait() +} + +func (m *OrderManager) processMatchingOrders(exch exchange.IBotExchange, orders []order.Detail, requiresProcessing map[string]bool, wg *sync.WaitGroup) { + defer func() { + if wg != nil { + wg.Done() + } + }() + for x := range orders { + if time.Since(orders[x].LastUpdated) < time.Minute { + continue + } + if requiresProcessing[orders[x].InternalOrderID] { + err := m.FetchAndUpdateExchangeOrder(exch, &orders[x], orders[x].AssetType) + if err != nil { + log.Error(log.OrderMgr, err) + } + } + } +} + +// FetchAndUpdateExchangeOrder calls the exchange to upsert an order to the order store +func (m *OrderManager) FetchAndUpdateExchangeOrder(exch exchange.IBotExchange, ord *order.Detail, assetType asset.Item) error { + if ord == nil { + return errors.New("order manager: Order is nil") + } + fetchedOrder, err := exch.GetOrderInfo(context.TODO(), ord.ID, ord.Pair, assetType) + if err != nil { + ord.Status = order.UnknownStatus + return err + } + fetchedOrder.LastUpdated = time.Now() + _, err = m.UpsertOrder(&fetchedOrder) + return err } // Exists checks whether an order exists in the order store @@ -670,14 +744,50 @@ func (m *OrderManager) UpdateExistingOrder(od *order.Detail) error { } // UpsertOrder updates an existing order or adds a new one to the orderstore -func (m *OrderManager) UpsertOrder(od *order.Detail) error { +func (m *OrderManager) UpsertOrder(od *order.Detail) (resp *OrderUpsertResponse, err error) { if m == nil { - return fmt.Errorf("order manager %w", ErrNilSubsystem) + return nil, fmt.Errorf("order manager %w", ErrNilSubsystem) } if atomic.LoadInt32(&m.started) == 0 { - return fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + if od == nil { + return nil, errNilOrder } - return m.orderStore.upsert(od) + var msg string + defer func(message *string) { + if message == nil { + log.Errorf(log.OrderMgr, "UpsertOrder: produced nil order event message\n") + return + } + m.orderStore.commsManager.PushEvent(base.Event{ + Type: "order", + Message: *message, + }) + }(&msg) + + upsertResponse, err := m.orderStore.upsert(od) + if err != nil { + msg = fmt.Sprintf( + "Order manager: Exchange %s unable to upsert order ID=%v internal ID=%v pair=%v price=%.8f amount=%.8f side=%v type=%v status=%v: %s", + od.Exchange, od.ID, od.InternalOrderID, od.Pair, od.Price, od.Amount, od.Side, od.Type, od.Status, err) + return nil, err + } + + status := "updated" + if upsertResponse.IsNewOrder { + status = "added" + } + msg = fmt.Sprintf("Order manager: Exchange %s %s order ID=%v internal ID=%v pair=%v price=%.8f amount=%.8f side=%v type=%v status=%v.", + upsertResponse.OrderDetails.Exchange, status, upsertResponse.OrderDetails.ID, upsertResponse.OrderDetails.InternalOrderID, + upsertResponse.OrderDetails.Pair, upsertResponse.OrderDetails.Price, upsertResponse.OrderDetails.Amount, + upsertResponse.OrderDetails.Side, upsertResponse.OrderDetails.Type, upsertResponse.OrderDetails.Status) + if upsertResponse.IsNewOrder { + log.Info(log.OrderMgr, msg) + return upsertResponse, nil + } + log.Debug(log.OrderMgr, msg) + return upsertResponse, nil } // get returns all orders for all exchanges @@ -745,27 +855,45 @@ func (s *store) modifyExisting(id string, mod *order.Modify) error { // upsert (1) checks if such an exchange exists in the exchangeManager, (2) checks if // order exists and updates/creates it. -func (s *store) upsert(od *order.Detail) error { +func (s *store) upsert(od *order.Detail) (resp *OrderUpsertResponse, err error) { + if od == nil { + return nil, errNilOrder + } lName := strings.ToLower(od.Exchange) - _, err := s.exchangeManager.GetExchangeByName(lName) + _, err = s.exchangeManager.GetExchangeByName(lName) if err != nil { - return err + return nil, err } s.m.Lock() defer s.m.Unlock() r, ok := s.Orders[lName] if !ok { + od.GenerateInternalOrderID() s.Orders[lName] = []*order.Detail{od} - return nil + resp = &OrderUpsertResponse{ + OrderDetails: od.Copy(), + IsNewOrder: true, + } + return resp, nil } for x := range r { if r[x].ID == od.ID { r[x].UpdateOrderFromDetail(od) - return nil + resp = &OrderUpsertResponse{ + OrderDetails: r[x].Copy(), + IsNewOrder: false, + } + return resp, nil } } + // Untracked websocket orders will not have internalIDs yet + od.GenerateInternalOrderID() s.Orders[lName] = append(s.Orders[lName], od) - return nil + resp = &OrderUpsertResponse{ + OrderDetails: od.Copy(), + IsNewOrder: true, + } + return resp, nil } // getByExchange returns orders by exchange @@ -827,16 +955,7 @@ func (s *store) add(det *order.Detail) error { 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() - } - } + det.GenerateInternalOrderID() s.m.Lock() defer s.m.Unlock() orders := s.Orders[strings.ToLower(det.Exchange)] @@ -877,3 +996,43 @@ func (s *store) getFilteredOrders(f *order.Filter) ([]order.Detail, error) { } return os, nil } + +// getActiveOrders returns copy of the orders that are active +func (s *store) getActiveOrders(f *order.Filter) []order.Detail { + s.m.RLock() + defer s.m.RUnlock() + + var orders []order.Detail + switch { + case f == nil: + for _, e := range s.Orders { + for i := range e { + if !e[i].IsActive() { + continue + } + orders = append(orders, e[i].Copy()) + } + } + case f.Exchange != "": + // optimization if Exchange is filtered + if e, ok := s.Orders[strings.ToLower(f.Exchange)]; ok { + for i := range e { + if !e[i].IsActive() || !e[i].MatchFilter(f) { + continue + } + orders = append(orders, e[i].Copy()) + } + } + default: + for _, e := range s.Orders { + for i := range e { + if !e[i].IsActive() || !e[i].MatchFilter(f) { + continue + } + orders = append(orders, e[i].Copy()) + } + } + } + + return orders +} diff --git a/engine/order_manager_test.go b/engine/order_manager_test.go index 5c5de616838..d3a7f781663 100644 --- a/engine/order_manager_test.go +++ b/engine/order_manager_test.go @@ -7,10 +7,13 @@ import ( "testing" "time" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "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/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" ) // omfExchange aka ordermanager fake exchange overrides exchange functions @@ -29,8 +32,31 @@ func (f omfExchange) CancelOrder(ctx context.Context, o *order.Cancel) error { // GetOrderInfo overrides testExchange's get order function // to do the bare minimum required with no API calls or credentials required func (f omfExchange) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) { - if orderID == "" { + switch orderID { + case "": return order.Detail{}, errors.New("") + case "Order1-unknown-to-active": + return order.Detail{ + Exchange: testExchange, + Pair: currency.Pair{Base: currency.BTC, Quote: currency.USD}, + AssetType: asset.Spot, + Amount: 1.0, + Side: order.Buy, + Status: order.Active, + LastUpdated: time.Now().Add(-time.Hour), + ID: "Order1-unknown-to-active", + }, nil + case "Order2-active-to-inactive": + return order.Detail{ + Exchange: testExchange, + Pair: currency.Pair{Base: currency.BTC, Quote: currency.USD}, + AssetType: asset.Spot, + Amount: 1.0, + Side: order.Sell, + Status: order.Cancelled, + LastUpdated: time.Now().Add(-time.Hour), + ID: "Order2-active-to-inactive", + }, nil } return order.Detail{ @@ -38,9 +64,24 @@ func (f omfExchange) GetOrderInfo(ctx context.Context, orderID string, pair curr ID: orderID, Pair: pair, AssetType: assetType, + Status: order.Cancelled, }, nil } +// GetActiveOrders overrides the function used by processOrders to return 1 active order +func (f omfExchange) GetActiveOrders(ctx context.Context, req *order.GetOrdersRequest) ([]order.Detail, error) { + return []order.Detail{{ + Exchange: testExchange, + Pair: currency.Pair{Base: currency.BTC, Quote: currency.USD}, + AssetType: asset.Spot, + Amount: 2.0, + Side: order.Sell, + Status: order.Active, + LastUpdated: time.Now().Add(-time.Hour), + ID: "Order3-unknown-to-active", + }}, nil +} + func (f omfExchange) ModifyOrder(ctx context.Context, action *order.Modify) (order.Modify, error) { ans := *action ans.ID = "modified_order_id" @@ -157,11 +198,7 @@ func OrdersSetup(t *testing.T) *OrderManager { 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 return m } @@ -710,8 +747,155 @@ func TestOrderManager_Modify(t *testing.T) { } func TestProcessOrders(t *testing.T) { - m := OrdersSetup(t) + var wg sync.WaitGroup + em := SetupExchangeManager() + exch, err := em.NewExchangeByName(testExchange) + if err != nil { + t.Fatal(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) + } + m.started = 1 + pairs := currency.Pairs{ + currency.Pair{Base: currency.BTC, Quote: currency.USD}, + } + // Ensure processOrders() can run the REST calls to GetActiveOrders + // and to GetOrders + exch.GetBase().API = exchange.API{ + AuthenticatedSupport: true, + AuthenticatedWebsocketSupport: false, + } + exch.GetBase().Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + RESTCapabilities: protocol.Features{ + GetOrder: true, + }, + }, + } + exch.GetBase().CurrencyPairs = currency.PairsManager{ + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + Pairs: map[asset.Item]*currency.PairStore{ + asset.Spot: { + AssetEnabled: convert.BoolPtr(true), + Enabled: pairs, + Available: pairs, + }, + }, + } + exch.GetBase().Config = &config.ExchangeConfig{ + CurrencyPairs: ¤cy.PairsManager{ + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + Pairs: map[asset.Item]*currency.PairStore{ + asset.Spot: { + AssetEnabled: convert.BoolPtr(true), + Enabled: pairs, + Available: pairs, + }, + }, + }, + } + + orders := []order.Detail{ + { + Exchange: testExchange, + Pair: pairs[0], + AssetType: asset.Spot, + Amount: 1.0, + Side: order.Buy, + Status: order.UnknownStatus, + LastUpdated: time.Now().Add(-time.Hour), + ID: "Order1-unknown-to-active", + }, + { + Exchange: testExchange, + Pair: pairs[0], + AssetType: asset.Spot, + Amount: 1.0, + Side: order.Sell, + Status: order.Active, + LastUpdated: time.Now().Add(-time.Hour), + ID: "Order2-active-to-inactive", + }, + { + Exchange: testExchange, + Pair: pairs[0], + AssetType: asset.Spot, + Amount: 2.0, + Side: order.Sell, + Status: order.UnknownStatus, + LastUpdated: time.Now().Add(-time.Hour), + ID: "Order3-unknown-to-active", + }, + } + for i := range orders { + if err = m.orderStore.add(&orders[i]); err != nil { + t.Error(err) + } + } + m.processOrders() + + // Order1 is not returned by exch.GetActiveOrders() + // It will be fetched by exch.GetOrderInfo(), which will say it is active + res, err := m.GetOrdersFiltered(&order.Filter{ID: "Order1-unknown-to-active"}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("Expected 3 result, got: %d", len(res)) + } + if res[0].Status != order.Active { + t.Errorf("Order 1 should be active, but status is %s", string(res[0].Status)) + } + + // Order2 is not returned by exch.GetActiveOrders() + // It will be fetched by exch.GetOrderInfo(), which will say it is cancelled + res, err = m.GetOrdersFiltered(&order.Filter{ID: "Order2-active-to-inactive"}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("Expected 1 result, got: %d", len(res)) + } + if res[0].Status != order.Cancelled { + t.Errorf("Order 2 should be cancelled, but status is %s", string(res[0].Status)) + } + + // Order3 is returned by exch.GetActiveOrders(), which will say it is active + res, err = m.GetOrdersFiltered(&order.Filter{ID: "Order3-unknown-to-active"}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("Expected 1 result, got: %d", len(res)) + } + if res[0].Status != order.Active { + t.Errorf("Order 3 should be active, but status is %s", string(res[0].Status)) + } } func TestGetOrdersFiltered(t *testing.T) { @@ -775,3 +959,187 @@ func Test_getFilteredOrders(t *testing.T) { t.Errorf("Expected 1 result, got: %d", len(res)) } } + +func TestGetOrdersActive(t *testing.T) { + m := OrdersSetup(t) + var err error + orders := []order.Detail{ + { + Exchange: testExchange, + Amount: 1.0, + Side: order.Buy, + Status: order.Cancelled, + ID: "Test1", + }, + { + Exchange: testExchange, + Amount: 1.0, + Side: order.Sell, + Status: order.Active, + ID: "Test2", + }, + } + for i := range orders { + if err = m.orderStore.add(&orders[i]); err != nil { + t.Error(err) + } + } + res, err := m.GetOrdersActive(nil) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("TestGetOrdersActive - Expected 1 result, got: %d", len(res)) + } + res, err = m.GetOrdersActive(&order.Filter{Side: order.Sell}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("TestGetOrdersActive - Expected 1 result, got: %d", len(res)) + } + res, err = m.GetOrdersActive(&order.Filter{Side: order.Buy}) + if err != nil { + t.Error(err) + } + if len(res) != 0 { + t.Errorf("TestGetOrdersActive - Expected 0 results, got: %d", len(res)) + } +} + +func Test_processMatchingOrders(t *testing.T) { + m := OrdersSetup(t) + exch, err := m.orderStore.exchangeManager.GetExchangeByName(testExchange) + if err != nil { + t.Fatal(err) + } + orders := []order.Detail{ + { + Exchange: testExchange, + ID: "Test1", + LastUpdated: time.Now(), + }, + { + Exchange: testExchange, + ID: "Test2", + LastUpdated: time.Now(), + }, + { + Exchange: testExchange, + ID: "Test3", + LastUpdated: time.Now().Add(-time.Hour), + }, + { + Exchange: testExchange, + ID: "Test4", + LastUpdated: time.Now().Add(-time.Hour), + }, + } + requiresProcessing := make(map[string]bool, len(orders)) + for i := range orders { + orders[i].GenerateInternalOrderID() + if i%2 == 0 { + requiresProcessing[orders[i].InternalOrderID] = false + } else { + requiresProcessing[orders[i].InternalOrderID] = true + } + } + var wg sync.WaitGroup + wg.Add(1) + m.processMatchingOrders(exch, orders, requiresProcessing, &wg) + wg.Wait() + res, err := m.GetOrdersFiltered(&order.Filter{Exchange: testExchange}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("Expected 1 result, got: %d", len(res)) + } + if res[0].ID != "Test4" { + t.Error("Order Test4 should have been fetched and updated") + } +} + +func TestFetchAndUpdateExchangeOrder(t *testing.T) { + m := OrdersSetup(t) + exch, err := m.orderStore.exchangeManager.GetExchangeByName(testExchange) + if err != nil { + t.Fatal(err) + } + err = m.FetchAndUpdateExchangeOrder(exch, nil, asset.Spot) + if err == nil { + t.Error("Error expected when order is nil") + } + o := &order.Detail{ + Exchange: testExchange, + Amount: 1.0, + Side: order.Sell, + Status: order.Active, + ID: "Test", + } + err = m.FetchAndUpdateExchangeOrder(exch, o, asset.Spot) + if err != nil { + t.Error(err) + } + if o.Status != order.Active { + t.Error("Order should be active") + } + res, err := m.GetOrdersFiltered(&order.Filter{Exchange: testExchange}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("Expected 1 result, got: %d", len(res)) + } + + o.Status = order.PartiallyCancelled + err = m.FetchAndUpdateExchangeOrder(exch, o, asset.Spot) + if err != nil { + t.Error(err) + } + res, err = m.GetOrdersFiltered(&order.Filter{Exchange: testExchange}) + if err != nil { + t.Error(err) + } + if len(res) != 1 { + t.Errorf("Expected 1 result, got: %d", len(res)) + } +} + +func Test_getActiveOrders(t *testing.T) { + m := OrdersSetup(t) + var err error + orders := []order.Detail{ + { + Exchange: testExchange, + Amount: 1.0, + Side: order.Buy, + Status: order.Cancelled, + ID: "Test1", + }, + { + Exchange: testExchange, + Amount: 1.0, + Side: order.Sell, + Status: order.Active, + ID: "Test2", + }, + } + for i := range orders { + if err = m.orderStore.add(&orders[i]); err != nil { + t.Error(err) + } + } + res := m.orderStore.getActiveOrders(nil) + if len(res) != 1 { + t.Errorf("Test_getActiveOrders - Expected 1 result, got: %d", len(res)) + } + res = m.orderStore.getActiveOrders(&order.Filter{Side: order.Sell}) + if len(res) != 1 { + t.Errorf("Test_getActiveOrders - Expected 1 result, got: %d", len(res)) + } + res = m.orderStore.getActiveOrders(&order.Filter{Side: order.Buy}) + if len(res) != 0 { + t.Errorf("Test_getActiveOrders - Expected 0 results, got: %d", len(res)) + } +} diff --git a/engine/order_manager_types.go b/engine/order_manager_types.go index 711a993fda0..4bbb73e937c 100644 --- a/engine/order_manager_types.go +++ b/engine/order_manager_types.go @@ -22,6 +22,7 @@ var ( 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") + errNilOrder = errors.New("nil order received") ) type orderManagerConfig struct { @@ -45,11 +46,12 @@ type store struct { // OrderManager processes and stores orders across enabled exchanges type OrderManager struct { - started int32 - shutdown chan struct{} - orderStore store - cfg orderManagerConfig - verbose bool + started int32 + processingOrders int32 + shutdown chan struct{} + orderStore store + cfg orderManagerConfig + verbose bool } // OrderSubmitResponse contains the order response along with an internal order ID @@ -57,3 +59,10 @@ type OrderSubmitResponse struct { order.SubmitResponse InternalOrderID string } + +// OrderUpsertResponse contains a copy of the resulting order details and a bool +// indicating if the order details were inserted (true) or updated (false) +type OrderUpsertResponse struct { + OrderDetails order.Detail + IsNewOrder bool +} diff --git a/engine/portfolio_manager.go b/engine/portfolio_manager.go index 2292f3cbbac..2e100d0d111 100644 --- a/engine/portfolio_manager.go +++ b/engine/portfolio_manager.go @@ -143,7 +143,11 @@ func (m *portfolioManager) processPortfolio() { value) } - d := m.getExchangeAccountInfo(m.exchangeManager.GetExchanges()) + exchanges, err := m.exchangeManager.GetExchanges() + if err != nil { + log.Errorf(log.PortfolioMgr, "Portfolio manager cannot get exchanges: %v", err) + } + d := m.getExchangeAccountInfo(exchanges) m.seedExchangeAccountInfo(d) atomic.CompareAndSwapInt32(&m.processing, 1, 0) } diff --git a/engine/rpcserver.go b/engine/rpcserver.go index eae2f369298..5b947a79f59 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -482,7 +482,10 @@ func (s *RPCServer) GetOrderbook(ctx context.Context, r *gctrpc.GetOrderbookRequ // GetOrderbooks returns a list of orderbooks for all enabled exchanges and all // enabled currency pairs func (s *RPCServer) GetOrderbooks(ctx context.Context, _ *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) { - exchanges := s.ExchangeManager.GetExchanges() + exchanges, err := s.ExchangeManager.GetExchanges() + if err != nil { + return nil, err + } var obResponse []*gctrpc.Orderbooks var obs []*gctrpc.OrderbookResponse for x := range exchanges { diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index 2ee65162e46..cc1c5bfcd3a 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -1015,10 +1015,7 @@ func TestGetOrders(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received '%v', expected '%v'", err, nil) } - err = om.Start() - if !errors.Is(err, nil) { - t.Errorf("received '%v', expected '%v'", err, nil) - } + om.started = 1 s := RPCServer{Engine: &Engine{ExchangeManager: em, OrderManager: om}} p := &gctrpc.CurrencyPair{ @@ -1126,7 +1123,7 @@ func TestGetOrder(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received '%v', expected '%v'", err, nil) } - err = om.Start() + om.started = 1 if !errors.Is(err, nil) { t.Errorf("received '%v', expected '%v'", err, nil) } @@ -1656,10 +1653,7 @@ func TestGetManagedOrders(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received '%v', expected '%v'", err, nil) } - err = om.Start() - if !errors.Is(err, nil) { - t.Errorf("received '%v', expected '%v'", err, nil) - } + om.started = 1 s := RPCServer{Engine: &Engine{ExchangeManager: em, OrderManager: om}} p := &gctrpc.CurrencyPair{ diff --git a/engine/subsystem_types.go b/engine/subsystem_types.go index 978d26534d4..2817b9128e8 100644 --- a/engine/subsystem_types.go +++ b/engine/subsystem_types.go @@ -42,7 +42,7 @@ var ( // iExchangeManager limits exposure of accessible functions to exchange manager // so that subsystems can use some functionality type iExchangeManager interface { - GetExchanges() []exchange.IBotExchange + GetExchanges() ([]exchange.IBotExchange, error) GetExchangeByName(string) (exchange.IBotExchange, error) } diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 2ea58c3b8bc..047acd25a96 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -104,7 +104,10 @@ func (m *syncManager) Start() error { m.initSyncWG.Add(1) m.inService.Done() log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer started.") - exchanges := m.exchangeManager.GetExchanges() + exchanges, err := m.exchangeManager.GetExchanges() + if err != nil { + return err + } for x := range exchanges { exchangeName := exchanges[x].GetName() supportsWebsocket := exchanges[x].SupportsWebsocket() @@ -454,7 +457,10 @@ func (m *syncManager) worker() { defer cleanup() for atomic.LoadInt32(&m.started) != 0 { - exchanges := m.exchangeManager.GetExchanges() + exchanges, err := m.exchangeManager.GetExchanges() + if err != nil { + log.Errorf(log.SyncMgr, "Sync manager cannot get exchanges: %v", err) + } for x := range exchanges { exchangeName := exchanges[x].GetName() supportsREST := exchanges[x].SupportsREST() diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index 8074da8ed40..4b96e1e4dd2 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -81,7 +81,10 @@ func (m *websocketRoutineManager) websocketRoutine() { if m.verbose { log.Debugln(log.WebsocketMgr, "Connecting exchange websocket services...") } - exchanges := m.exchangeManager.GetExchanges() + exchanges, err := m.exchangeManager.GetExchanges() + if err != nil { + log.Errorf(log.WebsocketMgr, "websocket routine manager cannot get exchanges: %v", err) + } for i := range exchanges { go func(i int) { if exchanges[i].SupportsWebsocket() { diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 9ebee7620a9..b412deb6450 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -84,6 +84,7 @@ func (b *Bittrex) SetDefaults() { TradeFetching: true, OrderbookFetching: true, AutoPairUpdates: true, + GetOrder: true, GetOrders: true, CancelOrder: true, CancelOrders: true, diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 06ce3954d24..ea917751037 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/gofrs/uuid" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" @@ -815,7 +816,7 @@ func TestUpdateOrderFromDetail(t *testing.T) { RemainingAmount: 0, Fee: 0, Exchange: "test", - ID: "1", + ID: "", AccountID: "", ClientID: "", WalletAddress: "", @@ -866,8 +867,8 @@ func TestUpdateOrderFromDetail(t *testing.T) { } od.UpdateOrderFromDetail(&om) - if od.InternalOrderID == "1" { - t.Error("Should not be able to update the internal order ID") + if od.InternalOrderID != "1" { + t.Error("Failed to initialize the internal order ID") } if !od.ImmediateOrCancel { t.Error("Failed to update") @@ -987,6 +988,15 @@ func TestUpdateOrderFromDetail(t *testing.T) { if od.Trades[0].Amount != 1337 { t.Error("Failed to update trades") } + + om = Detail{ + InternalOrderID: "2", + } + + od.UpdateOrderFromDetail(&om) + if od.InternalOrderID == "2" { + t.Error("Should not be able to update the internal order ID after initialization") + } } func TestClassificationError_Error(t *testing.T) { @@ -1209,6 +1219,136 @@ func TestMatchFilter(t *testing.T) { } } +func TestIsActive(t *testing.T) { + orders := map[int]Detail{ + 0: {Amount: 0.0, Status: Active}, + 1: {Amount: 1.0, ExecutedAmount: 0.9, Status: Active}, + 2: {Amount: 1.0, ExecutedAmount: 1.0, Status: Active}, + 3: {Amount: 1.0, ExecutedAmount: 1.1, Status: Active}, + } + + amountTests := map[int]struct { + o Detail + expRes bool + }{ + 0: {orders[0], false}, + 1: {orders[1], true}, + 2: {orders[2], false}, + 3: {orders[3], false}, + } + // specific tests + for num, tt := range amountTests { + if tt.o.IsActive() != tt.expRes { + t.Errorf("amountTests[%v] failed", num) + } + } + + statusTests := map[int]struct { + o Detail + expRes bool + }{ + 0: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AnyStatus}, true}, + 1: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: New}, true}, + 2: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Active}, true}, + 3: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyCancelled}, false}, + 4: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyFilled}, true}, + 5: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Filled}, false}, + 6: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Cancelled}, false}, + 7: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PendingCancel}, true}, + 8: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: InsufficientBalance}, false}, + 9: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: MarketUnavailable}, false}, + 10: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Rejected}, false}, + 11: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Expired}, false}, + 12: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Hidden}, true}, + 13: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: UnknownStatus}, true}, + 14: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Open}, true}, + 15: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AutoDeleverage}, true}, + 16: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Closed}, false}, + 17: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Pending}, true}, + } + // specific tests + for num, tt := range statusTests { + if tt.o.IsActive() != tt.expRes { + t.Errorf("statusTests[%v] failed", num) + } + } +} + +func TestIsInctive(t *testing.T) { + orders := map[int]Detail{ + 0: {Amount: 0.0, Status: Active}, + 1: {Amount: 1.0, ExecutedAmount: 0.9, Status: Active}, + 2: {Amount: 1.0, ExecutedAmount: 1.0, Status: Active}, + 3: {Amount: 1.0, ExecutedAmount: 1.1, Status: Active}, + } + + amountTests := map[int]struct { + o Detail + expRes bool + }{ + 0: {orders[0], true}, + 1: {orders[1], false}, + 2: {orders[2], true}, + 3: {orders[3], true}, + } + // specific tests + for num, tt := range amountTests { + if tt.o.IsInactive() != tt.expRes { + t.Errorf("amountTests[%v] failed", num) + } + } + + statusTests := map[int]struct { + o Detail + expRes bool + }{ + 0: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AnyStatus}, false}, + 1: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: New}, false}, + 2: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Active}, false}, + 3: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyCancelled}, true}, + 4: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyFilled}, false}, + 5: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Filled}, true}, + 6: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Cancelled}, true}, + 7: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PendingCancel}, false}, + 8: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: InsufficientBalance}, true}, + 9: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: MarketUnavailable}, true}, + 10: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Rejected}, true}, + 11: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Expired}, true}, + 12: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Hidden}, false}, + 13: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: UnknownStatus}, false}, + 14: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Open}, false}, + 15: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AutoDeleverage}, false}, + 16: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Closed}, true}, + 17: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Pending}, false}, + } + // specific tests + for num, tt := range statusTests { + if tt.o.IsInactive() != tt.expRes { + t.Errorf("statusTests[%v] failed", num) + } + } +} + +func TestGenerateInternalOrderID(t *testing.T) { + id, err := uuid.NewV4() + if err != nil { + t.Errorf("unable to create uuid: %s", err) + } + od := Detail{ + InternalOrderID: id.String(), + } + od.GenerateInternalOrderID() + if od.InternalOrderID != id.String() { + t.Error("Should not be able to generate a new internal order ID") + } + + od = Detail{} + od.GenerateInternalOrderID() + if od.InternalOrderID == "" { + t.Error("unable to generate internal order ID") + } +} + func TestDetail_Copy(t *testing.T) { d := []Detail{ { diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 3010d3b663d..72a27fd4c14 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/gofrs/uuid" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" @@ -208,6 +209,9 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) { if d.ID == "" { d.ID = m.ID } + if d.InternalOrderID == "" { + d.InternalOrderID = m.InternalOrderID + } } // UpdateOrderFromModify Will update an order detail (used in order management) @@ -405,6 +409,40 @@ func (d *Detail) MatchFilter(f *Filter) bool { return true } +// IsActive returns true if an order has a status that indicates it is +// currently available on the exchange +func (d *Detail) IsActive() bool { + if d.Amount <= 0 || d.Amount <= d.ExecutedAmount { + return false + } + return d.Status == Active || d.Status == Open || d.Status == PartiallyFilled || d.Status == New || + d.Status == AnyStatus || d.Status == PendingCancel || d.Status == Hidden || d.Status == UnknownStatus || + d.Status == AutoDeleverage || d.Status == Pending +} + +// IsInactive returns true if an order has a status that indicates it is +// currently not available on the exchange +func (d *Detail) IsInactive() bool { + if d.Amount <= 0 || d.Amount <= d.ExecutedAmount { + return true + } + return d.Status == Filled || d.Status == Cancelled || d.Status == InsufficientBalance || d.Status == MarketUnavailable || + d.Status == Rejected || d.Status == PartiallyCancelled || d.Status == Expired || d.Status == Closed +} + +// GenerateInternalOrderID sets a new V4 order ID or a V5 order ID if +// the V4 function returns an error +func (d *Detail) GenerateInternalOrderID() { + if d.InternalOrderID == "" { + var id uuid.UUID + id, err := uuid.NewV4() + if err != nil { + id = uuid.NewV5(uuid.UUID{}, d.ID) + } + d.InternalOrderID = id.String() + } +} + // Copy will return a copy of Detail func (d *Detail) Copy() Detail { c := *d