package engine import ( "context" "crypto/tls" "crypto/x509" "errors" "fmt" "io" "log" "math/rand" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strconv" "strings" "sync" "testing" "time" "github.com/gofrs/uuid" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" "github.com/thrasher-corp/gocryptotrader/database/repository" dbexchange "github.com/thrasher-corp/gocryptotrader/database/repository/exchange" sqltrade "github.com/thrasher-corp/gocryptotrader/database/repository/trade" "github.com/thrasher-corp/gocryptotrader/encoding/json" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/binance" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/margin" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/gctrpc" "github.com/thrasher-corp/gocryptotrader/portfolio/banking" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" "github.com/thrasher-corp/goose" "google.golang.org/grpc/metadata" ) const ( unexpectedLackOfError = "unexpected lack of error" migrationsFolder = "migrations" databaseFolder = "database" fakeExchangeName = "fake" ) var errExpectedTestError = errors.New("expected test error") // fExchange is a fake exchange with function overrides // we're not testing an actual exchange's implemented functions type fExchange struct { exchange.IBotExchange } func (f fExchange) GetFuturesPositionSummary(context.Context, *futures.PositionSummaryRequest) (*futures.PositionSummary, error) { leet := decimal.NewFromInt(1337) return &futures.PositionSummary{ MaintenanceMarginRequirement: leet, InitialMarginRequirement: leet, EstimatedLiquidationPrice: leet, CollateralUsed: leet, MarkPrice: leet, CurrentSize: leet, AverageOpenPrice: leet, UnrealisedPNL: leet, MaintenanceMarginFraction: leet, FreeCollateral: leet, TotalCollateral: leet, }, nil } func (f fExchange) ChangePositionMargin(_ context.Context, req *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) { return &margin.PositionChangeResponse{ Exchange: f.GetName(), Pair: req.Pair, Asset: req.Asset, AllocatedMargin: req.NewAllocatedMargin, MarginType: req.MarginType, }, nil } func (f fExchange) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ float64, _ order.Side) error { return nil } func (f fExchange) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) { return 1337, nil } func (f fExchange) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error { return nil } func (f fExchange) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error { return nil } func (f fExchange) GetOpenInterest(_ context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) { if len(k) > 0 { return []futures.OpenInterest{ { Key: key.NewExchangeAssetPair(f.GetName(), k[0].Asset, k[0].Pair()), OpenInterest: 1337, }, }, nil } return nil, nil } func (f fExchange) GetCollateralMode(_ context.Context, _ asset.Item) (collateral.Mode, error) { return collateral.SingleMode, nil } func (f fExchange) GetFuturesPositionOrders(_ context.Context, req *futures.PositionsRequest) ([]futures.PositionResponse, error) { id, err := uuid.NewV4() if err != nil { return nil, err } resp := make([]futures.PositionResponse, len(req.Pairs)) tt := time.Now() for i := range req.Pairs { resp[i] = futures.PositionResponse{ 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) GetLatestFundingRates(_ context.Context, request *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { leet := decimal.NewFromInt(1337) return []fundingrate.LatestRateResponse{ { Exchange: f.GetName(), Asset: request.Asset, Pair: request.Pair, LatestRate: fundingrate.Rate{ Time: time.Now(), Rate: leet, Payment: leet, }, PredictedUpcomingRate: fundingrate.Rate{ Time: time.Now(), Rate: leet, Payment: leet, }, TimeOfNextRate: time.Now(), }, }, nil } func (f fExchange) GetHistoricalFundingRates(_ context.Context, request *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { leet := decimal.NewFromInt(1337) return &fundingrate.HistoricalRates{ Exchange: f.GetName(), Asset: request.Asset, Pair: request.Pair, StartDate: request.StartDate, EndDate: request.EndDate, LatestRate: fundingrate.Rate{ Time: request.EndDate, Rate: leet, Payment: leet, }, PredictedUpcomingRate: fundingrate.Rate{ Time: request.EndDate, Rate: leet, Payment: leet, }, FundingRates: []fundingrate.Rate{ { Time: request.EndDate, Rate: leet, Payment: leet, }, }, PaymentSum: leet, }, nil } func (f fExchange) GetHistoricCandles(_ context.Context, p currency.Pair, a asset.Item, interval kline.Interval, timeStart, _ time.Time) (*kline.Item, error) { return &kline.Item{ Exchange: fakeExchangeName, Pair: p, Asset: a, Interval: interval, Candles: []kline.Candle{ { Time: timeStart, Open: 1337, High: 1337, Low: 1337, Close: 1337, Volume: 1337, }, }, }, nil } func generateCandles(amount int, timeStart time.Time, interval kline.Interval) []kline.Candle { candy := make([]kline.Candle, amount) for x := range amount { candy[x] = kline.Candle{ Time: timeStart, Open: 1337, High: 1337, Low: 1337, Close: 1337, Volume: 1337, } timeStart = timeStart.Add(interval.Duration()) } return candy } func (f fExchange) GetHistoricCandlesExtended(_ context.Context, p currency.Pair, a asset.Item, interval kline.Interval, timeStart, _ time.Time) (*kline.Item, error) { if interval == 0 { return nil, errExpectedTestError } return &kline.Item{ Exchange: fakeExchangeName, Pair: p, Asset: a, Interval: interval, Candles: generateCandles(33, timeStart, interval), }, nil } func (f fExchange) GetCurrencyTradeURL(_ context.Context, _ asset.Item, _ currency.Pair) (string, error) { return "https://google.com", nil } func (f fExchange) GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error) { leet := decimal.NewFromInt(1337) rates := []margin.Rate{ { Time: time.Now(), MarketBorrowSize: leet, HourlyRate: leet, HourlyBorrowRate: leet, LendingPayment: margin.LendingPayment{ Payment: leet, Size: leet, }, BorrowCost: margin.BorrowCost{ Cost: leet, Size: leet, }, }, } resp := &margin.RateHistoryResponse{ Rates: rates, SumBorrowCosts: leet, AverageBorrowSize: leet, SumLendingPayments: leet, AverageLendingSize: leet, PredictedRate: rates[0], TakerFeeRate: leet, } return resp, nil } func (f fExchange) GetCachedTicker(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(), }, nil } // GetCachedAccountInfo overrides testExchange's fetch account info function // to do the bare minimum required with no API calls or credentials required func (f fExchange) GetCachedAccountInfo(_ context.Context, a asset.Item) (account.Holdings, error) { return account.Holdings{ Exchange: f.GetName(), Accounts: []account.SubAccount{ { ID: "1337", AssetType: a, Currencies: []account.Balance{ { Currency: currency.USD, Total: 1337, }, { Currency: currency.BTC, Total: 13337, }, }, }, }, }, nil } // CalculateTotalCollateral overrides testExchange's CalculateTotalCollateral function func (f fExchange) CalculateTotalCollateral(context.Context, *futures.TotalCollateralCalculator) (*futures.TotalCollateralResponse, error) { return &futures.TotalCollateralResponse{ CollateralCurrency: currency.USD, AvailableMaintenanceCollateral: decimal.NewFromInt(1338), AvailableCollateral: decimal.NewFromInt(1337), UsedBreakdown: &collateral.UsedBreakdown{ LockedInStakes: decimal.NewFromInt(3), LockedInNFTBids: decimal.NewFromInt(3), LockedInFeeVoucher: decimal.NewFromInt(3), LockedInSpotMarginFundingOffers: decimal.NewFromInt(3), LockedInSpotOrders: decimal.NewFromInt(3), LockedAsCollateral: decimal.NewFromInt(3), }, BreakdownByCurrency: []collateral.ByCurrency{ { Currency: currency.USD, TotalFunds: decimal.NewFromInt(1330), CollateralContribution: decimal.NewFromInt(1330), ScaledCurrency: currency.USD, }, { Currency: currency.DOGE, TotalFunds: decimal.NewFromInt(1000), ScaledUsed: decimal.NewFromInt(6), ScaledUsedBreakdown: &collateral.UsedBreakdown{ LockedInStakes: decimal.NewFromInt(1), LockedInNFTBids: decimal.NewFromInt(1), LockedInFeeVoucher: decimal.NewFromInt(1), LockedInSpotMarginFundingOffers: decimal.NewFromInt(1), LockedInSpotOrders: decimal.NewFromInt(1), LockedAsCollateral: decimal.NewFromInt(1), }, CollateralContribution: decimal.NewFromInt(4), ScaledCurrency: currency.USD, }, { Currency: currency.XRP, TotalFunds: decimal.NewFromInt(1333333333333337), CollateralContribution: decimal.NewFromInt(-3), ScaledCurrency: currency.USD, }, }, }, nil } // UpdateAccountInfo overrides testExchange's update account info function // to do the bare minimum required with no API calls or credentials required func (f fExchange) UpdateAccountInfo(_ context.Context, a asset.Item) (account.Holdings, error) { if a == asset.Futures { return account.Holdings{}, asset.ErrNotSupported } return account.Holdings{ Exchange: f.GetName(), Accounts: []account.SubAccount{ { ID: "1337", AssetType: a, Currencies: nil, }, }, }, nil } // GetCurrencyStateSnapshot overrides interface function func (f fExchange) GetCurrencyStateSnapshot() ([]currencystate.Snapshot, error) { return []currencystate.Snapshot{ { Code: currency.BTC, Asset: asset.Spot, }, }, nil } // CanTradePair overrides interface function func (f fExchange) CanTradePair(_ currency.Pair, _ asset.Item) error { return nil } // CanTrade overrides interface function func (f fExchange) CanTrade(_ currency.Code, _ asset.Item) error { return nil } // CanWithdraw overrides interface function func (f fExchange) CanWithdraw(_ currency.Code, _ asset.Item) error { return nil } // CanDeposit overrides interface function func (f fExchange) CanDeposit(_ currency.Code, _ asset.Item) error { return nil } // Sets up everything required to run any function inside rpcserver // Only use if you require a database, this makes tests slow func RPCTestSetup(t *testing.T) *Engine { t.Helper() var err error dbConf := database.Config{ Enabled: true, Driver: database.DBSQLite3, ConnectionDetails: drivers.ConnectionDetails{ Host: "localhost", Database: "test123.db", }, } engerino := new(Engine) dbm, err := SetupDatabaseConnectionManager(&dbConf) if err != nil { t.Fatal(err) } dbm.dbConn.DataPath = t.TempDir() engerino.DatabaseManager = dbm var wg sync.WaitGroup err = dbm.Start(&wg) if err != nil { t.Fatal(err) } t.Cleanup(func() { err = dbm.Stop() if err != nil { t.Fatal(err) } }) engerino.Config = &config.Config{} em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() cp := currency.NewBTCUSD() b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Uppercase: true}, RequestFormat: ¤cy.PairFormat{Uppercase: true}, } err = em.Add(exch) require.NoError(t, err) exch, err = em.NewExchangeByName("Binance") if err != nil { t.Fatal(err) } exch.SetDefaults() b = exch.GetBase() cp = currency.NewBTCUSDT() b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Uppercase: true}, RequestFormat: ¤cy.PairFormat{Uppercase: true}, } err = em.Add(exch) require.NoError(t, err) engerino.ExchangeManager = em engerino.Config.Database = dbConf engerino.DatabaseManager, err = SetupDatabaseConnectionManager(&engerino.Config.Database) if err != nil { log.Fatal(err) } err = engerino.DatabaseManager.Start(&engerino.ServicesWG) if err != nil { log.Fatal(err) } path := filepath.Join("..", databaseFolder, migrationsFolder) err = goose.Run("up", database.DB.SQL, repository.GetSQLDialect(), path, "") if err != nil { t.Fatalf("failed to run migrations %v", err) } uuider, err := uuid.NewV4() if err != nil { t.Fatal(err) } uuider2, err := uuid.NewV4() if err != nil { t.Fatal(err) } err = dbexchange.InsertMany([]dbexchange.Details{{Name: testExchange, UUID: uuider}, {Name: "Binance", UUID: uuider2}}) if err != nil { t.Fatalf("failed to insert exchange %v", err) } return engerino } func CleanRPCTest(t *testing.T, engerino *Engine) { t.Helper() err := engerino.DatabaseManager.Stop() if err != nil { t.Error(err) return } err = os.Remove(filepath.Join(engerino.DatabaseManager.dbConn.DataPath, engerino.DatabaseManager.cfg.Database)) if err != nil { t.Error(err) } } func TestGetSavedTrades(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} _, err := s.GetSavedTrades(t.Context(), &gctrpc.GetSavedTradesRequest{}) assert.ErrorIs(t, err, errInvalidArguments) _, err = s.GetSavedTrades(t.Context(), &gctrpc.GetSavedTradesRequest{ Exchange: fakeExchangeName, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormatWithTimezone), }) assert.ErrorIs(t, err, ErrExchangeNotFound) _, err = s.GetSavedTrades(t.Context(), &gctrpc.GetSavedTradesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormatWithTimezone), }) if err == nil { t.Error(unexpectedLackOfError) return } if err.Error() != "request for Bitstamp spot trade data between 2019-11-30 00:00:00 UTC and 2020-01-01 01:01:01 UTC and returned no results" { t.Error(err) } err = sqltrade.Insert(sqltrade.Data{ Timestamp: time.Date(2020, 0, 0, 0, 0, 1, 0, time.UTC), Exchange: testExchange, Base: currency.BTC.String(), Quote: currency.USD.String(), AssetType: asset.Spot.String(), Price: 1337, Amount: 1337, Side: order.Buy.String(), }) if err != nil { t.Error(err) return } _, err = s.GetSavedTrades(t.Context(), &gctrpc.GetSavedTradesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormatWithTimezone), }) if err != nil { t.Error(err) } } func TestConvertTradesToCandles(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} // bad param test _, err := s.ConvertTradesToCandles(t.Context(), &gctrpc.ConvertTradesToCandlesRequest{}) assert.ErrorIs(t, err, errInvalidArguments) // bad exchange test _, err = s.ConvertTradesToCandles(t.Context(), &gctrpc.ConvertTradesToCandlesRequest{ Exchange: "faker", Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), }) assert.ErrorIs(t, err, ErrExchangeNotFound) // no trades test _, err = s.ConvertTradesToCandles(t.Context(), &gctrpc.ConvertTradesToCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), }) assert.ErrorIs(t, err, errNoTrades) // add a trade err = sqltrade.Insert(sqltrade.Data{ Timestamp: time.Date(2020, 0, 0, 0, 30, 0, 0, time.UTC), Exchange: testExchange, Base: currency.BTC.String(), Quote: currency.USD.String(), AssetType: asset.Spot.String(), Price: 1337, Amount: 1337, Side: order.Buy.String(), }) if err != nil { t.Fatal(err) } // get candle from one trade var candles *gctrpc.GetHistoricCandlesResponse candles, err = s.ConvertTradesToCandles(t.Context(), &gctrpc.ConvertTradesToCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), }) if err != nil { t.Error(err) } if len(candles.Candle) == 0 { t.Error("no candles returned") } // save generated candle to database _, err = s.ConvertTradesToCandles(t.Context(), &gctrpc.ConvertTradesToCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), Sync: true, }) if err != nil { t.Error(err) } // forcefully remove previous candle and insert a new one _, err = s.ConvertTradesToCandles(t.Context(), &gctrpc.ConvertTradesToCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), Sync: true, Force: true, }) if err != nil { t.Error(err) } // load the saved candle to verify that it was overwritten candles, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), UseDb: true, }) if err != nil { t.Error(err) } if len(candles.Candle) != 1 { t.Error("expected only one candle") } } func TestGetHistoricCandles(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} // error checks defaultStart := time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC) defaultEnd := time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC) cp := currency.NewBTCUSD() _, err := s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: "", Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), AssetType: asset.Spot.String(), }) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) _, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: "bruh", Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), AssetType: asset.Spot.String(), }) assert.ErrorIs(t, err, ErrExchangeNotFound) _, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), Pair: nil, AssetType: asset.Spot.String(), }) assert.ErrorIs(t, err, errCurrencyPairUnset) _, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Base: currency.BTC.String(), Quote: currency.USD.String(), }, Start: "2020-01-02 15:04:05 UTC", End: "2020-01-02 15:04:05 UTC", }) assert.ErrorIs(t, err, common.ErrStartEqualsEnd) var results *gctrpc.GetHistoricCandlesResponse // default run results, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), AssetType: asset.Spot.String(), TimeInterval: int64(kline.OneHour.Duration()), }) require.NoError(t, err) if len(results.Candle) == 0 { t.Error("expected results") } // sync run results, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, AssetType: asset.Spot.String(), Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), Sync: true, ExRequest: true, }) require.NoError(t, err) if len(results.Candle) == 0 { t.Error("expected results") } // db run results, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, AssetType: asset.Spot.String(), Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), UseDb: true, }) require.NoError(t, err) if len(results.Candle) == 0 { t.Error("expected results") } err = trade.SaveTradesToDatabase(trade.Data{ TID: "test123", Exchange: testExchange, CurrencyPair: cp, AssetType: asset.Spot, Price: 1337, Amount: 1337, Side: order.Buy, Timestamp: time.Date(2020, 0, 0, 2, 0, 0, 0, time.UTC), }) if err != nil { t.Error(err) return } // db run including trades results, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, AssetType: asset.Spot.String(), Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 3, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), TimeInterval: int64(kline.OneHour.Duration()), UseDb: true, FillMissingWithTrades: true, }) require.NoError(t, err) if results.Candle[len(results.Candle)-1].Close != 1337 { t.Error("expected fancy new candle based off fancy new trade data") } } func TestFindMissingSavedTradeIntervals(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} // bad request checks _, err := s.FindMissingSavedTradeIntervals(t.Context(), &gctrpc.FindMissingTradePeriodsRequest{}) if err == nil { t.Error("expected error") return } require.ErrorIs(t, err, errInvalidArguments) cp := currency.NewBTCUSD() // no data found response defaultStart := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).UTC() defaultEnd := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).UTC() var resp *gctrpc.FindMissingIntervalsResponse resp, err = s.FindMissingSavedTradeIntervals(t.Context(), &gctrpc.FindMissingTradePeriodsRequest{ ExchangeName: testExchange, AssetType: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Start: defaultStart.UTC().Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.UTC().Format(common.SimpleTimeFormatWithTimezone), }) if err != nil { t.Error(err) } if resp.Status == "" { t.Errorf("expected a status message") } // one trade response err = trade.SaveTradesToDatabase(trade.Data{ TID: "test1234", Exchange: testExchange, CurrencyPair: cp, AssetType: asset.Spot, Price: 1337, Amount: 1337, Side: order.Buy, Timestamp: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC), }) if err != nil { t.Error(err) return } resp, err = s.FindMissingSavedTradeIntervals(t.Context(), &gctrpc.FindMissingTradePeriodsRequest{ ExchangeName: testExchange, AssetType: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Start: defaultStart.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), }) if err != nil { t.Error(err) } if len(resp.MissingPeriods) != 2 { t.Errorf("expected 2 missing period, received: %v", len(resp.MissingPeriods)) } // two trades response err = trade.SaveTradesToDatabase(trade.Data{ TID: "test123", Exchange: testExchange, CurrencyPair: cp, AssetType: asset.Spot, Price: 1337, Amount: 1337, Side: order.Buy, Timestamp: time.Date(2020, 1, 1, 13, 0, 0, 0, time.UTC), }) if err != nil { t.Error(err) return } resp, err = s.FindMissingSavedTradeIntervals(t.Context(), &gctrpc.FindMissingTradePeriodsRequest{ ExchangeName: testExchange, AssetType: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Start: defaultStart.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), }) if err != nil { t.Error(err) } if len(resp.MissingPeriods) != 2 { t.Errorf("expected 2 missing periods, received: %v", len(resp.MissingPeriods)) } } func TestFindMissingSavedCandleIntervals(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} // bad request checks _, err := s.FindMissingSavedCandleIntervals(t.Context(), &gctrpc.FindMissingCandlePeriodsRequest{}) if err == nil { t.Error("expected error") return } require.ErrorIs(t, err, errInvalidArguments) cp := currency.NewBTCUSD() // no data found response defaultStart := time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC) defaultEnd := time.Date(2020, 0, 0, 4, 0, 0, 0, time.UTC) var resp *gctrpc.FindMissingIntervalsResponse _, err = s.FindMissingSavedCandleIntervals(t.Context(), &gctrpc.FindMissingCandlePeriodsRequest{ ExchangeName: testExchange, AssetType: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Interval: int64(time.Hour), Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), }) if err != nil && err.Error() != "no candle data found: Bitstamp BTC USD 3600 spot" { t.Error(err) return } // one candle missing periods response _, err = kline.StoreInDatabase(&kline.Item{ Exchange: testExchange, Pair: cp, Asset: asset.Spot, Interval: kline.OneHour, Candles: []kline.Candle{ { Time: time.Date(2020, 0, 0, 0, 30, 0, 0, time.UTC), Open: 1337, High: 1337, Low: 1337, Close: 1337, Volume: 1337, }, }, }, false) if err != nil { t.Error(err) return } _, err = s.FindMissingSavedCandleIntervals(t.Context(), &gctrpc.FindMissingCandlePeriodsRequest{ ExchangeName: testExchange, AssetType: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Interval: int64(time.Hour), Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), }) if err != nil { t.Error(err) } // two candle missing periods response _, err = kline.StoreInDatabase(&kline.Item{ Exchange: testExchange, Pair: cp, Asset: asset.Spot, Interval: kline.OneHour, Candles: []kline.Candle{ { Time: time.Date(2020, 0, 0, 2, 45, 0, 0, time.UTC), Open: 1337, High: 1337, Low: 1337, Close: 1337, Volume: 1337, }, }, }, false) if err != nil { t.Error(err) return } resp, err = s.FindMissingSavedCandleIntervals(t.Context(), &gctrpc.FindMissingCandlePeriodsRequest{ ExchangeName: testExchange, AssetType: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Base: cp.Base.String(), Quote: cp.Quote.String(), }, Interval: int64(time.Hour), Start: defaultStart.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), }) if err != nil { t.Error(err) } if len(resp.MissingPeriods) != 2 { t.Errorf("expected 2 missing periods, received: %v", len(resp.MissingPeriods)) } } func TestSetExchangeTradeProcessing(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() b.Config = &config.Exchange{ Features: &config.FeaturesConfig{Enabled: config.FeaturesEnabledConfig{SaveTradeData: false}}, } err = em.Add(exch) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.SetExchangeTradeProcessing(t.Context(), &gctrpc.SetExchangeTradeProcessingRequest{Exchange: testExchange, Status: true}) if err != nil { t.Error(err) return } if !b.IsSaveTradeDataEnabled() { t.Error("expected true") } _, err = s.SetExchangeTradeProcessing(t.Context(), &gctrpc.SetExchangeTradeProcessingRequest{Exchange: testExchange, Status: false}) if err != nil { t.Error(err) return } if b.IsSaveTradeDataEnabled() { t.Error("expected false") } } func TestGetRecentTrades(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} _, err := s.GetRecentTrades(t.Context(), &gctrpc.GetSavedTradesRequest{}) assert.ErrorIs(t, err, errInvalidArguments) _, err = s.GetRecentTrades(t.Context(), &gctrpc.GetSavedTradesRequest{ Exchange: fakeExchangeName, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), }) assert.ErrorIs(t, err, ErrExchangeNotFound) _, err = s.GetRecentTrades(t.Context(), &gctrpc.GetSavedTradesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), }) if err != nil { t.Error(err) } } // dummyServer implements a basic RPC server interface for deployment in a test // when streaming occurs, so we can deliver a context value. type dummyServer struct{} func (d *dummyServer) Send(*gctrpc.SavedTradesResponse) error { return nil } func (d *dummyServer) SetHeader(metadata.MD) error { return nil } func (d *dummyServer) SendHeader(metadata.MD) error { return nil } func (d *dummyServer) SetTrailer(metadata.MD) {} func (d *dummyServer) Context() context.Context { return context.Background() } func (d *dummyServer) SendMsg(_ any) error { return nil } func (d *dummyServer) RecvMsg(_ any) error { return nil } func TestGetHistoricTrades(t *testing.T) { engerino := RPCTestSetup(t) defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} err := s.GetHistoricTrades(&gctrpc.GetSavedTradesRequest{}, nil) assert.ErrorIs(t, err, errInvalidArguments) err = s.GetHistoricTrades(&gctrpc.GetSavedTradesRequest{ Exchange: fakeExchangeName, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), }, nil) assert.ErrorIs(t, err, ErrExchangeNotFound) err = s.GetHistoricTrades(&gctrpc.GetSavedTradesRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: currency.BTC.String(), Quote: currency.USD.String(), }, AssetType: asset.Spot.String(), Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), End: time.Date(2020, 0, 0, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormatWithTimezone), }, &dummyServer{}) if err != common.ErrFunctionNotSupported { t.Error(err) } } func TestGetAccountInfo(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.GetAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) assert.NoError(t, err) } func TestUpdateAccountInfo(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.GetAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) assert.NoError(t, err) _, err = s.UpdateAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Futures.String()}) assert.ErrorIs(t, err, currency.ErrAssetNotFound) _, err = s.UpdateAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{ Exchange: fakeExchangeName, AssetType: asset.Spot.String(), }) assert.NoError(t, err) } func TestGetOrders(t *testing.T) { t.Parallel() exchName := "Binance" engerino := &Engine{} em := NewExchangeManager() exch, err := em.NewExchangeByName(exchName) if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() cp := currency.NewBTCUSDT() b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Uppercase: true}, RequestFormat: ¤cy.PairFormat{Uppercase: true}, } err = em.Add(exch) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, &config.OrderManager{}) assert.NoError(t, err) om.started = 1 s := RPCServer{Engine: &Engine{ExchangeManager: em, OrderManager: om}} p := &gctrpc.CurrencyPair{ Delimiter: "-", Base: currency.BTC.String(), Quote: currency.USDT.String(), } _, err = s.GetOrders(t.Context(), nil) assert.ErrorIs(t, err, errInvalidArguments) _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ AssetType: asset.Spot.String(), Pair: p, }) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: "bruh", AssetType: asset.Spot.String(), Pair: p, }) assert.ErrorIs(t, err, ErrExchangeNotFound) _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, AssetType: asset.Spot.String(), }) assert.ErrorIs(t, err, errCurrencyPairUnset) _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, Pair: p, }) assert.ErrorIs(t, err, asset.ErrNotSupported) _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, AssetType: asset.Spot.String(), Pair: p, StartDate: time.Now().UTC().Add(time.Second).Format(common.SimpleTimeFormatWithTimezone), EndDate: time.Now().UTC().Add(-time.Hour).Format(common.SimpleTimeFormatWithTimezone), }) assert.ErrorIs(t, err, common.ErrStartAfterEnd) _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, AssetType: asset.Spot.String(), Pair: p, StartDate: time.Now().UTC().Add(-time.Hour).Format(common.SimpleTimeFormatWithTimezone), EndDate: time.Now().UTC().Add(time.Hour).Format(common.SimpleTimeFormatWithTimezone), }) assert.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) b.SetCredentials("test", "test", "", "", "", "") b.API.AuthenticatedSupport = true _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, AssetType: asset.Spot.String(), Pair: p, }) if err == nil { t.Error("expected error") } } func TestGetOrder(t *testing.T) { t.Parallel() exchName := "Binance" engerino := &Engine{} em := NewExchangeManager() exch, err := em.NewExchangeByName(exchName) if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() cp := currency.NewBTCUSDT() b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Uppercase: true}, RequestFormat: ¤cy.PairFormat{Uppercase: true}, } err = em.Add(exch) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, &config.OrderManager{}) assert.NoError(t, err) om.started = 1 assert.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em, OrderManager: om}} p := &gctrpc.CurrencyPair{ Delimiter: "-", Base: "BTC", Quote: "USDT", } _, err = s.GetOrder(t.Context(), nil) assert.ErrorIs(t, err, errInvalidArguments) _, err = s.GetOrder(t.Context(), &gctrpc.GetOrderRequest{ Exchange: "test123", OrderId: "", Pair: p, Asset: "spot", }) assert.ErrorIs(t, err, ErrExchangeNotFound) _, err = s.GetOrder(t.Context(), &gctrpc.GetOrderRequest{ Exchange: exchName, OrderId: "", Pair: nil, Asset: "", }) assert.ErrorIs(t, err, errCurrencyPairUnset) _, err = s.GetOrder(t.Context(), &gctrpc.GetOrderRequest{ Exchange: exchName, OrderId: "", Pair: p, Asset: "", }) assert.ErrorIs(t, err, asset.ErrNotSupported) _, err = s.GetOrder(t.Context(), &gctrpc.GetOrderRequest{ Exchange: exchName, OrderId: "", Pair: p, Asset: asset.Spot.String(), }) assert.ErrorIs(t, err, ErrOrderIDCannotBeEmpty) _, err = s.GetOrder(t.Context(), &gctrpc.GetOrderRequest{ Exchange: exchName, OrderId: "1234", Pair: p, Asset: asset.Spot.String(), }) assert.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) } func TestCheckVars(t *testing.T) { t.Parallel() var e exchange.IBotExchange err := checkParams("Binance", e, asset.Spot, currency.NewBTCUSDT()) assert.ErrorIs(t, err, errExchangeNotLoaded, "checkParams should error correctly") e = &binance.Exchange{} err = checkParams("Binance", e, asset.Spot, currency.NewBTCUSDT()) assert.ErrorIs(t, err, errExchangeNotEnabled, "checkParams should error correctly") e.SetEnabled(true) err = checkParams("Binance", e, asset.Spot, currency.NewBTCUSDT()) assert.ErrorIs(t, err, currency.ErrPairManagerNotInitialised, "checkParams should error correctly") b := e.GetBase() for _, a := range []asset.Item{asset.Spot, asset.Margin, asset.CoinMarginedFutures, asset.USDTMarginedFutures} { ps := currency.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Uppercase: true}, ConfigFormat: ¤cy.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}, } switch a { case asset.CoinMarginedFutures: ps.RequestFormat = ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter} ps.ConfigFormat = ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter} case asset.USDTMarginedFutures: ps.ConfigFormat = ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} } require.NoError(t, b.SetAssetPairStore(a, ps), "SetAssetPairStore must not error") } err = checkParams("Binance", e, asset.Spot, currency.NewBTCUSDT()) assert.ErrorIs(t, err, errCurrencyPairInvalid, "checkParams should error correctly") data := []currency.Pair{ {Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.USDT}, } err = b.CurrencyPairs.StorePairs(asset.Spot, data, false) require.NoError(t, err, "StorePairs must not error") err = checkParams("Binance", e, asset.Spot, currency.NewBTCUSDT()) require.ErrorIs(t, err, errCurrencyNotEnabled, "checkParams must error correctly") err = b.CurrencyPairs.EnablePair(asset.Spot, currency.Pair{Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.USDT}) require.NoError(t, err, "EnablePair must not error") err = checkParams("Binance", e, asset.Spot, currency.NewBTCUSDT()) require.NoError(t, err, "checkParams must not error") } func TestParseEvents(t *testing.T) { t.Parallel() exchangeName := "Binance" testData := make([]*withdraw.Response, 5) for x := range 5 { test := fmt.Sprintf("test-%v", x) resp := &withdraw.Response{ ID: withdraw.DryRunID, Exchange: withdraw.ExchangeResponse{ Name: test, ID: test, Status: test, }, RequestDetails: withdraw.Request{ Exchange: test, Description: test, Amount: 1.0, }, } if x%2 == 0 { resp.RequestDetails.Currency = currency.AUD resp.RequestDetails.Type = 1 resp.RequestDetails.Fiat = withdraw.FiatRequest{ Bank: banking.Account{ Enabled: false, ID: fmt.Sprintf("test-%v", x), BankName: fmt.Sprintf("test-%v-bank", x), AccountName: "hello", AccountNumber: fmt.Sprintf("test-%v", x), BSBNumber: "123456", SupportedCurrencies: "BTC-AUD", SupportedExchanges: exchangeName, }, } } else { resp.RequestDetails.Currency = currency.BTC resp.RequestDetails.Type = 0 resp.RequestDetails.Crypto.Address = test resp.RequestDetails.Crypto.FeeAmount = 0 resp.RequestDetails.Crypto.AddressTag = test } testData[x] = resp } v := parseMultipleEvents(testData) if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" { t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse") } if len(testData) < 2 { t.Fatal("expected at least 2") } v = parseSingleEvents(testData[0]) if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" { t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse") } v = parseSingleEvents(testData[1]) if v.Event[0].Request.Type != 0 { t.Fatal("Expected second entry in slice to return a Request.Type of Crypto") } } func TestRPCServerUpsertDataHistoryJob(t *testing.T) { t.Parallel() m, _ := createDHM(t) em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() cp := currency.NewBTCUSD() b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, AssetEnabled: true, } err = em.Add(exch) require.NoError(t, err) s := RPCServer{Engine: &Engine{dataHistoryManager: m, ExchangeManager: em}} _, err = s.UpsertDataHistoryJob(t.Context(), nil) assert.ErrorIs(t, err, errNilRequestData) _, err = s.UpsertDataHistoryJob(t.Context(), &gctrpc.UpsertDataHistoryJobRequest{}) assert.ErrorIs(t, err, asset.ErrNotSupported) job := &gctrpc.UpsertDataHistoryJobRequest{ Nickname: "hellomoto", Exchange: testExchange, Asset: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: "-", Base: "BTC", Quote: "USD", }, StartDate: time.Now().Add(-time.Hour * 24).Format(common.SimpleTimeFormatWithTimezone), EndDate: time.Now().Format(common.SimpleTimeFormatWithTimezone), Interval: int64(kline.OneHour.Duration()), RequestSizeLimit: 10, DataType: int64(dataHistoryCandleDataType), MaxRetryAttempts: 3, BatchSize: 500, } _, err = s.UpsertDataHistoryJob(t.Context(), job) assert.NoError(t, err) } func TestGetDataHistoryJobDetails(t *testing.T) { t.Parallel() m, _ := createDHM(t) s := RPCServer{Engine: &Engine{dataHistoryManager: m}} dhj := &DataHistoryJob{ Nickname: "TestGetDataHistoryJobDetails", Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewBTCUSD(), StartDate: time.Now().UTC().Add(-time.Minute * 2), EndDate: time.Now().UTC(), Interval: kline.OneMin, } err := m.UpsertJob(dhj, false) assert.NoError(t, err) _, err = s.GetDataHistoryJobDetails(t.Context(), nil) assert.ErrorIs(t, err, errNilRequestData) _, err = s.GetDataHistoryJobDetails(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{}) assert.ErrorIs(t, err, errNicknameIDUnset) _, err = s.GetDataHistoryJobDetails(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{Id: "123", Nickname: "123"}) assert.ErrorIs(t, err, errOnlyNicknameOrID) _, err = s.GetDataHistoryJobDetails(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{Nickname: "TestGetDataHistoryJobDetails"}) assert.NoError(t, err) _, err = s.GetDataHistoryJobDetails(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{Id: dhj.ID.String()}) assert.NoError(t, err) resp, err := s.GetDataHistoryJobDetails(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{Nickname: "TestGetDataHistoryJobDetails", FullDetails: true}) require.NoError(t, err) if resp == nil { //nolint:staticcheck,nolintlint // SA5011 Ignore the nil warnings t.Fatal("expected job") } if !strings.EqualFold(resp.Nickname, "TestGetDataHistoryJobDetails") { //nolint:nolintlint,staticcheck // SA5011 Ignore the nil warnings t.Errorf("received %v, expected %v", resp.Nickname, "TestGetDataHistoryJobDetails") } } func TestSetDataHistoryJobStatus(t *testing.T) { t.Parallel() m, j := createDHM(t) s := RPCServer{Engine: &Engine{dataHistoryManager: m}} dhj := &DataHistoryJob{ Nickname: "TestDeleteDataHistoryJob", Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewBTCUSD(), StartDate: time.Now().UTC().Add(-time.Minute * 2), EndDate: time.Now().UTC(), Interval: kline.OneMin, } err := m.UpsertJob(dhj, false) require.NoError(t, err) _, err = s.SetDataHistoryJobStatus(t.Context(), nil) assert.ErrorIs(t, err, errNilRequestData) _, err = s.SetDataHistoryJobStatus(t.Context(), &gctrpc.SetDataHistoryJobStatusRequest{}) assert.ErrorIs(t, err, errNicknameIDUnset) _, err = s.SetDataHistoryJobStatus(t.Context(), &gctrpc.SetDataHistoryJobStatusRequest{Id: "123", Nickname: "123"}) assert.ErrorIs(t, err, errOnlyNicknameOrID) id := dhj.ID _, err = s.SetDataHistoryJobStatus(t.Context(), &gctrpc.SetDataHistoryJobStatusRequest{Nickname: "TestDeleteDataHistoryJob", Status: int64(dataHistoryStatusRemoved)}) assert.NoError(t, err) dhj.ID = id j.Status = int64(dataHistoryStatusActive) _, err = s.SetDataHistoryJobStatus(t.Context(), &gctrpc.SetDataHistoryJobStatusRequest{Id: id.String(), Status: int64(dataHistoryStatusRemoved)}) assert.NoError(t, err) _, err = s.SetDataHistoryJobStatus(t.Context(), &gctrpc.SetDataHistoryJobStatusRequest{Id: id.String(), Status: int64(dataHistoryStatusActive)}) assert.ErrorIs(t, err, errBadStatus) j.Status = int64(dataHistoryStatusActive) _, err = s.SetDataHistoryJobStatus(t.Context(), &gctrpc.SetDataHistoryJobStatusRequest{Id: id.String(), Status: int64(dataHistoryStatusPaused)}) assert.NoError(t, err) if j.Status != int64(dataHistoryStatusPaused) { t.Errorf("received %v, expected %v", dataHistoryStatus(j.Status), dataHistoryStatusPaused) } } func TestGetActiveDataHistoryJobs(t *testing.T) { t.Parallel() m, _ := createDHM(t) s := RPCServer{Engine: &Engine{dataHistoryManager: m}} dhj := &DataHistoryJob{ Nickname: "TestGetActiveDataHistoryJobs", Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewBTCUSD(), StartDate: time.Now().UTC().Add(-time.Minute * 2), EndDate: time.Now().UTC(), Interval: kline.OneMin, } require.NoError(t, m.UpsertJob(dhj, false)) r, err := s.GetActiveDataHistoryJobs(t.Context(), nil) require.NoError(t, err) if len(r.Results) != 1 { t.Fatalf("received %v, expected %v", len(r.Results), 1) } } func TestGetDataHistoryJobsBetween(t *testing.T) { t.Parallel() m, _ := createDHM(t) s := RPCServer{Engine: &Engine{dataHistoryManager: m}} dhj := &DataHistoryJob{ Nickname: "GetDataHistoryJobsBetween", Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewBTCUSD(), StartDate: time.Now().UTC().Add(-time.Minute * 2), EndDate: time.Now().UTC(), Interval: kline.OneMin, } _, err := s.GetDataHistoryJobsBetween(t.Context(), nil) require.ErrorIs(t, err, errNilRequestData) _, err = s.GetDataHistoryJobsBetween(t.Context(), &gctrpc.GetDataHistoryJobsBetweenRequest{ StartDate: time.Now().UTC().Add(time.Minute).Format(common.SimpleTimeFormatWithTimezone), EndDate: time.Now().UTC().Format(common.SimpleTimeFormatWithTimezone), }) require.ErrorIs(t, err, common.ErrStartAfterEnd) err = m.UpsertJob(dhj, false) require.NoError(t, err) r, err := s.GetDataHistoryJobsBetween(t.Context(), &gctrpc.GetDataHistoryJobsBetweenRequest{ StartDate: time.Now().Add(-time.Minute).UTC().Format(common.SimpleTimeFormatWithTimezone), EndDate: time.Now().Add(time.Minute).UTC().Format(common.SimpleTimeFormatWithTimezone), }) assert.NoError(t, err) if len(r.Results) != 1 { t.Errorf("received %v, expected %v", len(r.Results), 1) } } func TestGetDataHistoryJobSummary(t *testing.T) { t.Parallel() m, _ := createDHM(t) s := RPCServer{Engine: &Engine{dataHistoryManager: m}} dhj := &DataHistoryJob{ Nickname: "TestGetDataHistoryJobSummary", Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewBTCUSD(), StartDate: time.Now().UTC().Add(-time.Minute * 2), EndDate: time.Now().UTC(), Interval: kline.OneMin, } assert.NoError(t, m.UpsertJob(dhj, false), "UpsertJob should not error") _, err := s.GetDataHistoryJobSummary(t.Context(), nil) assert.ErrorIs(t, err, errNilRequestData) _, err = s.GetDataHistoryJobSummary(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{}) assert.ErrorIs(t, err, errNicknameUnset) resp, err := s.GetDataHistoryJobSummary(t.Context(), &gctrpc.GetDataHistoryJobDetailsRequest{Nickname: "TestGetDataHistoryJobSummary"}) assert.NoError(t, err, "GetDataHistoryJobSummary should not error") require.NotNil(t, resp) assert.NotEmpty(t, resp.Nickname) assert.NotEmpty(t, resp.ResultSummaries) } func TestGetManagedOrders(t *testing.T) { exchName := "Binance" engerino := &Engine{} em := NewExchangeManager() exch, err := em.NewExchangeByName(exchName) if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() cp := currency.NewBTCUSDT() b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Uppercase: true}, RequestFormat: ¤cy.PairFormat{Uppercase: true}, } err = em.Add(exch) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, engerino.CommunicationsManager, &wg, &config.OrderManager{}) assert.NoError(t, err) om.started = 1 s := RPCServer{Engine: &Engine{ExchangeManager: em, OrderManager: om}} p := &gctrpc.CurrencyPair{ Delimiter: "-", Base: currency.BTC.String(), Quote: currency.USDT.String(), } _, err = s.GetManagedOrders(t.Context(), nil) assert.ErrorIs(t, err, errInvalidArguments) _, err = s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{ AssetType: asset.Spot.String(), Pair: p, }) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) _, err = s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: "bruh", AssetType: asset.Spot.String(), Pair: p, }) assert.ErrorIs(t, err, ErrExchangeNotFound) _, err = s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, AssetType: asset.Spot.String(), }) assert.ErrorIs(t, err, errCurrencyPairUnset) _, err = s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, Pair: p, }) assert.ErrorIs(t, err, asset.ErrNotSupported) o := order.Detail{ Price: 100000, Amount: 0.002, Exchange: "Binance", Type: order.Limit, Side: order.Sell, Status: order.New, AssetType: asset.Spot, Pair: currency.NewBTCUSDT(), } err = om.Add(&o) if err != nil { t.Errorf("Error: %v", err) } oo, err := s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{ Exchange: exchName, AssetType: "spot", Pair: p, }) if err != nil { t.Errorf("non expected Error: %v", err) } else if oo == nil || len(oo.GetOrders()) != 1 { t.Errorf("unexpected order result: %v", oo) } } func TestRPCServer_unixTimestamp(t *testing.T) { t.Parallel() s := RPCServer{ Engine: &Engine{ Config: &config.Config{ RemoteControl: config.RemoteControlConfig{ GRPC: config.GRPCConfig{ TimeInNanoSeconds: false, }, }, }, }, } const sec = 1618888141 const nsec = 2 x := time.Unix(sec, nsec) timestampSeconds := s.unixTimestamp(x) if timestampSeconds != sec { t.Errorf("have %d, want %d", timestampSeconds, sec) } s.Config.RemoteControl.GRPC.TimeInNanoSeconds = true timestampNanos := s.unixTimestamp(x) if want := int64(sec*1_000_000_000 + nsec); timestampNanos != want { t.Errorf("have %d, want %d", timestampSeconds, want) } } func TestRPCServer_GetTicker_LastUpdatedNanos(t *testing.T) { t.Parallel() // Make a dummy pair we'll be using for this test. pair := currency.NewPairWithDelimiter("XXXXX", "YYYYY", "") // Create a mock-up RPCServer and add our newly made pair to its list of // available and enabled pairs. server := RPCServer{Engine: RPCTestSetup(t)} exch, err := server.GetExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.CurrencyPairs.Pairs[asset.Spot].Available = append( b.CurrencyPairs.Pairs[asset.Spot].Available, pair, ) b.CurrencyPairs.Pairs[asset.Spot].Enabled = append( b.CurrencyPairs.Pairs[asset.Spot].Enabled, pair, ) // Push a mock-up ticker. now := time.Now() err = ticker.ProcessTicker(&ticker.Price{ ExchangeName: testExchange, Pair: pair, AssetType: asset.Spot, LastUpdated: now, Open: 69, High: 96, Low: 169, Close: 196, }) if err != nil { t.Fatal(err) } // Prepare a ticker request. request := &gctrpc.GetTickerRequest{ Exchange: testExchange, Pair: &gctrpc.CurrencyPair{ Delimiter: pair.Delimiter, Base: pair.Base.String(), Quote: pair.Quote.String(), }, AssetType: asset.Spot.String(), } // Check if timestamp returned is in seconds if !TimeInNanoSeconds. server.Config.RemoteControl.GRPC.TimeInNanoSeconds = false one, err := server.GetTicker(t.Context(), request) if err != nil { t.Error(err) } if want := now.Unix(); one.LastUpdated != want { t.Errorf("have %d, want %d", one.LastUpdated, want) } // Check if timestamp returned is in nanoseconds if TimeInNanoSeconds. server.Config.RemoteControl.GRPC.TimeInNanoSeconds = true two, err := server.GetTicker(t.Context(), request) if err != nil { t.Error(err) } if want := now.UnixNano(); two.LastUpdated != want { t.Errorf("have %d, want %d", two.LastUpdated, want) } } func TestUpdateDataHistoryJobPrerequisite(t *testing.T) { t.Parallel() m, _ := createDHM(t) s := RPCServer{Engine: &Engine{dataHistoryManager: m}} _, err := s.UpdateDataHistoryJobPrerequisite(t.Context(), nil) assert.ErrorIs(t, err, errNilRequestData) _, err = s.UpdateDataHistoryJobPrerequisite(t.Context(), &gctrpc.UpdateDataHistoryJobPrerequisiteRequest{}) assert.ErrorIs(t, err, errNicknameUnset) _, err = s.UpdateDataHistoryJobPrerequisite(t.Context(), &gctrpc.UpdateDataHistoryJobPrerequisiteRequest{ Nickname: "test456", }) assert.NoError(t, err) _, err = s.UpdateDataHistoryJobPrerequisite(t.Context(), &gctrpc.UpdateDataHistoryJobPrerequisiteRequest{ Nickname: "test456", PrerequisiteJobNickname: "test123", }) assert.NoError(t, err) } func TestCurrencyStateGetAll(t *testing.T) { t.Parallel() _, err := (&RPCServer{Engine: &Engine{}}).CurrencyStateGetAll(t.Context(), &gctrpc.CurrencyStateGetAllRequest{Exchange: fakeExchangeName}) assert.ErrorIs(t, err, ErrSubSystemNotStarted) } func TestCurrencyStateWithdraw(t *testing.T) { t.Parallel() _, err := (&RPCServer{ Engine: &Engine{}, }).CurrencyStateWithdraw(t.Context(), &gctrpc.CurrencyStateWithdrawRequest{ Exchange: "wow", Asset: "meow", }) require.ErrorIs(t, err, asset.ErrNotSupported) _, err = (&RPCServer{ Engine: &Engine{}, }).CurrencyStateWithdraw(t.Context(), &gctrpc.CurrencyStateWithdrawRequest{ Exchange: "wow", Asset: "spot", }) require.ErrorIs(t, err, ErrSubSystemNotStarted) } func TestCurrencyStateDeposit(t *testing.T) { t.Parallel() _, err := (&RPCServer{ Engine: &Engine{}, }).CurrencyStateDeposit(t.Context(), &gctrpc.CurrencyStateDepositRequest{Exchange: "wow", Asset: "meow"}) require.ErrorIs(t, err, asset.ErrNotSupported) _, err = (&RPCServer{ Engine: &Engine{}, }).CurrencyStateDeposit(t.Context(), &gctrpc.CurrencyStateDepositRequest{Exchange: "wow", Asset: "spot"}) require.ErrorIs(t, err, ErrSubSystemNotStarted) } func TestCurrencyStateTrading(t *testing.T) { t.Parallel() _, err := (&RPCServer{ Engine: &Engine{}, }).CurrencyStateTrading(t.Context(), &gctrpc.CurrencyStateTradingRequest{Exchange: "wow", Asset: "meow"}) require.ErrorIs(t, err, asset.ErrNotSupported) _, err = (&RPCServer{ Engine: &Engine{}, }).CurrencyStateTrading(t.Context(), &gctrpc.CurrencyStateTradingRequest{Exchange: "wow", Asset: "spot"}) require.ErrorIs(t, err, ErrSubSystemNotStarted) } func TestCurrencyStateTradingPair(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-usd") if err != nil { t.Fatal(err) } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.EMPTYFORMAT, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{started: 1, iExchangeManager: em}, }} _, err = s.CurrencyStateTradingPair(t.Context(), &gctrpc.CurrencyStateTradingPairRequest{ Exchange: fakeExchangeName, Pair: "btc-usd", Asset: "spot", }) require.NoError(t, err) } func TestGetFuturesPositionsOrders(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") 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) } cp.Delimiter = "" b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Futures] = ¤cy.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour}) assert.NoError(t, err) om.started = 1 s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, OrderManager: om, }, } _, err = s.GetFuturesPositionsOrders(t.Context(), &gctrpc.GetFuturesPositionsOrdersRequest{ Exchange: fakeExchangeName, Asset: asset.Futures.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: cp.Base.String(), Quote: cp.Quote.String(), }, }) require.NoError(t, err) _, err = s.GetFuturesPositionsOrders(t.Context(), &gctrpc.GetFuturesPositionsOrdersRequest{ Exchange: fakeExchangeName, Asset: asset.Spot.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: currency.DashDelimiter, Base: cp.Base.String(), Quote: cp.Quote.String(), }, }) assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) } func TestGetCollateral(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-usd") require.NoError(t, err) b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Futures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.EMPTYFORMAT, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.EMPTYFORMAT, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } b.Features.Supports.FuturesCapabilities.Collateral = true fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, }, } _, err = s.GetCollateral(t.Context(), &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, Asset: asset.Futures.String(), }) require.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "fakerino", Secret: "supafake"}) _, err = s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, Asset: asset.Futures.String(), }) require.ErrorIs(t, err, errNoAccountInformation) ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "1337"}) r, err := s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, Asset: asset.Futures.String(), IncludeBreakdown: true, }) require.NoError(t, err) if len(r.CurrencyBreakdown) != 3 { t.Errorf("expected 3 currencies, received '%v'", len(r.CurrencyBreakdown)) } if r.AvailableCollateral != "1337 USD" { t.Errorf("received '%v' expected '1337 USD'", r.AvailableCollateral) } _, err = s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, Asset: asset.Spot.String(), IncludeBreakdown: true, }) assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) _, err = s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, Asset: asset.Futures.String(), IncludeBreakdown: true, CalculateOffline: true, }) assert.NoError(t, err) } func TestShutdown(t *testing.T) { t.Parallel() s := RPCServer{Engine: &Engine{}} _, err := s.Shutdown(t.Context(), &gctrpc.ShutdownRequest{}) require.ErrorIs(t, err, errShutdownNotAllowed) s.Engine.Settings.EnableGRPCShutdown = true _, err = s.Shutdown(t.Context(), &gctrpc.ShutdownRequest{}) require.ErrorIs(t, err, errGRPCShutdownSignalIsNil) s.Engine.GRPCShutdownSignal = make(chan struct{}, 1) _, err = s.Shutdown(t.Context(), &gctrpc.ShutdownRequest{}) require.NoError(t, err) } func TestGetTechnicalAnalysis(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-usd") require.NoError(t, err) b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Futures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneDay}) err = em.Add(fExchange{IBotExchange: exch}) require.NoError(t, err) s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, }, } _, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{}) require.ErrorIs(t, err, common.ErrExchangeNameNotSet) _, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, }) require.ErrorIs(t, err, asset.ErrNotSupported) _, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "upsideprofitcontract", Pair: &gctrpc.CurrencyPair{}, }) require.ErrorIs(t, err, errExpectedTestError) _, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), }) require.ErrorIs(t, err, errInvalidStrategy) resp, err := s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "twap", }) require.NoError(t, err) if resp.Signals["TWAP"].Signals[0] != 1337 { t.Fatalf("received: '%v' but expected: '%v'", resp.Signals["TWAP"].Signals[0], 1337) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "vwap", }) require.NoError(t, err) if len(resp.Signals["VWAP"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["VWAP"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "atr", Period: 9, }) require.NoError(t, err) if len(resp.Signals["ATR"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["ATR"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "bbands", Period: 9, StandardDeviationUp: 0.5, StandardDeviationDown: 0.5, }) require.NoError(t, err) if len(resp.Signals["UPPER"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["UPPER"].Signals), 33) } if len(resp.Signals["MIDDLE"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["MIDDLE"].Signals), 33) } if len(resp.Signals["LOWER"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["LOWER"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, OtherPair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "COCO", Period: 9, }) require.NoError(t, err) if len(resp.Signals["COCO"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["COCO"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "sma", Period: 9, }) require.NoError(t, err) if len(resp.Signals["SMA"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["SMA"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "ema", Period: 9, }) require.NoError(t, err) if len(resp.Signals["EMA"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["EMA"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "macd", Period: 9, FastPeriod: 12, SlowPeriod: 26, }) require.NoError(t, err) if len(resp.Signals["MACD"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["MACD"].Signals), 33) } if len(resp.Signals["SIGNAL"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["SIGNAL"].Signals), 33) } if len(resp.Signals["HISTOGRAM"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["HISTOGRAM"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "mfi", Period: 9, }) require.NoError(t, err) if len(resp.Signals["MFI"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["MFI"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "obv", Period: 9, }) require.NoError(t, err) if len(resp.Signals["OBV"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["OBV"].Signals), 33) } resp, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ Exchange: fakeExchangeName, AssetType: "spot", Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"}, Interval: int64(kline.OneDay), AlgorithmType: "rsi", Period: 9, }) require.NoError(t, err) if len(resp.Signals["RSI"].Signals) != 33 { t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["RSI"].Signals), 33) } } func TestGetMarginRatesHistory(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { t.Fatal(err) } b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-usd") require.NoError(t, err) b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, }, } _, err = s.GetMarginRatesHistory(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) request := &gctrpc.GetMarginRatesHistoryRequest{} _, err = s.GetMarginRatesHistory(t.Context(), request) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) request.Exchange = fakeExchangeName _, err = s.GetMarginRatesHistory(t.Context(), request) assert.ErrorIs(t, err, asset.ErrNotSupported) request.Asset = asset.Spot.String() _, err = s.GetMarginRatesHistory(t.Context(), request) assert.ErrorIs(t, err, currency.ErrCurrencyNotFound) request.Currency = "usd" _, err = s.GetMarginRatesHistory(t.Context(), request) assert.NoError(t, err) request.GetBorrowRates = true request.GetLendingPayments = true request.GetBorrowCosts = true request.GetPredictedRate = true request.IncludeAllRates = true resp, err := s.GetMarginRatesHistory(t.Context(), request) assert.NoError(t, err) if len(resp.Rates) == 0 { t.Errorf("received '%v' expected '%v'", len(resp.Rates), 1) } if resp.PredictedRate == nil { t.Errorf("received '%v' expected '%v'", nil, "not nil") } if resp.TakerFeeRate != "1337" { t.Errorf("received '%v' expected '%v'", resp.TakerFeeRate, "1337") } if resp.SumLendingPayments != "1337" { t.Errorf("received '%v' expected '%v'", resp.SumLendingPayments, "1337") } if resp.AvgBorrowSize != "1337" { t.Errorf("received '%v' expected '%v'", resp.AvgBorrowSize, "1337") } if resp.AvgLendingSize != "1337" { t.Errorf("received '%v' expected '%v'", resp.AvgLendingSize, "1337") } if resp.SumBorrowCosts != "1337" { t.Errorf("received '%v' expected '%v'", resp.SumBorrowCosts, "1337") } request.CalculateOffline = true _, err = s.GetMarginRatesHistory(t.Context(), request) assert.ErrorIs(t, err, common.ErrCannotCalculateOffline) request.TakerFeeRate = "-1337" _, err = s.GetMarginRatesHistory(t.Context(), request) assert.ErrorIs(t, err, common.ErrCannotCalculateOffline) request.TakerFeeRate = "1337" _, err = s.GetMarginRatesHistory(t.Context(), request) assert.ErrorIs(t, err, common.ErrCannotCalculateOffline) request.Rates = []*gctrpc.MarginRate{ { Time: time.Now().Format(common.SimpleTimeFormatWithTimezone), HourlyRate: "1337", }, } _, err = s.GetMarginRatesHistory(t.Context(), request) assert.NoError(t, err) request.Rates = []*gctrpc.MarginRate{ { Time: time.Now().Format(common.SimpleTimeFormatWithTimezone), HourlyRate: "1337", LendingPayment: &gctrpc.LendingPayment{Size: "1337"}, BorrowCost: &gctrpc.BorrowCost{Size: "1337"}, }, } _, err = s.GetMarginRatesHistory(t.Context(), request) assert.NoError(t, err) } func TestGetFundingRates(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") 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) err = b.CurrencyPairs.Store(asset.Futures, ¤cy.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, }) if err != nil { t.Fatal(err) } err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, }) if err != nil { t.Fatal(err) } b.Features.Supports.FuturesCapabilities.FundingRates = true fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour}) assert.NoError(t, err) om.started = 1 s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, OrderManager: om, }, } _, err = s.GetFundingRates(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) request := &gctrpc.GetFundingRatesRequest{ Exchange: "", Asset: "", Pair: nil, StartDate: "", EndDate: "", IncludePredicted: false, IncludePayments: false, } _, err = s.GetFundingRates(t.Context(), request) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) request.Exchange = exch.GetName() _, err = s.GetFundingRates(t.Context(), request) assert.ErrorIs(t, err, asset.ErrNotSupported) request.Asset = asset.Spot.String() _, err = s.GetFundingRates(t.Context(), request) assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) request.Asset = asset.Futures.String() request.Pair = &gctrpc.CurrencyPair{ Delimiter: cp.Delimiter, Base: cp.Base.String(), Quote: cp.Quote.String(), } request.IncludePredicted = true request.IncludePayments = true _, err = s.GetFundingRates(t.Context(), request) assert.NoError(t, err) } func TestGetLatestFundingRate(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") 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) err = b.CurrencyPairs.Store(asset.Futures, ¤cy.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, }) if err != nil { t.Fatal(err) } err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, }) if err != nil { t.Fatal(err) } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour}) assert.NoError(t, err) om.started = 1 s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, OrderManager: om, }, } _, err = s.GetLatestFundingRate(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) request := &gctrpc.GetLatestFundingRateRequest{ Exchange: "", Asset: "", Pair: nil, IncludePredicted: false, } _, err = s.GetLatestFundingRate(t.Context(), request) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) request.Exchange = exch.GetName() _, err = s.GetLatestFundingRate(t.Context(), request) assert.ErrorIs(t, err, asset.ErrNotSupported) request.Asset = asset.Spot.String() _, err = s.GetLatestFundingRate(t.Context(), request) assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) request.Asset = asset.Futures.String() request.Pair = &gctrpc.CurrencyPair{ Delimiter: cp.Delimiter, Base: cp.Base.String(), Quote: cp.Quote.String(), } request.IncludePredicted = true _, err = s.GetLatestFundingRate(t.Context(), request) assert.NoError(t, err) } func TestGetManagedPosition(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") 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] = ¤cy.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp, cp2}, Enabled: currency.Pairs{cp, cp2}, } b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp, cp2}, Enabled: currency.Pairs{cp, cp2}, } b.Features.Supports.FuturesCapabilities.OrderManagerPositionTracking = true fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour}) assert.NoError(t, err) om.started = 1 s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, OrderManager: om, }, } _, err = s.GetManagedPosition(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) request := &gctrpc.GetManagedPositionRequest{} _, err = s.GetManagedPosition(t.Context(), request) assert.ErrorIs(t, err, common.ErrNilPointer) request.Pair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: "BTC", Quote: "USD", } _, err = s.GetManagedPosition(t.Context(), request) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) request.Exchange = fakeExchangeName _, err = s.GetManagedPosition(t.Context(), request) assert.ErrorIs(t, err, asset.ErrNotSupported) request.Asset = asset.Spot.String() _, err = s.GetManagedPosition(t.Context(), request) assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) request.Asset = asset.Futures.String() s.OrderManager, err = SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour}) assert.NoError(t, err) s.OrderManager.started = 1 s.OrderManager.activelyTrackFuturesPositions = true _, err = s.GetManagedPosition(t.Context(), request) assert.ErrorIs(t, err, futures.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, }, }, }) assert.NoError(t, err) _, err = s.GetManagedPosition(t.Context(), request) assert.NoError(t, err) } func TestGetAllManagedPositions(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") 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] = ¤cy.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp, cp2}, Enabled: currency.Pairs{cp, cp2}, } b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp, cp2}, Enabled: currency.Pairs{cp, cp2}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) var wg sync.WaitGroup om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour}) assert.NoError(t, err) om.started = 1 s := RPCServer{ Engine: &Engine{ ExchangeManager: em, currencyStateManager: &CurrencyStateManager{ started: 1, iExchangeManager: em, }, OrderManager: om, }, } _, err = s.GetAllManagedPositions(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) request := &gctrpc.GetAllManagedPositionsRequest{} s.OrderManager, err = SetupOrderManager(em, &CommunicationManager{}, &wg, &config.OrderManager{FuturesTrackingSeekDuration: time.Hour, ActivelyTrackFuturesPositions: true}) assert.NoError(t, err) s.OrderManager.started = 1 _, err = s.GetAllManagedPositions(t.Context(), request) assert.ErrorIs(t, err, futures.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, }) assert.NoError(t, err) request.IncludePredictedRate = true request.GetFundingPayments = true request.IncludeFullFundingRates = true request.IncludeFullOrderData = true _, err = s.GetAllManagedPositions(t.Context(), request) assert.NoError(t, err) } func TestGetOrderbookMovement(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") require.NoError(t, err, "NewExchangeByName must not error") exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp := currency.NewPairWithDelimiter("btc", "metal", "-") b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} req := &gctrpc.GetOrderbookMovementRequest{} _, err = s.GetOrderbookMovement(t.Context(), req) require.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = "fake" _, err = s.GetOrderbookMovement(t.Context(), req) require.ErrorIs(t, err, asset.ErrNotSupported) req.Asset = asset.Spot.String() req.Pair = &gctrpc.CurrencyPair{} _, err = s.GetOrderbookMovement(t.Context(), req) require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Pair = &gctrpc.CurrencyPair{ Base: currency.BTC.String(), Quote: currency.METAL.String(), } _, err = s.GetOrderbookMovement(t.Context(), req) if !strings.Contains(err.Error(), "cannot find orderbook") { t.Fatalf("received: '%+v' but expected: '%v'", err, "cannot find orderbook") } depth, err := orderbook.DeployDepth(req.Exchange, currency.NewPair(currency.BTC, currency.METAL), asset.Spot) require.NoError(t, err, "orderbook.DeployDepth must not error") bid := []orderbook.Level{ {Price: 10, Amount: 1}, {Price: 9, Amount: 1}, {Price: 8, Amount: 1}, {Price: 7, Amount: 1}, } ask := []orderbook.Level{ {Price: 11, Amount: 1}, {Price: 12, Amount: 1}, {Price: 13, Amount: 1}, {Price: 14, Amount: 1}, } err = depth.LoadSnapshot(&orderbook.Book{Bids: bid, Asks: ask, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true}) require.NoError(t, err, "depth.LoadSnapshot must not error") _, err = s.GetOrderbookMovement(t.Context(), req) if err.Error() != "quote amount invalid" { t.Fatalf("received: '%+v' but expected: '%v'", err, "quote amount invalid") } req.Amount = 11 move, err := s.GetOrderbookMovement(t.Context(), req) require.NoError(t, err) if move.Bought != 1 { t.Fatalf("received: '%v' but expected: '%v'", move.Bought, 1) } req.Sell = true req.Amount = 1 move, err = s.GetOrderbookMovement(t.Context(), req) require.NoError(t, err) if move.Bought != 10 { t.Fatalf("received: '%v' but expected: '%v'", move.Bought, 10) } } func TestGetOrderbookAmountByNominal(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") require.NoError(t, err, "NewExchangeByName must not error") exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp := currency.NewPairWithDelimiter("btc", "meme", "-") b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} req := &gctrpc.GetOrderbookAmountByNominalRequest{} _, err = s.GetOrderbookAmountByNominal(t.Context(), req) require.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = "fake" _, err = s.GetOrderbookAmountByNominal(t.Context(), req) require.ErrorIs(t, err, asset.ErrNotSupported) req.Asset = asset.Spot.String() req.Pair = &gctrpc.CurrencyPair{} _, err = s.GetOrderbookAmountByNominal(t.Context(), req) require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Pair = &gctrpc.CurrencyPair{ Base: currency.BTC.String(), Quote: currency.MEME.String(), } _, err = s.GetOrderbookAmountByNominal(t.Context(), req) if !strings.Contains(err.Error(), "cannot find orderbook") { t.Fatalf("received: '%+v' but expected: '%v'", err, "cannot find orderbook") } depth, err := orderbook.DeployDepth(req.Exchange, currency.NewPair(currency.BTC, currency.MEME), asset.Spot) require.NoError(t, err, "orderbook.DeployDepth must not error") bid := []orderbook.Level{ {Price: 10, Amount: 1}, {Price: 9, Amount: 1}, {Price: 8, Amount: 1}, {Price: 7, Amount: 1}, } ask := []orderbook.Level{ {Price: 11, Amount: 1}, {Price: 12, Amount: 1}, {Price: 13, Amount: 1}, {Price: 14, Amount: 1}, } err = depth.LoadSnapshot(&orderbook.Book{Bids: bid, Asks: ask, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true}) require.NoError(t, err, "depth.LoadSnapshot must not error") nominal, err := s.GetOrderbookAmountByNominal(t.Context(), req) require.NoError(t, err) if nominal.AmountRequired != 11 { t.Fatalf("received: '%v' but expected: '%v'", nominal.AmountRequired, 11) } req.Sell = true nominal, err = s.GetOrderbookAmountByNominal(t.Context(), req) require.NoError(t, err) if nominal.AmountRequired != 1 { t.Fatalf("received: '%v' but expected: '%v'", nominal.AmountRequired, 1) } } func TestGetOrderbookAmountByImpact(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") require.NoError(t, err, "NewExchangeByName must not error") exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp := currency.NewPairWithDelimiter("btc", "mad", "-") b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} req := &gctrpc.GetOrderbookAmountByImpactRequest{} _, err = s.GetOrderbookAmountByImpact(t.Context(), req) require.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = "fake" _, err = s.GetOrderbookAmountByImpact(t.Context(), req) require.ErrorIs(t, err, asset.ErrNotSupported) req.Asset = asset.Spot.String() req.Pair = &gctrpc.CurrencyPair{} _, err = s.GetOrderbookAmountByImpact(t.Context(), req) require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Pair = &gctrpc.CurrencyPair{ Base: currency.BTC.String(), Quote: currency.MAD.String(), } _, err = s.GetOrderbookAmountByImpact(t.Context(), req) if !strings.Contains(err.Error(), "cannot find orderbook") { t.Fatalf("received: '%+v' but expected: '%v'", err, "cannot find orderbook") } depth, err := orderbook.DeployDepth(req.Exchange, currency.NewPair(currency.BTC, currency.MAD), asset.Spot) require.NoError(t, err, "orderbook.DeployDepth must not error") bid := []orderbook.Level{ {Price: 10, Amount: 1}, {Price: 9, Amount: 1}, {Price: 8, Amount: 1}, {Price: 7, Amount: 1}, } ask := []orderbook.Level{ {Price: 11, Amount: 1}, {Price: 12, Amount: 1}, {Price: 13, Amount: 1}, {Price: 14, Amount: 1}, } err = depth.LoadSnapshot(&orderbook.Book{Bids: bid, Asks: ask, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true}) require.NoError(t, err, "depth.LoadSnapshot must not error") req.ImpactPercentage = 9.090909090909092 impact, err := s.GetOrderbookAmountByImpact(t.Context(), req) require.NoError(t, err) if impact.AmountRequired != 11 { t.Fatalf("received: '%v' but expected: '%v'", impact.AmountRequired, 11) } req.Sell = true req.ImpactPercentage = 10 impact, err = s.GetOrderbookAmountByImpact(t.Context(), req) require.NoError(t, err) if impact.AmountRequired != 1 { t.Fatalf("received: '%v' but expected: '%v'", impact.AmountRequired, 1) } } func TestChangePositionMargin(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-mad") if err != nil { t.Fatal(err) } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.ChangePositionMargin(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.ChangePositionMarginRequest{} _, err = s.ChangePositionMargin(t.Context(), req) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Exchange = fakeExchangeName req.Pair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: cp.Base.String(), Quote: cp.Quote.String(), } req.Asset = asset.USDTMarginedFutures.String() req.MarginSide = "BOTH" req.OriginalAllocatedMargin = 1337 req.NewAllocatedMargin = 1338 req.MarginType = "isolated" _, err = s.ChangePositionMargin(t.Context(), req) assert.NoError(t, err) } func TestSetLeverage(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-mad") if err != nil { t.Fatal(err) } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.SetLeverage(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.SetLeverageRequest{} _, err = s.SetLeverage(t.Context(), req) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Exchange = fakeExchangeName req.Pair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: cp.Base.String(), Quote: cp.Quote.String(), } req.UnderlyingPair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: cp.Base.String(), Quote: cp.Quote.String(), } req.Asset = asset.USDTMarginedFutures.String() req.MarginType = "isolated" req.Leverage = 1337 _, err = s.SetLeverage(t.Context(), req) assert.NoError(t, err) req.OrderSide = "lol" _, err = s.SetLeverage(t.Context(), req) assert.ErrorIs(t, err, order.ErrSideIsInvalid) req.OrderSide = order.Long.String() _, err = s.SetLeverage(t.Context(), req) assert.NoError(t, err) } func TestGetLeverage(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-mad") if err != nil { t.Fatal(err) } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.GetLeverage(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.GetLeverageRequest{} _, err = s.GetLeverage(t.Context(), req) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Exchange = fakeExchangeName req.Pair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: cp.Base.String(), Quote: cp.Quote.String(), } req.UnderlyingPair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: cp.Base.String(), Quote: cp.Quote.String(), } req.Asset = asset.USDTMarginedFutures.String() req.MarginType = "isolated" lev, err := s.GetLeverage(t.Context(), req) assert.NoError(t, err) if lev.Leverage != 1337 { t.Errorf("received '%v' expected '%v'", lev, 1337) } req.OrderSide = "lol" _, err = s.GetLeverage(t.Context(), req) assert.ErrorIs(t, err, order.ErrSideIsInvalid) req.OrderSide = order.Long.String() _, err = s.GetLeverage(t.Context(), req) assert.NoError(t, err) } func TestSetMarginType(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-mad") if err != nil { t.Fatal(err) } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.SetMarginType(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.SetMarginTypeRequest{} _, err = s.SetMarginType(t.Context(), req) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Exchange = fakeExchangeName req.Pair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: cp.Base.String(), Quote: cp.Quote.String(), } req.Asset = asset.USDTMarginedFutures.String() req.MarginType = "isolated" _, err = s.SetMarginType(t.Context(), req) assert.NoError(t, err) } func TestSetCollateralMode(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") if err != nil { t.Fatal(err) } exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true cp, err := currency.NewPairFromString("btc-mad") if err != nil { t.Fatal(err) } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.SetCollateralMode(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.SetCollateralModeRequest{} _, err = s.SetCollateralMode(t.Context(), req) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = fakeExchangeName req.Asset = asset.USDTMarginedFutures.String() req.CollateralMode = "single" _, err = s.SetCollateralMode(t.Context(), req) assert.NoError(t, err) } func TestGetCollateralMode(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") 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.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.GetCollateralMode(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.GetCollateralModeRequest{} _, err = s.GetCollateralMode(t.Context(), req) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = fakeExchangeName req.Asset = asset.USDTMarginedFutures.String() _, err = s.GetCollateralMode(t.Context(), req) assert.NoError(t, err) } func TestGetOpenInterest(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") assert.NoError(t, 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.USDTMarginedFutures] = ¤cy.PairStore{ AssetEnabled: true, } fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) assert.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.GetOpenInterest(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.GetOpenInterestRequest{} _, err = s.GetOpenInterest(t.Context(), req) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = fakeExchangeName _, err = s.GetOpenInterest(t.Context(), req) assert.NoError(t, err) req.Data = append(req.Data, &gctrpc.OpenInterestDataRequest{ Asset: asset.USDTMarginedFutures.String(), Pair: &gctrpc.CurrencyPair{Base: currency.BTC.String(), Quote: currency.USDT.String()}, }) _, err = s.GetOpenInterest(t.Context(), req) assert.NoError(t, err) } func TestStartRPCRESTProxy(t *testing.T) { t.Parallel() tempDir := filepath.Join(os.TempDir(), "gct-grpc-proxy-test") tempDirTLS := filepath.Join(tempDir, "tls") t.Cleanup(func() { assert.NoErrorf(t, os.RemoveAll(tempDir), "RemoveAll should not error, manual directory deletion required for TempDir: %s", tempDir) }) if !assert.NoError(t, genCert(tempDirTLS), "genCert should not error") { t.FailNow() } gRPCPort := rand.Intn(65535-42069) + 42069 //nolint:gosec // Don't require crypto/rand usage here gRPCProxyPort := gRPCPort + 1 e := &Engine{ Config: &config.Config{ RemoteControl: config.RemoteControlConfig{ Username: "bobmarley", Password: "Sup3rdup3rS3cr3t", GRPC: config.GRPCConfig{ Enabled: true, ListenAddress: "localhost:" + strconv.Itoa(gRPCPort), GRPCProxyListenAddress: "localhost:" + strconv.Itoa(gRPCProxyPort), }, }, }, Settings: Settings{ DataDir: tempDir, CoreSettings: CoreSettings{EnableGRPCProxy: true}, }, } fakeTime := time.Now().Add(-time.Hour) e.uptime = fakeTime StartRPCServer(e) // Give the proxy time to start time.Sleep(time.Millisecond * 500) certFile := filepath.Join(tempDirTLS, "cert.pem") caCert, err := os.ReadFile(certFile) require.NoError(t, err, "ReadFile must not error") caCertPool := x509.NewCertPool() ok := caCertPool.AppendCertsFromPEM(caCert) require.True(t, ok, "AppendCertsFromPEM must return true") client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12}}} for _, creds := range []struct { testDescription string username string password string }{ {"Valid credentials", "bobmarley", "Sup3rdup3rS3cr3t"}, {"Valid username but invalid password", "bobmarley", "wrongpass"}, {"Invalid username but valid password", "bonk", "Sup3rdup3rS3cr3t"}, {"Invalid username and password despite glorious credentials", "bonk", "wif"}, } { t.Run(creds.testDescription, func(t *testing.T) { t.Parallel() req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://localhost:"+strconv.Itoa(gRPCProxyPort)+"/v1/getinfo", http.NoBody) require.NoError(t, err, "NewRequestWithContext must not error") req.SetBasicAuth(creds.username, creds.password) resp, err := client.Do(req) require.NoError(t, err, "Do must not error") defer resp.Body.Close() if creds.username == "bobmarley" && creds.password == "Sup3rdup3rS3cr3t" { var info gctrpc.GetInfoResponse err = json.NewDecoder(resp.Body).Decode(&info) require.NoError(t, err, "Decode must not error") uptimeDuration, err := time.ParseDuration(info.Uptime) require.NoError(t, err, "ParseDuration must not error") assert.InDelta(t, time.Since(fakeTime).Seconds(), uptimeDuration.Seconds(), 1.0, "Uptime should be within 1 second of the expected duration") } else { respBody, err := io.ReadAll(resp.Body) require.NoError(t, err, "ReadAll must not error") assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "HTTP status code should be 401") assert.Equal(t, "Access denied\n", string(respBody), "Response body should be 'Access denied\n'") } }) } } func TestRPCProxyAuthClient(t *testing.T) { t.Parallel() s := new(RPCServer) s.Engine = &Engine{ Config: &config.Config{ RemoteControl: config.RemoteControlConfig{ Username: "bobmarley", Password: "Sup3rdup3rS3cr3t", }, }, } dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, err := w.Write([]byte("MEOW")) assert.NoError(t, err, "Write should not error") }) handler := s.authClient(dummyHandler) for _, creds := range []struct { testDescription string username string password string }{ {"Valid credentials", "bobmarley", "Sup3rdup3rS3cr3t"}, {"Valid username but invalid password", "bobmarley", "wrongpass"}, {"Invalid username but valid password", "bonk", "Sup3rdup3rS3cr3t"}, {"Invalid username and password despite glorious credentials", "bonk", "wif"}, } { t.Run(creds.testDescription, func(t *testing.T) { t.Parallel() req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody) require.NoError(t, err, "NewRequestWithContext must not error") req.SetBasicAuth(creds.username, creds.password) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if creds.username == "bobmarley" && creds.password == "Sup3rdup3rS3cr3t" { assert.Equal(t, http.StatusOK, rr.Code, "HTTP status code should be 200") assert.Equal(t, "MEOW", rr.Body.String(), "Response body should be 'MEOW'") } else { assert.Equal(t, http.StatusUnauthorized, rr.Code, "HTTP status code should be 401") assert.Equal(t, "Access denied\n", rr.Body.String(), "Response body should be 'Access denied\n'") } }) } } func TestGetCurrencyTradeURL(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName("binance") require.NoError(t, err) exch.SetDefaults() b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: true, Enabled: []currency.Pair{currency.NewBTCUSDT()}, Available: []currency.Pair{currency.NewBTCUSDT()}, RequestFormat: ¤cy.PairFormat{Uppercase: true}, ConfigFormat: ¤cy.PairFormat{Uppercase: true}, }) require.NoError(t, err) fakeExchange := fExchange{ IBotExchange: exch, } err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} _, err = s.GetCurrencyTradeURL(t.Context(), nil) assert.ErrorIs(t, err, common.ErrNilPointer) req := &gctrpc.GetCurrencyTradeURLRequest{} _, err = s.GetCurrencyTradeURL(t.Context(), req) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) req.Exchange = fakeExchangeName _, err = s.GetCurrencyTradeURL(t.Context(), req) assert.ErrorIs(t, err, asset.ErrNotSupported) req.Asset = "spot" _, err = s.GetCurrencyTradeURL(t.Context(), req) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) req.Pair = &gctrpc.CurrencyPair{ Delimiter: "-", Base: "btc", Quote: "usdt", } resp, err := s.GetCurrencyTradeURL(t.Context(), req) assert.NoError(t, err) assert.NotEmpty(t, resp.Url) }