FTX: Funding rates, payments & stats + order manager tracking (#976)

* Adds basic PoC for calculating/retrieving position data

* A very unfortunate day of miscalculations

* Adds position summary and funding rate details to RPC

* Offline funding rate calculations

* More helpers, more stats, refining data, automated retrieval

* Adds new rpc server commands and attempts some organisation

* lower string, lower stress

* Adds ordermanager config. Fleshes outcli. Tracks positions automatically

* Adds new separation for funding payments/rates

* Combines funding rates and payments

* Fun test coverage

* ALL THE TESTS... I hope

* Fixes

* polishes ftx tests. improves perp check. Loops rates

* Final touches before nit attax

* buff 💪

* Stops NotYetImplemented spam with one simple trick!

* Some lovely little niteroos

* linteroo

* Clarifies a couple of errors to help narrow likely end user problems

* Fixes asset type bug, fixes closed position order return, fixes unset status bug

* Fixes order manager handling when no rates are available yet

* Continues on no funding rates instead. Removes err

* Don't show predicted rate if the time is zero

* Addresses scenario with no funding rate payments

* Bug fixes and commentary before updating maps to use *currency.Item

* Adds a pair key type

* Polishes pKey, fixes map order bug

* key is not a property in the event someone changes the base/quote

* Adds improvements to order processing...Breaks it all

* Shakes up the design of things by removing a function

* Fixes issues with order manager positions. Limits update range

* Fixes build issues. Identification of bad tests.

* Merges and fixes features from master and this branch

* buff linter 💪

* re-gen

* proto regen

* Addresses some nits. But not all of them.

* Fixes issue where funding rates weren't returned 🎉

* completes transition futures tracking to map[*currency.Item]map[*currency.Item]

* who did that? not me

* removes redundant check on account of being redundant and unnecessary

* so buf

* addresses nits: duplications, startTime, loops, go tidy, typos

* fixes minor mistakes

* fixes 🍣 🐻 changes to int64
This commit is contained in:
Scott
2022-08-23 12:16:50 +10:00
committed by GitHub
parent e93ee83563
commit 46cadd6f15
50 changed files with 9249 additions and 3730 deletions

View File

@@ -163,6 +163,7 @@ func validateSettings(b *Engine, s *Settings, flagSet FlagSet) {
b.Settings = *s
flagSet.WithBool("coinmarketcap", &b.Settings.EnableCoinmarketcapAnalysis, b.Config.Currency.CryptocurrencyProvider.Enabled)
flagSet.WithBool("ordermanager", &b.Settings.EnableOrderManager, b.Config.OrderManager.Enabled != nil && *b.Config.OrderManager.Enabled)
flagSet.WithBool("currencyconverter", &b.Settings.EnableCurrencyConverter, b.Config.Currency.ForexProviders.IsEnabled("currencyconverter"))
@@ -525,8 +526,9 @@ func (bot *Engine) Start() error {
bot.ExchangeManager,
bot.CommunicationsManager,
&bot.ServicesWG,
bot.Settings.EnableFuturesTracking,
bot.Settings.Verbose)
bot.Config.OrderManager.Verbose,
bot.Config.OrderManager.ActivelyTrackFuturesPositions,
bot.Config.OrderManager.FuturesTrackingSeekDuration)
if err != nil {
gctlog.Errorf(gctlog.Global, "Order manager unable to setup: %s", err)
} else {

View File

@@ -137,8 +137,9 @@ func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error {
bot.ExchangeManager,
bot.CommunicationsManager,
&bot.ServicesWG,
bot.Settings.EnableFuturesTracking,
bot.Settings.Verbose)
bot.Config.OrderManager.Verbose,
bot.Config.OrderManager.ActivelyTrackFuturesPositions,
bot.Config.OrderManager.FuturesTrackingSeekDuration)
if err != nil {
return err
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -21,7 +22,7 @@ import (
)
// SetupOrderManager will boot up the OrderManager
func SetupOrderManager(exchangeManager iExchangeManager, communicationsManager iCommsManager, wg *sync.WaitGroup, enabledFuturesTracking, verbose bool) (*OrderManager, error) {
func SetupOrderManager(exchangeManager iExchangeManager, communicationsManager iCommsManager, wg *sync.WaitGroup, verbose, activelyTrackFuturesPositions bool, futuresTrackingSeekDuration time.Duration) (*OrderManager, error) {
if exchangeManager == nil {
return nil, errNilExchangeManager
}
@@ -32,18 +33,28 @@ func SetupOrderManager(exchangeManager iExchangeManager, communicationsManager i
return nil, errNilWaitGroup
}
return &OrderManager{
shutdown: make(chan struct{}),
om := &OrderManager{
shutdown: make(chan struct{}),
activelyTrackFuturesPositions: activelyTrackFuturesPositions,
orderStore: store{
Orders: make(map[string][]*order.Detail),
exchangeManager: exchangeManager,
commsManager: communicationsManager,
wg: wg,
futuresPositionController: order.SetupPositionController(),
trackFuturesPositions: enabledFuturesTracking,
},
verbose: verbose,
}, nil
}
if activelyTrackFuturesPositions {
if futuresTrackingSeekDuration > 0 {
futuresTrackingSeekDuration = -futuresTrackingSeekDuration
}
if futuresTrackingSeekDuration == 0 {
futuresTrackingSeekDuration = defaultOrderSeekTime
}
om.futuresPositionSeekDuration = futuresTrackingSeekDuration
}
return om, nil
}
// IsRunning safely checks whether the subsystem is running
@@ -85,7 +96,7 @@ func (m *OrderManager) gracefulShutdown() {
if !m.cfg.CancelOrdersOnShutdown {
return
}
log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...")
log.Debugln(log.OrderMgr, "Cancelling any open orders...")
exchanges, err := m.orderStore.exchangeManager.GetExchanges()
if err != nil {
log.Errorf(log.OrderMgr, "Order manager cannot get exchanges: %v", err)
@@ -133,7 +144,7 @@ func (m *OrderManager) CancelAllOrders(ctx context.Context, exchanges []exchange
continue
}
for j := range orders {
log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.", exchanges[i].GetName())
log.Debugf(log.OrderMgr, "Cancelling order(s) for exchange %s.", exchanges[i].GetName())
cancel, err := orders[j].DeriveCancel()
if err != nil {
log.Error(log.OrderMgr, err)
@@ -189,7 +200,7 @@ func (m *OrderManager) Cancel(ctx context.Context, cancel *order.Cancel) error {
return err
}
log.Debugf(log.OrderMgr, "Order manager: Cancelling order ID %v [%+v]",
log.Debugf(log.OrderMgr, "Cancelling order ID %v [%+v]",
cancel.OrderID, cancel)
err = exch.CancelOrder(ctx, cancel)
@@ -210,7 +221,7 @@ func (m *OrderManager) Cancel(ctx context.Context, cancel *order.Cancel) error {
return err
}
msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.",
msg := fmt.Sprintf("Exchange %s order ID=%v cancelled.",
od.Exchange, od.OrderID)
log.Debugln(log.OrderMgr, msg)
m.orderStore.commsManager.PushEvent(base.Event{Type: "order", Message: msg})
@@ -219,16 +230,13 @@ func (m *OrderManager) Cancel(ctx context.Context, cancel *order.Cancel) error {
// GetFuturesPositionsForExchange returns futures positions stored within
// the order manager's futures position tracker that match the provided params
func (m *OrderManager) GetFuturesPositionsForExchange(exch string, item asset.Item, pair currency.Pair) ([]order.PositionStats, error) {
func (m *OrderManager) GetFuturesPositionsForExchange(exch string, item asset.Item, pair currency.Pair) ([]order.Position, 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)
}
if m.orderStore.futuresPositionController == nil {
return nil, errFuturesTrackerNotSetup
}
if !item.IsFutures() {
return nil, fmt.Errorf("%v %w", item, order.ErrNotFuturesAsset)
}
@@ -236,6 +244,39 @@ func (m *OrderManager) GetFuturesPositionsForExchange(exch string, item asset.It
return m.orderStore.futuresPositionController.GetPositionsForExchange(exch, item, pair)
}
// GetOpenFuturesPosition returns an open futures position stored within
// the order manager's futures position tracker that match the provided params
func (m *OrderManager) GetOpenFuturesPosition(exch string, item asset.Item, pair currency.Pair) (*order.Position, 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)
}
if !item.IsFutures() {
return nil, fmt.Errorf("%v %w", item, order.ErrNotFuturesAsset)
}
if !m.activelyTrackFuturesPositions {
return nil, errFuturesTrackingDisabled
}
return m.orderStore.futuresPositionController.GetOpenPosition(exch, item, pair)
}
// GetAllOpenFuturesPositions returns all open futures positions stored within
// the order manager's futures position tracker that match the provided params
func (m *OrderManager) GetAllOpenFuturesPositions() ([]order.Position, 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)
}
if !m.activelyTrackFuturesPositions {
return nil, errFuturesTrackingDisabled
}
return m.orderStore.futuresPositionController.GetAllOpenPositions()
}
// ClearFuturesTracking will clear existing futures positions for a given exchange,
// asset, pair for the event that positions have not been tracked accurately
func (m *OrderManager) ClearFuturesTracking(exch string, item asset.Item, pair currency.Pair) error {
@@ -245,9 +286,6 @@ func (m *OrderManager) ClearFuturesTracking(exch string, item asset.Item, pair c
if atomic.LoadInt32(&m.started) == 0 {
return fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
if m.orderStore.futuresPositionController == nil {
return errFuturesTrackerNotSetup
}
if !item.IsFutures() {
return fmt.Errorf("%v %w", item, order.ErrNotFuturesAsset)
}
@@ -265,9 +303,6 @@ func (m *OrderManager) UpdateOpenPositionUnrealisedPNL(e string, item asset.Item
if atomic.LoadInt32(&m.started) == 0 {
return decimal.Zero, fmt.Errorf("order manager %w", ErrSubSystemNotStarted)
}
if m.orderStore.futuresPositionController == nil {
return decimal.Zero, errFuturesTrackerNotSetup
}
if !item.IsFutures() {
return decimal.Zero, fmt.Errorf("%v %w", item, order.ErrNotFuturesAsset)
}
@@ -381,7 +416,7 @@ func (m *OrderManager) Modify(ctx context.Context, mod *order.Modify) (*order.Mo
res, err := exch.ModifyOrder(ctx, mod)
if err != nil {
message := fmt.Sprintf(
"Order manager: Exchange %s order ID=%v: failed to modify",
"Exchange %s order ID=%v: failed to modify",
mod.Exchange,
mod.OrderID,
)
@@ -401,9 +436,9 @@ func (m *OrderManager) Modify(ctx context.Context, mod *order.Modify) (*order.Mo
// Notify observers.
var message string
if err != nil {
message = "Order manager: Exchange %s order ID=%v: modified on exchange, but failed to modify locally"
message = "Exchange %s order ID=%v: modified on exchange, but failed to modify locally"
} else {
message = "Order manager: Exchange %s order ID=%v: modified successfully"
message = "Exchange %s order ID=%v: modified successfully"
}
m.orderStore.commsManager.PushEvent(base.Event{
Type: "order",
@@ -551,7 +586,7 @@ func (m *OrderManager) processSubmittedOrder(newOrderResp *order.SubmitResponse)
id, err := uuid.NewV4()
if err != nil {
log.Warnf(log.OrderMgr, "Order manager: Unable to generate UUID. Err: %s", err)
log.Warnf(log.OrderMgr, "Unable to generate UUID. Err: %s", err)
}
detail, err := newOrderResp.DeriveDetail(id)
@@ -559,7 +594,7 @@ func (m *OrderManager) processSubmittedOrder(newOrderResp *order.SubmitResponse)
return nil, err
}
msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v quoteAmount=%v side=%v type=%v for time %v.",
msg := fmt.Sprintf("Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v quoteAmount=%v side=%v type=%v for time %v.",
detail.Exchange,
detail.OrderID,
detail.InternalOrderID.String(),
@@ -592,28 +627,29 @@ func (m *OrderManager) processOrders() {
return
}
defer 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)
log.Errorf(log.OrderMgr, "order manager cannot get exchanges: %v", err)
return
}
var wg sync.WaitGroup
for i := range exchanges {
if !exchanges[i].IsRESTAuthenticationSupported() {
for x := range exchanges {
if !exchanges[x].IsRESTAuthenticationSupported() {
continue
}
log.Debugf(log.OrderMgr,
"Order manager: Processing orders for exchange %v.",
exchanges[i].GetName())
enabledAssets := exchanges[i].GetAssetTypes(true)
if m.verbose {
log.Debugf(log.OrderMgr,
"Processing orders for exchange %v",
exchanges[x].GetName())
}
enabledAssets := exchanges[x].GetAssetTypes(true)
for y := range enabledAssets {
pairs, err := exchanges[i].GetEnabledPairs(enabledAssets[y])
var pairs currency.Pairs
pairs, err = exchanges[x].GetEnabledPairs(enabledAssets[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(),
"Unable to get enabled pairs for %s and asset type %s: %s",
exchanges[x].GetName(),
enabledAssets[y],
err)
continue
@@ -622,18 +658,18 @@ func (m *OrderManager) processOrders() {
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(),
"No pairs enabled for %s and asset type %s, skipping...",
exchanges[x].GetName(),
enabledAssets[y])
}
continue
}
filter := &order.Filter{Exchange: exchanges[i].GetName()}
filter := &order.Filter{Exchange: exchanges[x].GetName()}
orders := m.orderStore.getActiveOrders(filter)
order.FilterOrdersByPairs(&orders, pairs)
result, err := exchanges[i].GetActiveOrders(context.TODO(), &order.GetOrdersRequest{
var result []order.Detail
result, err = exchanges[x].GetActiveOrders(context.TODO(), &order.GetOrdersRequest{
Side: order.AnySide,
Type: order.AnyType,
Pairs: pairs,
@@ -641,38 +677,141 @@ func (m *OrderManager) processOrders() {
})
if err != nil {
log.Errorf(log.OrderMgr,
"Order manager: Unable to get active orders for %s and asset type %s: %s",
exchanges[i].GetName(),
"Unable to get active orders for %s and asset type %s: %s",
exchanges[x].GetName(),
enabledAssets[y],
err)
continue
}
if len(orders) == 0 && len(result) == 0 {
continue
}
for z := range result {
upsertResponse, err := m.UpsertOrder(&result[z])
if err != nil {
log.Error(log.OrderMgr, err)
} else {
for i := range orders {
if orders[i].InternalOrderID != upsertResponse.OrderDetails.InternalOrderID {
continue
if len(orders) > 0 && len(result) > 0 {
for z := range result {
var upsertResponse *OrderUpsertResponse
upsertResponse, err = m.UpsertOrder(&result[z])
if err != nil {
log.Error(log.OrderMgr, err)
} else {
for i := range orders {
if orders[i].InternalOrderID != upsertResponse.OrderDetails.InternalOrderID {
continue
}
orders[i] = orders[len(orders)-1]
orders = orders[:len(orders)-1]
break
}
orders[i] = orders[len(orders)-1]
orders = orders[:len(orders)-1]
}
}
}
if !exchanges[i].GetBase().GetSupportedFeatures().RESTCapabilities.GetOrder {
continue
if exchanges[x].GetBase().GetSupportedFeatures().RESTCapabilities.GetOrder {
wg.Add(1)
go m.processMatchingOrders(exchanges[x], orders, &wg)
}
if m.activelyTrackFuturesPositions && enabledAssets[y].IsFutures() {
var positions []order.PositionDetails
var sd time.Time
sd, err = m.orderStore.futuresPositionController.LastUpdated()
if err != nil {
log.Error(log.OrderMgr, err)
return
}
if sd.IsZero() {
sd = time.Now().Add(m.futuresPositionSeekDuration)
}
positions, err = exchanges[x].GetFuturesPositions(context.TODO(), &order.PositionsRequest{
Asset: enabledAssets[y],
Pairs: pairs,
StartDate: sd,
})
if err != nil {
if !errors.Is(err, common.ErrNotYetImplemented) {
log.Error(log.OrderMgr, err)
}
return
}
for z := range positions {
if len(positions[z].Orders) == 0 {
continue
}
err = m.processFuturesPositions(exchanges[x], &positions[z])
if err != nil {
log.Errorf(log.OrderMgr, "unable to process future positions for %v %v %v. err: %v", positions[z].Exchange, positions[z].Asset, positions[z].Pair, err)
}
}
}
wg.Add(1)
go m.processMatchingOrders(exchanges[i], orders, &wg)
}
}
wg.Wait()
if m.verbose {
log.Debugf(log.OrderMgr, "Finished processing orders")
}
}
// processFuturesPositions ensures any open position found is kept up to date in the order manager
func (m *OrderManager) processFuturesPositions(exch exchange.IBotExchange, position *order.PositionDetails) error {
if !m.activelyTrackFuturesPositions {
return errFuturesTrackingDisabled
}
if exch == nil {
return fmt.Errorf("%w IBotExchange", common.ErrNilPointer)
}
if position == nil {
return fmt.Errorf("%w PositionDetails", common.ErrNilPointer)
}
if len(position.Orders) == 0 {
return fmt.Errorf("%w position for '%v' '%v' '%v' has no orders", errNilOrder, position.Exchange, position.Asset, position.Pair)
}
sort.Slice(position.Orders, func(i, j int) bool {
return position.Orders[i].Date.Before(position.Orders[j].Date)
})
var err error
for i := range position.Orders {
err = m.orderStore.futuresPositionController.TrackNewOrder(&position.Orders[i])
if err != nil {
return err
}
}
_, err = m.orderStore.futuresPositionController.GetOpenPosition(position.Exchange, position.Asset, position.Pair)
if err != nil {
if errors.Is(err, order.ErrPositionNotFound) {
return nil
}
return err
}
tick, err := exch.FetchTicker(context.TODO(), position.Pair, position.Asset)
if err != nil {
return fmt.Errorf("%w when fetching ticker data for %v %v %v", err, position.Exchange, position.Asset, position.Pair)
}
_, err = m.UpdateOpenPositionUnrealisedPNL(position.Exchange, position.Asset, position.Pair, tick.Last, tick.LastUpdated)
if err != nil {
return fmt.Errorf("%w when updating unrealised PNL for %v %v %v", err, position.Exchange, position.Asset, position.Pair)
}
isPerp, err := exch.IsPerpetualFutureCurrency(position.Asset, position.Pair)
if err != nil {
return err
}
if !isPerp {
return nil
}
frp, err := exch.GetFundingRates(context.TODO(), &order.FundingRatesRequest{
Asset: position.Asset,
Pairs: currency.Pairs{position.Pair},
StartDate: position.Orders[0].Date,
EndDate: time.Now(),
IncludePayments: true,
IncludePredictedRate: true,
})
if err != nil {
return err
}
for i := range frp {
err = m.orderStore.futuresPositionController.TrackFundingDetails(&frp[i])
if err != nil {
return err
}
}
return nil
}
func (m *OrderManager) processMatchingOrders(exch exchange.IBotExchange, orders []order.Detail, wg *sync.WaitGroup) {
@@ -770,7 +909,7 @@ func (m *OrderManager) UpsertOrder(od *order.Detail) (resp *OrderUpsertResponse,
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",
"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.OrderID, od.InternalOrderID, od.Pair, od.Price, od.Amount, od.Side, od.Type, od.Status, err)
return nil, err
}
@@ -779,7 +918,7 @@ func (m *OrderManager) UpsertOrder(od *order.Detail) (resp *OrderUpsertResponse,
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.",
msg = fmt.Sprintf("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.OrderID, upsertResponse.OrderDetails.InternalOrderID,
upsertResponse.OrderDetails.Pair, upsertResponse.OrderDetails.Price, upsertResponse.OrderDetails.Amount,
upsertResponse.OrderDetails.Side, upsertResponse.OrderDetails.Type, upsertResponse.OrderDetails.Status)
@@ -890,7 +1029,7 @@ func (s *store) upsert(od *order.Detail) (*OrderUpsertResponse, error) {
}
s.m.Lock()
defer s.m.Unlock()
if s.trackFuturesPositions && od.AssetType.IsFutures() {
if od.AssetType.IsFutures() {
err = s.futuresPositionController.TrackNewOrder(od)
if err != nil && !errors.Is(err, order.ErrPositionClosed) {
return nil, err

View File

@@ -8,7 +8,9 @@ import (
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
@@ -16,9 +18,10 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
// omfExchange aka ordermanager fake exchange overrides exchange functions
// omfExchange aka order manager fake exchange overrides exchange functions
// we're not testing an actual exchange's implemented functions
type omfExchange struct {
exchange.IBotExchange
@@ -30,6 +33,31 @@ func (f omfExchange) CancelOrder(ctx context.Context, o *order.Cancel) error {
return nil
}
func (f omfExchange) FetchTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
return &ticker.Price{
Last: 1337,
High: 1337,
Low: 1337,
Bid: 1337,
Ask: 1337,
Volume: 1337,
QuoteVolume: 1337,
PriceATH: 1337,
Open: 1337,
Close: 1337,
Pair: p,
ExchangeName: f.GetName(),
AssetType: a,
LastUpdated: time.Now(),
FlashReturnRate: 1337,
BidPeriod: 1337,
BidSize: 1337,
AskPeriod: 1337,
AskSize: 1337,
FlashReturnRateAmount: 1337,
}, 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(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) {
@@ -92,22 +120,64 @@ func (f omfExchange) ModifyOrder(ctx context.Context, action *order.Modify) (*or
return ans, nil
}
func (f omfExchange) GetFuturesPositions(ctx context.Context, req *order.PositionsRequest) ([]order.PositionDetails, error) {
id, err := uuid.NewV4()
if err != nil {
return nil, err
}
resp := make([]order.PositionDetails, len(req.Pairs))
tt := time.Now()
for i := range req.Pairs {
resp[i] = order.PositionDetails{
Exchange: f.GetName(),
Asset: req.Asset,
Pair: req.Pairs[i],
Orders: []order.Detail{
{
Exchange: f.GetName(),
Price: 1337,
Amount: 1337,
InternalOrderID: id,
OrderID: "1337",
ClientOrderID: "1337",
Type: order.Market,
Side: order.Short,
Status: order.Open,
AssetType: req.Asset,
Date: tt,
CloseTime: tt,
LastUpdated: tt,
Pair: req.Pairs[i],
},
},
}
}
return resp, nil
}
func TestSetupOrderManager(t *testing.T) {
_, err := SetupOrderManager(nil, nil, nil, false, false)
_, err := SetupOrderManager(nil, nil, nil, false, false, 0)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
_, err = SetupOrderManager(SetupExchangeManager(), nil, nil, false, false)
_, err = SetupOrderManager(SetupExchangeManager(), nil, nil, false, false, 0)
if !errors.Is(err, errNilCommunicationsManager) {
t.Errorf("error '%v', expected '%v'", err, errNilCommunicationsManager)
}
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, nil, false, false)
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, nil, false, false, 0)
if !errors.Is(err, errNilWaitGroup) {
t.Errorf("error '%v', expected '%v'", err, errNilWaitGroup)
}
var wg sync.WaitGroup
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false)
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, true, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
_, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, true, 1337)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
@@ -120,7 +190,7 @@ func TestOrderManagerStart(t *testing.T) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
var wg sync.WaitGroup
m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false)
m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
@@ -141,7 +211,7 @@ func TestOrderManagerIsRunning(t *testing.T) {
}
var wg sync.WaitGroup
m, err := SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false)
m, err := SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
@@ -166,7 +236,7 @@ func TestOrderManagerStop(t *testing.T) {
}
var wg sync.WaitGroup
m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false)
m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
@@ -204,12 +274,11 @@ func OrdersSetup(t *testing.T) *OrderManager {
if err != nil {
t.Fatal(err)
}
fakeExchange := omfExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
m, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false)
m, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
@@ -674,7 +743,7 @@ func TestProcessOrders(t *testing.T) {
IBotExchange: exch,
}
em.Add(fakeExchange)
m, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false)
m, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
@@ -712,6 +781,11 @@ func TestProcessOrders(t *testing.T) {
Enabled: pairs,
Available: pairs,
},
asset.Futures: {
AssetEnabled: convert.BoolPtr(true),
Enabled: pairs,
Available: pairs,
},
},
}
exch.GetBase().Config = &config.Exchange{
@@ -731,6 +805,11 @@ func TestProcessOrders(t *testing.T) {
Enabled: pairs,
Available: pairs,
},
asset.Futures: {
AssetEnabled: convert.BoolPtr(true),
Enabled: pairs,
Available: pairs,
},
},
},
}
@@ -773,6 +852,21 @@ func TestProcessOrders(t *testing.T) {
}
}
m.orderStore.futuresPositionController = order.SetupPositionController()
if err = m.orderStore.add(&order.Detail{
Exchange: testExchange,
Pair: pairs[0],
AssetType: asset.Futures,
Amount: 2.0,
Side: order.Short,
Status: order.Open,
LastUpdated: time.Now().Add(-time.Hour),
OrderID: "4",
Date: time.Now(),
}); err != nil {
t.Error(err)
}
m.processOrders()
// Order1 is not returned by exch.GetActiveOrders()
@@ -1050,10 +1144,6 @@ func TestGetFuturesPositionsForExchange(t *testing.T) {
t.Errorf("received '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
o.started = 1
_, err = o.GetFuturesPositionsForExchange("test", asset.Spot, cp)
if !errors.Is(err, errFuturesTrackerNotSetup) {
t.Errorf("received '%v', expected '%v'", err, errFuturesTrackerNotSetup)
}
o.orderStore.futuresPositionController = order.SetupPositionController()
_, err = o.GetFuturesPositionsForExchange("test", asset.Spot, cp)
if !errors.Is(err, order.ErrNotFuturesAsset) {
@@ -1061,8 +1151,8 @@ func TestGetFuturesPositionsForExchange(t *testing.T) {
}
_, err = o.GetFuturesPositionsForExchange("test", asset.Futures, cp)
if !errors.Is(err, order.ErrPositionsNotLoadedForExchange) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionsNotLoadedForExchange)
if !errors.Is(err, order.ErrPositionNotFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionNotFound)
}
err = o.orderStore.futuresPositionController.TrackNewOrder(&order.Detail{
@@ -1101,10 +1191,6 @@ func TestClearFuturesPositionsForExchange(t *testing.T) {
t.Errorf("received '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
o.started = 1
err = o.ClearFuturesTracking("test", asset.Spot, cp)
if !errors.Is(err, errFuturesTrackerNotSetup) {
t.Errorf("received '%v', expected '%v'", err, errFuturesTrackerNotSetup)
}
o.orderStore.futuresPositionController = order.SetupPositionController()
err = o.ClearFuturesTracking("test", asset.Spot, cp)
if !errors.Is(err, order.ErrNotFuturesAsset) {
@@ -1112,8 +1198,8 @@ func TestClearFuturesPositionsForExchange(t *testing.T) {
}
err = o.ClearFuturesTracking("test", asset.Futures, cp)
if !errors.Is(err, order.ErrPositionsNotLoadedForExchange) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionsNotLoadedForExchange)
if !errors.Is(err, order.ErrPositionNotFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionNotFound)
}
err = o.orderStore.futuresPositionController.TrackNewOrder(&order.Detail{
@@ -1156,10 +1242,6 @@ func TestUpdateOpenPositionUnrealisedPNL(t *testing.T) {
t.Errorf("received '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
o.started = 1
_, err = o.UpdateOpenPositionUnrealisedPNL("test", asset.Spot, cp, 1, time.Now())
if !errors.Is(err, errFuturesTrackerNotSetup) {
t.Errorf("received '%v', expected '%v'", err, errFuturesTrackerNotSetup)
}
o.orderStore.futuresPositionController = order.SetupPositionController()
_, err = o.UpdateOpenPositionUnrealisedPNL("test", asset.Spot, cp, 1, time.Now())
if !errors.Is(err, order.ErrNotFuturesAsset) {
@@ -1167,8 +1249,8 @@ func TestUpdateOpenPositionUnrealisedPNL(t *testing.T) {
}
_, err = o.UpdateOpenPositionUnrealisedPNL("test", asset.Futures, cp, 1, time.Now())
if !errors.Is(err, order.ErrPositionsNotLoadedForExchange) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionsNotLoadedForExchange)
if !errors.Is(err, order.ErrPositionNotFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionNotFound)
}
err = o.orderStore.futuresPositionController.TrackNewOrder(&order.Detail{
@@ -1350,3 +1432,214 @@ func TestOrderManagerAdd(t *testing.T) {
t.Errorf("received '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestGetAllOpenFuturesPositions(t *testing.T) {
t.Parallel()
wg := &sync.WaitGroup{}
o, err := SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
o.started = 0
_, err = o.GetAllOpenFuturesPositions()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("received '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
o.started = 1
o.activelyTrackFuturesPositions = true
o.orderStore.futuresPositionController = order.SetupPositionController()
_, err = o.GetAllOpenFuturesPositions()
if !errors.Is(err, order.ErrNoPositionsFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNoPositionsFound)
}
o = nil
_, err = o.GetAllOpenFuturesPositions()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("received '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestGetOpenFuturesPosition(t *testing.T) {
t.Parallel()
wg := &sync.WaitGroup{}
o, err := SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
o.started = 0
cp := currency.NewPair(currency.BTC, currency.PERP)
_, err = o.GetOpenFuturesPosition(testExchange, asset.Spot, cp)
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("received '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
o.started = 1
_, err = o.GetOpenFuturesPosition(testExchange, asset.Spot, cp)
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNotFuturesAsset)
}
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
RequestFormat: &currency.PairFormat{Delimiter: "-"},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
o, err = SetupOrderManager(em, &CommunicationManager{}, wg, false, true, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
o.started = 1
_, err = o.GetOpenFuturesPosition(testExchange, asset.Spot, cp)
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNotFuturesAsset)
}
_, err = o.GetOpenFuturesPosition(testExchange, asset.Futures, cp)
if !errors.Is(err, order.ErrPositionNotFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionNotFound)
}
err = o.orderStore.futuresPositionController.TrackNewOrder(&order.Detail{
AssetType: asset.Futures,
OrderID: "123",
Pair: cp,
Side: order.Buy,
Type: order.Market,
Date: time.Now(),
Amount: 1337,
Exchange: testExchange,
})
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
_, err = o.GetOpenFuturesPosition(testExchange, asset.Futures, cp)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
o = nil
_, err = o.GetOpenFuturesPosition(testExchange, asset.Spot, cp)
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("received '%v', expected '%v'", err, ErrNilSubsystem)
}
}
func TestProcessFuturesPositions(t *testing.T) {
t.Parallel()
o := &OrderManager{}
err := o.processFuturesPositions(nil, nil)
if !errors.Is(err, errFuturesTrackingDisabled) {
t.Errorf("received '%v', expected '%v'", err, errFuturesTrackingDisabled)
}
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-perp")
if err != nil {
t.Fatal(err)
}
cp2, err := currency.NewPairFromString("btc-usd")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
RequestFormat: &currency.PairFormat{Delimiter: "-"},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
Available: currency.Pairs{cp, cp2},
Enabled: currency.Pairs{cp, cp2},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp, cp2},
Enabled: currency.Pairs{cp, cp2},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
var wg sync.WaitGroup
o, err = SetupOrderManager(em, &CommunicationManager{}, &wg, false, true, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
o.started = 1
err = o.processFuturesPositions(fakeExchange, nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received '%v', expected '%v'", err, common.ErrNilPointer)
}
position := &order.PositionDetails{
Exchange: b.Name,
Asset: asset.Spot,
Pair: cp,
Orders: nil,
}
err = o.processFuturesPositions(fakeExchange, position)
if !errors.Is(err, errNilOrder) {
t.Errorf("received '%v', expected '%v'", err, errNilOrder)
}
od := &order.Detail{
AssetType: asset.Spot,
OrderID: "123",
Pair: cp,
Side: order.Buy,
Type: order.Market,
Date: time.Now().Add(-time.Hour),
Amount: 1337,
Exchange: b.Name,
}
position.Orders = []order.Detail{
*od,
}
err = o.processFuturesPositions(fakeExchange, position)
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNotFuturesAsset)
}
position.Orders[0].AssetType = asset.Futures
position.Asset = asset.Futures
err = o.processFuturesPositions(fakeExchange, position)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}

View File

@@ -23,9 +23,9 @@ var (
errNilCommunicationsManager = errors.New("cannot start with nil communications manager")
errNilOrder = errors.New("nil order received")
errFuturesTrackerNotSetup = errors.New("futures position tracker not setup")
orderManagerDelay = time.Second * 10
errFuturesTrackingDisabled = errors.New("tracking futures positions disabled. enable it via config under orderManager activelyTrackFuturesPositions")
orderManagerDelay = time.Second * 10
defaultOrderSeekTime = -time.Hour * 24 * 365
)
type orderManagerConfig struct {
@@ -38,6 +38,18 @@ type orderManagerConfig struct {
OrderSubmissionRetries int64
}
// OrderManager processes and stores orders across enabled exchanges
type OrderManager struct {
started int32
processingOrders int32
shutdown chan struct{}
orderStore store
cfg orderManagerConfig
verbose bool
activelyTrackFuturesPositions bool
futuresPositionSeekDuration time.Duration
}
// store holds all orders by exchange
type store struct {
m sync.RWMutex
@@ -45,18 +57,7 @@ type store struct {
commsManager iCommsManager
exchangeManager iExchangeManager
wg *sync.WaitGroup
futuresPositionController *order.PositionController
trackFuturesPositions bool
}
// OrderManager processes and stores orders across enabled exchanges
type OrderManager struct {
started int32
processingOrders int32
shutdown chan struct{}
orderStore store
cfg orderManagerConfig
verbose bool
futuresPositionController order.PositionController
}
// OrderSubmitResponse contains the order response along with an internal order ID

View File

@@ -70,6 +70,7 @@ var (
errCurrencyNotSpecified = errors.New("a currency must be specified")
errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list")
errNoTrades = errors.New("no trades returned from supplied params")
errUnexpectedResponseSize = errors.New("unexpected slice size")
errNilRequestData = errors.New("nil request data received, cannot continue")
errNoAccountInformation = errors.New("account information does not exist")
errShutdownNotAllowed = errors.New("shutting down this bot instance is not allowed via gRPC, please enable by command line flag --grpcshutdown or config.json field grpcAllowBotShutdown")
@@ -998,10 +999,10 @@ func (s *RPCServer) GetOrders(ctx context.Context, r *gctrpc.GetOrdersRequest) (
Trades: trades,
}
if !resp[x].Date.IsZero() {
o.CreationTime = s.unixTimestamp(resp[x].Date)
o.CreationTime = resp[x].Date.Format(common.SimpleTimeFormatWithTimezone)
}
if !resp[x].LastUpdated.IsZero() {
o.UpdateTime = s.unixTimestamp(resp[x].LastUpdated)
o.UpdateTime = resp[x].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
orders[x] = o
}
@@ -1087,10 +1088,10 @@ func (s *RPCServer) GetManagedOrders(_ context.Context, r *gctrpc.GetOrdersReque
Trades: trades,
}
if !resp[x].Date.IsZero() {
o.CreationTime = s.unixTimestamp(resp[x].Date)
o.CreationTime = resp[x].Date.Format(common.SimpleTimeFormatWithTimezone)
}
if !resp[x].LastUpdated.IsZero() {
o.UpdateTime = s.unixTimestamp(resp[x].LastUpdated)
o.UpdateTime = resp[x].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
orders[x] = o
}
@@ -1152,12 +1153,12 @@ func (s *RPCServer) GetOrder(ctx context.Context, r *gctrpc.GetOrderRequest) (*g
}
}
var creationTime, updateTime int64
var creationTime, updateTime string
if !result.Date.IsZero() {
creationTime = s.unixTimestamp(result.Date)
creationTime = result.Date.Format(common.SimpleTimeFormatWithTimezone)
}
if !result.LastUpdated.IsZero() {
updateTime = s.unixTimestamp(result.LastUpdated)
updateTime = result.LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
return &gctrpc.OrderDetails{
@@ -4179,8 +4180,171 @@ func (s *RPCServer) CurrencyStateTradingPair(_ context.Context, r *gctrpc.Curren
ai)
}
func (s *RPCServer) buildFuturePosition(position *order.Position, getFundingPayments, includeFundingRates, includeOrders, includePredictedRate bool) *gctrpc.FuturePosition {
response := &gctrpc.FuturePosition{
Exchange: position.Exchange,
Asset: position.Asset.String(),
Pair: &gctrpc.CurrencyPair{
Delimiter: position.Pair.Delimiter,
Base: position.Pair.Base.String(),
Quote: position.Pair.Quote.String(),
},
Status: position.Status.String(),
OpeningDate: position.OpeningDate.Format(common.SimpleTimeFormatWithTimezone),
OpeningDirection: position.OpeningDirection.String(),
OpeningPrice: position.OpeningPrice.String(),
OpeningSize: position.OpeningSize.String(),
CurrentDirection: position.LatestDirection.String(),
CurrentPrice: position.LatestPrice.String(),
CurrentSize: position.LatestSize.String(),
UnrealisedPnl: position.UnrealisedPNL.String(),
RealisedPnl: position.RealisedPNL.String(),
OrderCount: int64(len(position.Orders)),
}
if getFundingPayments {
var sum decimal.Decimal
fundingData := &gctrpc.FundingData{}
for i := range position.FundingRates.FundingRates {
if includeFundingRates {
fundingData.Rates = append(fundingData.Rates, &gctrpc.FundingRate{
Date: position.FundingRates.FundingRates[i].Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: position.FundingRates.FundingRates[i].Rate.String(),
Payment: position.FundingRates.FundingRates[i].Payment.String(),
})
}
sum = sum.Add(position.FundingRates.FundingRates[i].Payment)
}
fundingData.PaymentSum = sum.String()
response.FundingData = fundingData
if includePredictedRate && !position.FundingRates.PredictedUpcomingRate.Time.IsZero() {
fundingData.UpcomingRate = &gctrpc.FundingRate{
Date: position.FundingRates.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: position.FundingRates.PredictedUpcomingRate.Rate.String(),
}
}
}
if includeOrders {
for i := range position.Orders {
od := &gctrpc.OrderDetails{
Exchange: position.Orders[i].Exchange,
Id: position.Orders[i].OrderID,
ClientOrderId: position.Orders[i].ClientOrderID,
BaseCurrency: position.Orders[i].Pair.Base.String(),
QuoteCurrency: position.Orders[i].Pair.Quote.String(),
AssetType: position.Orders[i].AssetType.String(),
OrderSide: position.Orders[i].Side.String(),
OrderType: position.Orders[i].Type.String(),
CreationTime: position.Orders[i].Date.Format(common.SimpleTimeFormatWithTimezone),
Status: position.Orders[i].Status.String(),
Price: position.Orders[i].Price,
Amount: position.Orders[i].Cost,
OpenVolume: position.Orders[i].RemainingAmount,
Fee: position.Orders[i].Fee,
Cost: position.Orders[i].Cost,
}
if !position.Orders[i].LastUpdated.IsZero() {
od.UpdateTime = position.Orders[i].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
for j := range position.Orders[i].Trades {
od.Trades = append(od.Trades, &gctrpc.TradeHistory{
CreationTime: position.Orders[i].Trades[j].Timestamp.Unix(),
Id: position.Orders[i].Trades[j].TID,
Price: position.Orders[i].Trades[j].Price,
Amount: position.Orders[i].Trades[j].Amount,
Exchange: position.Orders[i].Trades[j].Exchange,
AssetType: position.Orders[i].AssetType.String(),
OrderSide: position.Orders[i].Trades[j].Side.String(),
Fee: position.Orders[i].Trades[j].Fee,
Total: position.Orders[i].Trades[j].Total,
})
}
response.Orders = append(response.Orders, od)
}
}
return response
}
// GetManagedPosition returns an open positions from the order manager, no calling any API endpoints to return this information
func (s *RPCServer) GetManagedPosition(_ context.Context, r *gctrpc.GetManagedPositionRequest) (*gctrpc.GetManagedPositionsResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetManagedPositionRequest", common.ErrNilPointer)
}
if err := order.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil {
return nil, err
}
if r.Pair == nil {
return nil, fmt.Errorf("%w request pair", common.ErrNilPointer)
}
var (
exch exchange.IBotExchange
ai asset.Item
cp currency.Pair
err error
)
exch, err = s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
if !exch.IsEnabled() {
return nil, fmt.Errorf("%w '%v'", errExchangeDisabled, exch.GetName())
}
ai, err = asset.New(r.Asset)
if err != nil {
return nil, err
}
if !ai.IsFutures() {
return nil, fmt.Errorf("%w '%v'", order.ErrNotFuturesAsset, ai)
}
cp, err = currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
if err != nil {
return nil, err
}
err = checkParams(r.Exchange, exch, ai, cp)
if err != nil {
return nil, err
}
position, err := s.OrderManager.GetOpenFuturesPosition(r.Exchange, ai, cp)
if err != nil {
return nil, err
}
return &gctrpc.GetManagedPositionsResponse{Positions: []*gctrpc.FuturePosition{
s.buildFuturePosition(position, r.GetFundingPayments, r.IncludeFullFundingRates, r.IncludeFullOrderData, r.IncludePredictedRate),
}}, nil
}
// GetAllManagedPositions returns all open positions from the order manager, no calling any API endpoints to return this information
func (s *RPCServer) GetAllManagedPositions(_ context.Context, r *gctrpc.GetAllManagedPositionsRequest) (*gctrpc.GetManagedPositionsResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetAllManagedPositions", common.ErrNilPointer)
}
if err := order.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil {
return nil, err
}
positions, err := s.OrderManager.GetAllOpenFuturesPositions()
if err != nil {
return nil, err
}
sort.Slice(positions, func(i, j int) bool {
return positions[i].OpeningDate.Before(positions[j].OpeningDate)
})
response := make([]*gctrpc.FuturePosition, len(positions))
for i := range positions {
response[i] = s.buildFuturePosition(&positions[i], r.GetFundingPayments, r.IncludeFullFundingRates, r.IncludeFullOrderData, r.IncludePredictedRate)
}
return &gctrpc.GetManagedPositionsResponse{Positions: response}, nil
}
// GetFuturesPositions returns pnl positions for an exchange asset pair
func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuturesPositionsRequest) (*gctrpc.GetFuturesPositionsResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetFuturesPositions", common.ErrNilPointer)
}
if err := order.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil {
return nil, err
}
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
@@ -4225,25 +4389,29 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
if err != nil {
return nil, err
}
var subErr string
var subAccount string
if creds.SubAccount != "" {
subErr = "for subaccount: " + creds.SubAccount
subAccount = "for subaccount: " + creds.SubAccount
}
orders, err := exch.GetFuturesPositions(ctx, ai, cp, start, end)
if err != nil {
return nil, fmt.Errorf("%w %v", err, subErr)
}
sort.Slice(orders, func(i, j int) bool {
return orders[i].Date.Before(orders[j].Date)
positionDetails, err := exch.GetFuturesPositions(ctx, &order.PositionsRequest{
Asset: ai,
Pairs: currency.Pairs{cp},
StartDate: start,
})
if err != nil {
return nil, fmt.Errorf("%w %v", err, subAccount)
}
if len(positionDetails) != 1 {
return nil, errUnexpectedResponseSize
}
if r.Overwrite {
err = s.OrderManager.ClearFuturesTracking(r.Exchange, ai, cp)
if err != nil {
return nil, fmt.Errorf("%w %v", err, subErr)
return nil, fmt.Errorf("cannot overwrite %w %v", err, subAccount)
}
}
for i := range orders {
_, err = s.OrderManager.UpsertOrder(&orders[i])
for i := range positionDetails[0].Orders {
err = s.OrderManager.orderStore.futuresPositionController.TrackNewOrder(&positionDetails[0].Orders[i])
if err != nil {
if !errors.Is(err, order.ErrPositionClosed) {
return nil, err
@@ -4252,13 +4420,17 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
}
pos, err := s.OrderManager.GetFuturesPositionsForExchange(r.Exchange, ai, cp)
if err != nil {
return nil, fmt.Errorf("%w %v", err, subErr)
return nil, fmt.Errorf("cannot GetFuturesPositionsForExchange %w %v", err, subAccount)
}
response := &gctrpc.GetFuturesPositionsResponse{
SubAccount: creds.SubAccount,
}
var totalRealisedPNL, totalUnrealisedPNL decimal.Decimal
for i := range pos {
if r.Status != "" && pos[i].Status.String() != strings.ToUpper(r.Status) {
continue
}
if r.PositionLimit > 0 && len(response.Positions) >= int(r.PositionLimit) {
break
}
@@ -4266,18 +4438,34 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
var tick *ticker.Price
tick, err = exch.FetchTicker(ctx, pos[i].Pair, pos[i].Asset)
if err != nil {
return nil, fmt.Errorf("%w when fetching ticker data for %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair)
return nil, fmt.Errorf("%w when fetching ticker data for %v %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair, subAccount)
}
pos[i].UnrealisedPNL, err = s.OrderManager.UpdateOpenPositionUnrealisedPNL(pos[i].Exchange, pos[i].Asset, pos[i].Pair, tick.Last, tick.LastUpdated)
if err != nil {
return nil, fmt.Errorf("%w when updating unrealised PNL for %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair)
return nil, fmt.Errorf("%w when updating unrealised PNL for %v %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair, subAccount)
}
pos[i].LatestPrice = decimal.NewFromFloat(tick.Last)
}
response.TotalOrders += int64(len(pos[i].Orders))
details := &gctrpc.FuturePosition{
Status: pos[i].Status.String(),
UnrealisedPnl: pos[i].UnrealisedPNL.String(),
RealisedPnl: pos[i].RealisedPNL.String(),
Exchange: pos[i].Exchange,
Asset: pos[i].Asset.String(),
Pair: &gctrpc.CurrencyPair{
Delimiter: pos[i].Pair.Delimiter,
Base: pos[i].Pair.Base.String(),
Quote: pos[i].Pair.Quote.String(),
},
Status: pos[i].Status.String(),
OpeningDate: pos[i].OpeningDate.Format(common.SimpleTimeFormatWithTimezone),
OpeningDirection: pos[i].OpeningDirection.String(),
OpeningPrice: pos[i].OpeningPrice.String(),
OpeningSize: pos[i].OpeningSize.String(),
CurrentDirection: pos[i].LatestDirection.String(),
CurrentPrice: pos[i].LatestPrice.String(),
CurrentSize: pos[i].LatestSize.String(),
UnrealisedPnl: pos[i].UnrealisedPNL.String(),
RealisedPnl: pos[i].RealisedPNL.String(),
OrderCount: int64(len(pos[i].Orders)),
}
if !pos[i].UnrealisedPNL.IsZero() {
details.UnrealisedPnl = pos[i].UnrealisedPNL.String()
@@ -4296,7 +4484,85 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
}
totalRealisedPNL = totalRealisedPNL.Add(pos[i].RealisedPNL)
totalUnrealisedPNL = totalUnrealisedPNL.Add(pos[i].UnrealisedPNL)
if !r.Verbose {
if r.GetPositionStats {
var stats *order.PositionSummary
stats, err = exch.GetPositionSummary(ctx, &order.PositionSummaryRequest{
Asset: pos[i].Asset,
Pair: pos[i].Pair,
})
if err != nil {
return nil, fmt.Errorf("cannot GetPositionSummary %w %v", err, subAccount)
}
details.PositionStats = &gctrpc.FuturesPositionStats{
MaintenanceMarginRequirement: stats.MaintenanceMarginRequirement.String(),
InitialMarginRequirement: stats.InitialMarginRequirement.String(),
CollateralUsed: stats.CollateralUsed.String(),
MarkPrice: stats.MarkPrice.String(),
CurrentSize: stats.CurrentSize.String(),
BreakEvenPrice: stats.BreakEvenPrice.String(),
AverageOpenPrice: stats.AverageOpenPrice.String(),
RecentPnl: stats.RecentPNL.String(),
MarginFraction: stats.MarginFraction.String(),
FreeCollateral: stats.FreeCollateral.String(),
TotalCollateral: stats.TotalCollateral.String(),
}
if !stats.EstimatedLiquidationPrice.IsZero() {
details.PositionStats.EstimatedLiquidationPrice = stats.EstimatedLiquidationPrice.String()
}
}
if r.GetFundingPayments {
var endDate = time.Now()
if pos[i].Status == order.Closed {
endDate = pos[i].Orders[len(pos[i].Orders)-1].Date
}
var fundingDetails []order.FundingRates
fundingDetails, err = exch.GetFundingRates(ctx, &order.FundingRatesRequest{
Asset: pos[i].Asset,
Pairs: currency.Pairs{pos[i].Pair},
StartDate: pos[i].Orders[0].Date,
EndDate: endDate,
IncludePayments: r.GetFundingPayments,
IncludePredictedRate: r.IncludePredictedRate,
})
if err != nil {
return nil, err
}
switch {
case len(fundingDetails) == 0:
case len(fundingDetails) == 1:
var funding []*gctrpc.FundingRate
if r.IncludeFullFundingRates {
for j := range fundingDetails[0].FundingRates {
funding = append(funding, &gctrpc.FundingRate{
Date: fundingDetails[0].FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: fundingDetails[0].FundingRates[j].Rate.String(),
Payment: fundingDetails[0].FundingRates[j].Payment.String(),
})
}
}
fundingRates := &gctrpc.FundingData{
Rates: funding,
PaymentSum: fundingDetails[0].PaymentSum.String(),
}
if r.IncludeFullFundingRates {
fundingRates.LatestRate = funding[len(fundingRates.Rates)-1]
}
if r.IncludePredictedRate && !fundingDetails[0].PredictedUpcomingRate.Time.IsZero() {
fundingRates.UpcomingRate = &gctrpc.FundingRate{
Date: fundingDetails[0].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: fundingDetails[0].PredictedUpcomingRate.Rate.String(),
}
}
details.FundingData = fundingRates
err = s.OrderManager.orderStore.futuresPositionController.TrackFundingDetails(&fundingDetails[0])
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w expected 1 set of funding rates, got %d %v", errUnexpectedResponseSize, len(fundingDetails), subAccount)
}
}
if !r.IncludeFullOrderData {
response.Positions = append(response.Positions, details)
continue
}
@@ -4324,7 +4590,7 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
AssetType: pos[i].Orders[j].AssetType.String(),
OrderSide: pos[i].Orders[j].Side.String(),
OrderType: pos[i].Orders[j].Type.String(),
CreationTime: pos[i].Orders[j].Date.Unix(),
CreationTime: pos[i].Orders[j].Date.Format(common.SimpleTimeFormatWithTimezone),
Status: pos[i].Orders[j].Status.String(),
Price: pos[i].Orders[j].Price,
Amount: pos[i].Orders[j].Amount,
@@ -4333,7 +4599,7 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
Trades: trades,
}
if pos[i].Orders[j].LastUpdated.After(pos[i].Orders[j].Date) {
od.UpdateTime = pos[i].Orders[j].LastUpdated.Unix()
od.UpdateTime = pos[i].Orders[j].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
details.Orders = append(details.Orders, od)
}
@@ -4352,6 +4618,107 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
return response, nil
}
// GetFundingRates returns the funding rates for a slice of pairs of an exchange, asset
func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRatesRequest) (*gctrpc.GetFundingRatesResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetFundingRateRequest", common.ErrNilPointer)
}
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
a, err := asset.New(r.Asset)
if err != nil {
return nil, err
}
if !a.IsFutures() {
return nil, fmt.Errorf("%s %w", a, order.ErrNotFuturesAsset)
}
start := time.Now().AddDate(-1, 0, 0)
end := time.Now()
if r.StartDate != "" {
start, err = time.Parse(common.SimpleTimeFormat, r.StartDate)
if err != nil {
return nil, err
}
}
if r.EndDate != "" {
end, err = time.Parse(common.SimpleTimeFormat, r.EndDate)
if err != nil {
return nil, err
}
}
err = common.StartEndTimeCheck(start, end)
if err != nil && !errors.Is(err, common.ErrDateUnset) {
return nil, err
}
pairs, err := currency.NewPairsFromStrings(r.Pairs)
if err != nil {
return nil, err
}
for i := range pairs {
err = checkParams(r.Exchange, exch, a, pairs[i])
if err != nil {
return nil, err
}
}
funding, err := exch.GetFundingRates(ctx, &order.FundingRatesRequest{
Asset: a,
Pairs: pairs,
StartDate: start,
EndDate: end,
IncludePayments: r.IncludePayments,
IncludePredictedRate: r.IncludePredicted,
})
if err != nil {
return nil, err
}
var response gctrpc.GetFundingRatesResponse
responses := make([]*gctrpc.FundingData, len(funding))
for i := range funding {
fundingData := &gctrpc.FundingData{
Exchange: r.Exchange,
Asset: r.Asset,
Pair: &gctrpc.CurrencyPair{
Delimiter: funding[i].Pair.Delimiter,
Base: funding[i].Pair.Base.String(),
Quote: funding[i].Pair.Quote.String(),
},
StartDate: start.Format(common.SimpleTimeFormatWithTimezone),
EndDate: end.Format(common.SimpleTimeFormatWithTimezone),
LatestRate: &gctrpc.FundingRate{
Date: funding[i].LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding[i].LatestRate.Rate.String(),
},
}
var rates []*gctrpc.FundingRate
for j := range funding[i].FundingRates {
rate := &gctrpc.FundingRate{
Rate: funding[i].FundingRates[j].Rate.String(),
Date: funding[i].FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone),
}
if r.IncludePayments {
rate.Payment = funding[i].FundingRates[j].Payment.String()
}
rates = append(rates, rate)
}
if r.IncludePayments {
fundingData.PaymentSum = funding[i].PaymentSum.String()
}
fundingData.Rates = rates
if r.IncludePredicted {
fundingData.UpcomingRate = &gctrpc.FundingRate{
Date: funding[i].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding[i].PredictedUpcomingRate.Rate.String(),
}
}
responses[i] = fundingData
}
response.FundingPayments = responses
return &response, nil
}
// GetCollateral returns the total collateral for an exchange's asset
// as exchanges can scale collateral and represent it in a singular currency,
// a user can opt to include a breakdown by currency

View File

@@ -54,6 +54,90 @@ type fExchange struct {
exchange.IBotExchange
}
func (f fExchange) GetPositionSummary(context.Context, *order.PositionSummaryRequest) (*order.PositionSummary, error) {
leet := decimal.NewFromInt(1337)
return &order.PositionSummary{
MaintenanceMarginRequirement: leet,
InitialMarginRequirement: leet,
EstimatedLiquidationPrice: leet,
CollateralUsed: leet,
MarkPrice: leet,
CurrentSize: leet,
BreakEvenPrice: leet,
AverageOpenPrice: leet,
RecentPNL: leet,
MarginFraction: leet,
FreeCollateral: leet,
TotalCollateral: leet,
}, nil
}
func (f fExchange) GetFuturesPositions(ctx context.Context, req *order.PositionsRequest) ([]order.PositionDetails, error) {
id, err := uuid.NewV4()
if err != nil {
return nil, err
}
resp := make([]order.PositionDetails, len(req.Pairs))
tt := time.Now()
for i := range req.Pairs {
resp[i] = order.PositionDetails{
Exchange: f.GetName(),
Asset: req.Asset,
Pair: req.Pairs[i],
Orders: []order.Detail{
{
Exchange: f.GetName(),
Price: 1337,
Amount: 1337,
InternalOrderID: id,
OrderID: "1337",
ClientOrderID: "1337",
Type: order.Market,
Side: order.Short,
Status: order.Open,
AssetType: req.Asset,
Date: tt,
CloseTime: tt,
LastUpdated: tt,
Pair: req.Pairs[i],
},
},
}
}
return resp, nil
}
func (f fExchange) GetFundingRates(ctx context.Context, request *order.FundingRatesRequest) ([]order.FundingRates, error) {
leet := decimal.NewFromInt(1337)
return []order.FundingRates{
{
Exchange: f.GetName(),
Asset: request.Asset,
Pair: request.Pairs[0],
StartDate: request.StartDate,
EndDate: request.EndDate,
LatestRate: order.FundingRate{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
PredictedUpcomingRate: order.FundingRate{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
FundingRates: []order.FundingRate{
{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
},
PaymentSum: leet,
},
}, nil
}
func (f fExchange) GetHistoricCandles(ctx context.Context, p currency.Pair, a asset.Item, timeStart, _ time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{
Exchange: fakeExchangeName,
@@ -173,24 +257,6 @@ func (f fExchange) FetchAccountInfo(_ context.Context, a asset.Item) (account.Ho
}, nil
}
// GetFuturesPositions overrides testExchange's GetFuturesPositions function
func (f fExchange) GetFuturesPositions(_ context.Context, a asset.Item, cp currency.Pair, _, _ time.Time) ([]order.Detail, error) {
return []order.Detail{
{
Price: 1337,
Amount: 1337,
Fee: 1.337,
Exchange: f.GetName(),
OrderID: "test",
Side: order.Long,
Status: order.Open,
AssetType: a,
Date: time.Now(),
Pair: cp,
},
}, nil
}
// CalculateTotalCollateral overrides testExchange's CalculateTotalCollateral function
func (f fExchange) CalculateTotalCollateral(context.Context, *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error) {
return &order.TotalCollateralResponse{
@@ -1177,7 +1243,7 @@ func TestGetOrders(t *testing.T) {
RequestFormat: &currency.PairFormat{Uppercase: true}}
em.Add(exch)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, false, false)
om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
@@ -1284,7 +1350,7 @@ func TestGetOrder(t *testing.T) {
RequestFormat: &currency.PairFormat{Uppercase: true}}
em.Add(exch)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, false, false)
om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
@@ -1813,7 +1879,7 @@ func TestGetManagedOrders(t *testing.T) {
RequestFormat: &currency.PairFormat{Uppercase: true}}
em.Add(exch)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, false, false)
om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
@@ -2168,7 +2234,7 @@ func TestGetFuturesPositions(t *testing.T) {
}
em.Add(fakeExchange)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false)
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
@@ -2192,7 +2258,6 @@ func TestGetFuturesPositions(t *testing.T) {
Base: cp.Base.String(),
Quote: cp.Quote.String(),
},
Verbose: true,
})
if !errors.Is(err, exchange.ErrCredentialsAreEmpty) {
t.Fatalf("received '%v', expected '%v'", err, exchange.ErrCredentialsAreEmpty)
@@ -2206,17 +2271,21 @@ func TestGetFuturesPositions(t *testing.T) {
)
_, err = s.GetFuturesPositions(ctx, &gctrpc.GetFuturesPositionsRequest{
Exchange: fakeExchangeName,
Exchange: "test",
Asset: asset.Futures.String(),
Pair: &gctrpc.CurrencyPair{
Delimiter: currency.DashDelimiter,
Base: cp.Base.String(),
Quote: cp.Quote.String(),
},
Verbose: true,
IncludeFullOrderData: true,
IncludeFullFundingRates: true,
IncludePredictedRate: true,
GetPositionStats: true,
GetFundingPayments: true,
})
if !errors.Is(err, order.ErrPositionsNotLoadedForExchange) {
t.Fatalf("received '%v', expected '%v'", err, order.ErrPositionsNotLoadedForExchange)
if !errors.Is(err, ErrExchangeNotFound) {
t.Errorf("received '%v', expected '%v'", err, ErrExchangeNotFound)
}
od := &order.Detail{
@@ -2243,7 +2312,7 @@ func TestGetFuturesPositions(t *testing.T) {
Base: cp.Base.String(),
Quote: cp.Quote.String(),
},
Verbose: true,
IncludeFullOrderData: true,
})
if !errors.Is(err, nil) {
t.Fatalf("received '%v', expected '%v'", err, nil)
@@ -2257,7 +2326,6 @@ func TestGetFuturesPositions(t *testing.T) {
Base: cp.Base.String(),
Quote: cp.Quote.String(),
},
Verbose: true,
})
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNotFuturesAsset)
@@ -2793,3 +2861,340 @@ func TestGetMarginRatesHistory(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-perp")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
RequestFormat: &currency.PairFormat{Delimiter: "-"},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
om.started = 1
s := RPCServer{
Engine: &Engine{
ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{
started: 1,
iExchangeManager: em,
},
OrderManager: om,
},
}
_, err = s.GetFundingRates(context.Background(), nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received: '%v' but expected: '%v'", err, common.ErrNilPointer)
}
request := &gctrpc.GetFundingRatesRequest{
Exchange: "",
Asset: "",
Pairs: nil,
StartDate: "",
EndDate: "",
IncludePredicted: false,
IncludePayments: false,
}
_, err = s.GetFundingRates(context.Background(), request)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Errorf("received: '%v' but expected: '%v'", err, ErrExchangeNameIsEmpty)
}
request.Exchange = exch.GetName()
_, err = s.GetFundingRates(context.Background(), request)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
request.Asset = asset.Spot.String()
_, err = s.GetFundingRates(context.Background(), request)
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received: '%v' but expected: '%v'", err, order.ErrNotFuturesAsset)
}
request.Asset = asset.Futures.String()
request.Pairs = []string{cp.String()}
request.IncludePredicted = true
request.IncludePayments = true
_, err = s.GetFundingRates(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetManagedPosition(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-perp")
if err != nil {
t.Fatal(err)
}
cp2, err := currency.NewPairFromString("btc-usd")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
RequestFormat: &currency.PairFormat{Delimiter: "-"},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
Available: currency.Pairs{cp, cp2},
Enabled: currency.Pairs{cp, cp2},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp, cp2},
Enabled: currency.Pairs{cp, cp2},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
om.started = 1
s := RPCServer{
Engine: &Engine{
ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{
started: 1,
iExchangeManager: em,
},
OrderManager: om,
},
}
_, err = s.GetManagedPosition(context.Background(), nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received '%v', expected '%v'", err, common.ErrNilPointer)
}
request := &gctrpc.GetManagedPositionRequest{}
_, err = s.GetManagedPosition(context.Background(), request)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received '%v', expected '%v'", err, common.ErrNilPointer)
}
request.Pair = &gctrpc.CurrencyPair{
Delimiter: "-",
Base: "BTC",
Quote: "USD",
}
_, err = s.GetManagedPosition(context.Background(), request)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Errorf("received '%v', expected '%v'", err, ErrExchangeNameIsEmpty)
}
request.Exchange = fakeExchangeName
_, err = s.GetManagedPosition(context.Background(), request)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported)
}
request.Asset = asset.Spot.String()
_, err = s.GetManagedPosition(context.Background(), request)
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNotFuturesAsset)
}
request.Asset = asset.Futures.String()
s.OrderManager, err = SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
s.OrderManager.started = 1
s.OrderManager.activelyTrackFuturesPositions = true
_, err = s.GetManagedPosition(context.Background(), request)
if !errors.Is(err, order.ErrPositionNotFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrPositionNotFound)
}
err = s.OrderManager.orderStore.futuresPositionController.TrackNewOrder(&order.Detail{
Leverage: 1337,
Price: 1337,
Amount: 1337,
LimitPriceUpper: 1337,
LimitPriceLower: 1337,
TriggerPrice: 1337,
AverageExecutedPrice: 1337,
QuoteAmount: 1337,
ExecutedAmount: 1337,
RemainingAmount: 1337,
Cost: 1337,
Exchange: fakeExchangeName,
OrderID: "1337",
Type: order.Market,
Side: order.Buy,
Status: order.Filled,
AssetType: asset.Futures,
Date: time.Now(),
LastUpdated: time.Now(),
Pair: cp2,
Trades: []order.TradeHistory{
{
Timestamp: time.Now(),
Side: order.Buy,
},
},
})
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
_, err = s.GetManagedPosition(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}
func TestGetAllManagedPositions(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-perp")
if err != nil {
t.Fatal(err)
}
cp2, err := currency.NewPairFromString("btc-usd")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
RequestFormat: &currency.PairFormat{Delimiter: "-"},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
Available: currency.Pairs{cp, cp2},
Enabled: currency.Pairs{cp, cp2},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp, cp2},
Enabled: currency.Pairs{cp, cp2},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
var wg sync.WaitGroup
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
om.started = 1
s := RPCServer{
Engine: &Engine{
ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{
started: 1,
iExchangeManager: em,
},
OrderManager: om,
},
}
_, err = s.GetAllManagedPositions(context.Background(), nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received '%v', expected '%v'", err, common.ErrNilPointer)
}
request := &gctrpc.GetAllManagedPositionsRequest{}
s.OrderManager, err = SetupOrderManager(em, &CommunicationManager{}, &wg, false, true, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
s.OrderManager.started = 1
_, err = s.GetAllManagedPositions(context.Background(), request)
if !errors.Is(err, order.ErrNoPositionsFound) {
t.Errorf("received '%v', expected '%v'", err, order.ErrNoPositionsFound)
}
err = s.OrderManager.orderStore.futuresPositionController.TrackNewOrder(&order.Detail{
Leverage: 1337,
Price: 1337,
Amount: 1337,
LimitPriceUpper: 1337,
LimitPriceLower: 1337,
TriggerPrice: 1337,
AverageExecutedPrice: 1337,
QuoteAmount: 1337,
ExecutedAmount: 1337,
RemainingAmount: 1337,
Cost: 1337,
Exchange: fakeExchangeName,
OrderID: "1337",
Type: order.Market,
Side: order.Buy,
Status: order.Filled,
AssetType: asset.Futures,
Date: time.Now(),
LastUpdated: time.Now(),
Pair: cp2,
})
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
request.IncludePredictedRate = true
request.GetFundingPayments = true
request.IncludeFullFundingRates = true
request.IncludeFullOrderData = true
_, err = s.GetAllManagedPositions(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}

View File

@@ -131,7 +131,7 @@ func TestWebsocketRoutineManagerHandleData(t *testing.T) {
exch.SetDefaults()
em.Add(exch)
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false)
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}