package futures import ( "context" "testing" "time" "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/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) const testExchange = "test" // FakePNL implements PNL interface type FakePNL struct { err error result *PNLResult } // CalculatePNL overrides default pnl calculations func (f *FakePNL) CalculatePNL(context.Context, *PNLCalculatorRequest) (*PNLResult, error) { if f.err != nil { return nil, f.err } return f.result, nil } // GetCurrencyForRealisedPNL overrides default pnl calculations func (f *FakePNL) GetCurrencyForRealisedPNL(realisedAsset asset.Item, realisedPair currency.Pair) (currency.Code, asset.Item, error) { if f.err != nil { return realisedPair.Base, asset.Empty, f.err } return realisedPair.Base, realisedAsset, nil } func TestUpsertPNLEntry(t *testing.T) { t.Parallel() var results []PNLResult result := &PNLResult{ IsOrder: true, } _, err := upsertPNLEntry(results, result) assert.ErrorIs(t, err, errTimeUnset) tt := time.Now() result.Time = tt results, err = upsertPNLEntry(results, result) assert.NoError(t, err) if len(results) != 1 { t.Errorf("expected 1 received %v", len(results)) } result.Fee = decimal.NewFromInt(1337) results, err = upsertPNLEntry(results, result) assert.NoError(t, err) if len(results) != 1 { t.Errorf("expected 1 received %v", len(results)) } if !results[0].Fee.Equal(result.Fee) { t.Errorf("expected %v received %v", result.Fee, results[0].Fee) } } func TestTrackNewOrder(t *testing.T) { t.Parallel() exch := testExchange item := asset.Futures pair, err := currency.NewPairFromStrings("BTC", "1231") assert.NoError(t, err) setup := &PositionTrackerSetup{ Exchange: exch, Asset: item, Pair: pair, } c, err := SetupPositionTracker(setup) assert.NoError(t, err) err = c.TrackNewOrder(nil, false) assert.ErrorIs(t, err, common.ErrNilPointer) err = c.TrackNewOrder(&order.Detail{}, false) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) od := &order.Detail{ Exchange: exch, AssetType: item, Pair: pair, OrderID: "1", Price: 1337, } err = c.TrackNewOrder(od, false) assert.ErrorIs(t, err, order.ErrSideIsInvalid) od.Side = order.Long od.Amount = 1 od.OrderID = "2" err = c.TrackNewOrder(od, false) assert.ErrorIs(t, err, errTimeUnset) c.openingDirection = order.Long od.Date = time.Now() err = c.TrackNewOrder(od, false) assert.NoError(t, err) if !c.openingPrice.Equal(decimal.NewFromInt(1337)) { t.Errorf("expected 1337, received %v", c.openingPrice) } if len(c.longPositions) != 1 { t.Error("expected a long") } if c.latestDirection != order.Long { t.Error("expected recognition that its long") } if c.exposure.InexactFloat64() != od.Amount { t.Error("expected 1") } od.Date = od.Date.Add(1) od.Amount = 0.4 od.Side = order.Short od.OrderID = "3" err = c.TrackNewOrder(od, false) assert.NoError(t, err) if len(c.shortPositions) != 1 { t.Error("expected a short") } if c.latestDirection != order.Long { t.Error("expected recognition that its long") } if c.exposure.InexactFloat64() != 0.6 { t.Error("expected 0.6") } od.Date = od.Date.Add(1) od.Amount = 0.8 od.Side = order.Short od.OrderID = "4" od.Fee = 0.1 err = c.TrackNewOrder(od, false) assert.NoError(t, err) if c.latestDirection != order.Short { t.Error("expected recognition that its short") } if !c.exposure.Equal(decimal.NewFromFloat(0.2)) { t.Errorf("expected %v received %v", 0.2, c.exposure) } od.Date = od.Date.Add(1) od.OrderID = "5" od.Side = order.Long od.Amount = 0.2 err = c.TrackNewOrder(od, false) assert.NoError(t, err) if c.latestDirection != order.ClosePosition { t.Errorf("expected recognition that its closed, received '%v'", c.latestDirection) } if c.status != order.Closed { t.Errorf("expected recognition that its closed, received '%v'", c.status) } err = c.TrackNewOrder(od, false) assert.NoError(t, err) od.OrderID = "hellomoto" err = c.TrackNewOrder(od, false) assert.ErrorIs(t, err, ErrPositionClosed) if c.latestDirection != order.ClosePosition { t.Errorf("expected recognition that its closed, received '%v'", c.latestDirection) } if c.status != order.Closed { t.Errorf("expected recognition that its closed, received '%v'", c.status) } err = c.TrackNewOrder(od, true) assert.ErrorIs(t, err, errCannotTrackInvalidParams) c, err = SetupPositionTracker(setup) assert.NoError(t, err) err = c.TrackNewOrder(od, true) assert.NoError(t, err) var ptp *PositionTracker err = ptp.TrackNewOrder(nil, false) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestSetupMultiPositionTracker(t *testing.T) { t.Parallel() _, err := SetupMultiPositionTracker(nil) assert.ErrorIs(t, err, errNilSetup) setup := &MultiPositionTrackerSetup{} _, err = SetupMultiPositionTracker(setup) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) setup.Exchange = testExchange _, err = SetupMultiPositionTracker(setup) assert.ErrorIs(t, err, ErrNotFuturesAsset) setup.Asset = asset.Futures _, err = SetupMultiPositionTracker(setup) assert.ErrorIs(t, err, order.ErrPairIsEmpty) setup.Pair = currency.NewBTCUSDT() _, err = SetupMultiPositionTracker(setup) assert.ErrorIs(t, err, errEmptyUnderlying) setup.Underlying = currency.BTC _, err = SetupMultiPositionTracker(setup) assert.NoError(t, err) setup.UseExchangePNLCalculation = true _, err = SetupMultiPositionTracker(setup) assert.ErrorIs(t, err, errMissingPNLCalculationFunctions) setup.ExchangePNLCalculation = &FakePNL{} resp, err := SetupMultiPositionTracker(setup) assert.NoError(t, err) if resp.exchange != testExchange { t.Errorf("expected 'test' received %v", resp.exchange) } } func TestMultiPositionTrackerTrackNewOrder(t *testing.T) { t.Parallel() exch := testExchange item := asset.Futures pair := currency.NewBTCUSDT() setup := &MultiPositionTrackerSetup{ Asset: item, Pair: pair, Underlying: pair.Base, ExchangePNLCalculation: &FakePNL{}, } _, err := SetupMultiPositionTracker(setup) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) setup.Exchange = testExchange resp, err := SetupMultiPositionTracker(setup) assert.NoError(t, err) tt := time.Now() err = resp.TrackNewOrder(&order.Detail{ Date: tt, AssetType: item, Pair: pair, Side: order.Short, OrderID: "1", Amount: 1, }) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, AssetType: item, Pair: pair, Side: order.Short, OrderID: "1", Amount: 1, }) assert.NoError(t, err) if len(resp.positions) != 1 { t.Errorf("expected '1' received %v", len(resp.positions)) } err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, AssetType: item, Pair: pair, Side: order.Short, OrderID: "2", Amount: 1, }) assert.NoError(t, err) if len(resp.positions) != 1 { t.Errorf("expected '1' received %v", len(resp.positions)) } err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, AssetType: item, Pair: pair, Side: order.Long, OrderID: "3", Amount: 2, }) assert.NoError(t, err) if len(resp.positions) != 1 { t.Errorf("expected '1' received %v", len(resp.positions)) } if resp.positions[0].status != order.Closed { t.Errorf("expected 'closed' received %v", resp.positions[0].status) } resp.positions[0].status = order.Open resp.positions = append(resp.positions, resp.positions...) err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, AssetType: item, Pair: pair, Side: order.Long, OrderID: "4", Amount: 2, }) assert.ErrorIs(t, err, errPositionDiscrepancy) resp.positions = []*PositionTracker{resp.positions[0]} resp.positions[0].status = order.Closed err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, AssetType: item, Pair: pair, Side: order.Long, OrderID: "4", Amount: 2, }) assert.NoError(t, err) if len(resp.positions) != 2 { t.Errorf("expected '2' received %v", len(resp.positions)) } err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, AssetType: item, Pair: pair, Side: order.Long, OrderID: "4", Amount: 2, }) assert.NoError(t, err) if len(resp.positions) != 2 { t.Errorf("expected '2' received %v", len(resp.positions)) } resp.positions[0].status = order.Closed err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, Pair: pair, AssetType: asset.USDTMarginedFutures, Side: order.Long, OrderID: "5", Amount: 2, }) assert.ErrorIs(t, err, errAssetMismatch) err = resp.TrackNewOrder(nil) assert.ErrorIs(t, err, common.ErrNilPointer) resp = nil err = resp.TrackNewOrder(&order.Detail{ Date: tt, Exchange: exch, Pair: pair, AssetType: asset.USDTMarginedFutures, Side: order.Long, OrderID: "5", Amount: 2, }) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestSetupPositionControllerReal(t *testing.T) { t.Parallel() pc := SetupPositionController() if pc.multiPositionTrackers == nil { t.Error("unexpected nil") } } func TestPositionControllerTestTrackNewOrder(t *testing.T) { t.Parallel() pc := SetupPositionController() err := pc.TrackNewOrder(nil) assert.ErrorIs(t, err, errNilOrder) err = pc.TrackNewOrder(&order.Detail{ Date: time.Now(), Exchange: "hi", Pair: currency.NewBTCUSDT(), AssetType: asset.Spot, Side: order.Long, OrderID: "lol", }) assert.ErrorIs(t, err, ErrNotFuturesAsset) err = pc.TrackNewOrder(&order.Detail{ Date: time.Now(), Pair: currency.NewBTCUSDT(), AssetType: asset.Futures, Side: order.Long, OrderID: "lol", }) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) err = pc.TrackNewOrder(&order.Detail{ Exchange: testExchange, Date: time.Now(), Pair: currency.NewBTCUSDT(), AssetType: asset.Futures, Side: order.Long, OrderID: "lol", }) assert.NoError(t, err) var pcp *PositionController err = pcp.TrackNewOrder(nil) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestGetLatestPNLSnapshot(t *testing.T) { t.Parallel() pt := PositionTracker{} _, err := pt.GetLatestPNLSnapshot() assert.ErrorIs(t, err, errNoPNLHistory) pnl := PNLResult{ Time: time.Now(), UnrealisedPNL: decimal.NewFromInt(1337), RealisedPNLBeforeFees: decimal.NewFromInt(1337), } pt.pnlHistory = append(pt.pnlHistory, pnl) result, err := pt.GetLatestPNLSnapshot() assert.NoError(t, err) if result != pt.pnlHistory[0] { t.Error("unexpected result") } } func TestGetRealisedPNL(t *testing.T) { t.Parallel() p := PositionTracker{} result := p.GetRealisedPNL() if !result.IsZero() { t.Error("expected zero") } } func TestGetStats(t *testing.T) { t.Parallel() p := &PositionTracker{} stats := p.GetStats() if len(stats.Orders) != 0 { t.Error("expected 0") } p.exchange = testExchange p.fundingRateDetails = &fundingrate.HistoricalRates{ FundingRates: []fundingrate.Rate{ {}, }, } stats = p.GetStats() if stats.Exchange != p.exchange { t.Errorf("expected '%v' received '%v'", p.exchange, stats.Exchange) } p = nil stats = p.GetStats() if stats != nil { t.Errorf("expected '%v' received '%v'", nil, stats) } } func TestGetPositions(t *testing.T) { t.Parallel() p := &MultiPositionTracker{} positions := p.GetPositions() if len(positions) > 0 { t.Error("expected 0") } p.positions = append(p.positions, &PositionTracker{ exchange: testExchange, }) positions = p.GetPositions() if len(positions) != 1 { t.Fatal("expected 1") } if positions[0].Exchange != testExchange { t.Error("expected 'test'") } p = nil positions = p.GetPositions() if len(positions) > 0 { t.Error("expected 0") } } func TestGetPositionsForExchange(t *testing.T) { t.Parallel() c := &PositionController{} p := currency.NewBTCUSDT() _, err := c.GetPositionsForExchange("", asset.Futures, p) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) pos, err := c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) if len(pos) != 0 { t.Error("expected zero") } c.multiPositionTrackers = make(map[key.ExchangeAssetPair]*MultiPositionTracker) c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) require.ErrorIs(t, err, ErrPositionNotFound, "GetPositionsForExchange must return ErrPositionNotFound") c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) _, err = c.GetPositionsForExchange(testExchange, asset.Spot, p) assert.ErrorIs(t, err, ErrNotFuturesAsset) c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = &MultiPositionTracker{ exchange: testExchange, } pos, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.NoError(t, err) if len(pos) != 0 { t.Fatal("expected zero") } c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = &MultiPositionTracker{ exchange: testExchange, positions: []*PositionTracker{ { exchange: testExchange, }, }, } pos, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.NoError(t, err) if len(pos) != 1 { t.Fatal("expected 1") } if pos[0].Exchange != testExchange { t.Error("expected test") } c = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestClearPositionsForExchange(t *testing.T) { t.Parallel() c := &PositionController{} p := currency.NewBTCUSDT() err := c.ClearPositionsForExchange("", asset.Futures, p) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) c.multiPositionTrackers = make(map[key.ExchangeAssetPair]*MultiPositionTracker) err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) err = c.ClearPositionsForExchange(testExchange, asset.Spot, p) assert.ErrorIs(t, err, ErrNotFuturesAsset) c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = &MultiPositionTracker{ exchange: testExchange, underlying: currency.DOGE, positions: []*PositionTracker{ { exchange: testExchange, }, }, } err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) require.NoError(t, err, "ClearPositionsForExchange must not error") if len(c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)].positions) != 0 { t.Fatal("expected 0") } c = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestCalculateRealisedPNL(t *testing.T) { t.Parallel() result := calculateRealisedPNL(nil) if !result.IsZero() { t.Errorf("received '%v' expected '0'", result) } result = calculateRealisedPNL([]PNLResult{ { IsOrder: true, RealisedPNLBeforeFees: decimal.NewFromInt(1337), }, }) if !result.Equal(decimal.NewFromInt(1337)) { t.Errorf("received '%v' expected '1337'", result) } result = calculateRealisedPNL([]PNLResult{ { IsOrder: true, RealisedPNLBeforeFees: decimal.NewFromInt(1339), Fee: decimal.NewFromInt(2), }, { IsOrder: true, RealisedPNLBeforeFees: decimal.NewFromInt(2), Fee: decimal.NewFromInt(2), }, }) if !result.Equal(decimal.NewFromInt(1337)) { t.Errorf("received '%v' expected '1337'", result) } } func TestSetupPositionTracker(t *testing.T) { t.Parallel() p, err := SetupPositionTracker(nil) assert.ErrorIs(t, err, errNilSetup) if p != nil { t.Error("expected nil") } p, err = SetupPositionTracker(&PositionTrackerSetup{ Asset: asset.Spot, }) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) if p != nil { t.Error("expected nil") } p, err = SetupPositionTracker(&PositionTrackerSetup{ Exchange: testExchange, Asset: asset.Spot, }) assert.ErrorIs(t, err, ErrNotFuturesAsset) if p != nil { t.Error("expected nil") } p, err = SetupPositionTracker(&PositionTrackerSetup{ Exchange: testExchange, Asset: asset.Futures, }) assert.ErrorIs(t, err, order.ErrPairIsEmpty) if p != nil { t.Error("expected nil") } cp := currency.NewBTCUSDT() p, err = SetupPositionTracker(&PositionTrackerSetup{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, }) require.NoError(t, err) if p == nil { //nolint:staticcheck,nolintlint // SA5011 Ignore the nil warnings t.Fatal("expected not nil") } if p.exchange != testExchange { //nolint:staticcheck,nolintlint // SA5011 Ignore the nil warnings t.Error("expected test") } _, err = SetupPositionTracker(&PositionTrackerSetup{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, UseExchangePNLCalculation: true, }) assert.ErrorIs(t, err, ErrNilPNLCalculator) p, err = SetupPositionTracker(&PositionTrackerSetup{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, UseExchangePNLCalculation: true, PNLCalculator: &PNLCalculator{}, }) assert.NoError(t, err) if !p.useExchangePNLCalculation { t.Error("expected true") } } func TestCalculatePNL(t *testing.T) { t.Parallel() p := &PNLCalculator{} _, err := p.CalculatePNL(t.Context(), nil) assert.ErrorIs(t, err, ErrNilPNLCalculator) _, err = p.CalculatePNL(t.Context(), &PNLCalculatorRequest{}) assert.ErrorIs(t, err, errCannotCalculateUnrealisedPNL) _, err = p.CalculatePNL(t.Context(), &PNLCalculatorRequest{ OrderDirection: order.Short, CurrentDirection: order.Long, }) assert.ErrorIs(t, err, errCannotCalculateUnrealisedPNL) } func TestTrackPNLByTime(t *testing.T) { t.Parallel() p := &PositionTracker{} err := p.TrackPNLByTime(time.Now(), 1) assert.NoError(t, err) err = p.TrackPNLByTime(time.Now(), 2) assert.NoError(t, err) if !p.latestPrice.Equal(decimal.NewFromInt(2)) { t.Error("expected 2") } p = nil err = p.TrackPNLByTime(time.Now(), 2) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestUpdateOpenPositionUnrealisedPNL(t *testing.T) { t.Parallel() pc := SetupPositionController() _, err := pc.UpdateOpenPositionUnrealisedPNL("", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) _, err = pc.UpdateOpenPositionUnrealisedPNL("hi", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) assert.ErrorIs(t, err, ErrPositionNotFound) _, err = pc.UpdateOpenPositionUnrealisedPNL("hi", asset.Spot, currency.NewBTCUSDT(), 2, time.Now()) assert.ErrorIs(t, err, ErrNotFuturesAsset) err = pc.TrackNewOrder(&order.Detail{ Date: time.Now(), Exchange: "hi", Pair: currency.NewBTCUSDT(), AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1, Amount: 1, }) assert.NoError(t, err) _, err = pc.UpdateOpenPositionUnrealisedPNL("hi2", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) assert.ErrorIs(t, err, ErrPositionNotFound) _, err = pc.UpdateOpenPositionUnrealisedPNL("hi", asset.PerpetualSwap, currency.NewBTCUSDT(), 2, time.Now()) assert.ErrorIs(t, err, ErrPositionNotFound) _, err = pc.UpdateOpenPositionUnrealisedPNL("hi", asset.Futures, currency.NewPair(currency.BTC, currency.DOGE), 2, time.Now()) assert.ErrorIs(t, err, ErrPositionNotFound) pnl, err := pc.UpdateOpenPositionUnrealisedPNL("hi", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) assert.NoError(t, err) if !pnl.Equal(decimal.NewFromInt(1)) { t.Errorf("received '%v' expected '%v", pnl, 1) } var nilPC *PositionController _, err = nilPC.UpdateOpenPositionUnrealisedPNL("hi", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestSetCollateralCurrency(t *testing.T) { t.Parallel() pc := SetupPositionController() err := pc.SetCollateralCurrency("", asset.Spot, currency.EMPTYPAIR, currency.Code{}) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) err = pc.SetCollateralCurrency("hi", asset.Spot, currency.EMPTYPAIR, currency.Code{}) assert.ErrorIs(t, err, ErrNotFuturesAsset) p := currency.NewBTCUSDT() pc.multiPositionTrackers = make(map[key.ExchangeAssetPair]*MultiPositionTracker) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) require.ErrorIs(t, err, ErrPositionNotFound) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) require.ErrorIs(t, err, ErrPositionNotFound) mapKey := key.NewExchangeAssetPair("hi", asset.Futures, p) pc.multiPositionTrackers[mapKey] = &MultiPositionTracker{ exchange: "hi", asset: asset.Futures, pair: p, orderPositions: make(map[string]*PositionTracker), } err = pc.TrackNewOrder(&order.Detail{ Date: time.Now(), Exchange: "hi", Pair: p, AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1, Amount: 1, }) require.NoError(t, err) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) require.NoError(t, err) if !pc.multiPositionTrackers[mapKey].collateralCurrency.Equal(currency.DOGE) { t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers[mapKey].collateralCurrency, currency.DOGE) } if !pc.multiPositionTrackers[mapKey].positions[0].collateralCurrency.Equal(currency.DOGE) { t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers[mapKey].positions[0].collateralCurrency, currency.DOGE) } var nilPC *PositionController err = nilPC.SetCollateralCurrency("hi", asset.Spot, currency.EMPTYPAIR, currency.Code{}) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestMPTUpdateOpenPositionUnrealisedPNL(t *testing.T) { t.Parallel() p := currency.NewBTCUSDT() pc := SetupPositionController() err := pc.TrackNewOrder(&order.Detail{ Date: time.Now(), Exchange: "hi", Pair: p, AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1, Amount: 1, }) require.NoError(t, err) mapKey := key.NewExchangeAssetPair("hi", asset.Futures, p) result, err := pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) require.NoError(t, err) if result.Equal(decimal.NewFromInt(1337)) { t.Error("") } pc.multiPositionTrackers[mapKey].positions[0].status = order.Closed _, err = pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) require.ErrorIs(t, err, ErrPositionClosed) pc.multiPositionTrackers[mapKey].positions = nil _, err = pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) require.ErrorIs(t, err, ErrPositionNotFound) } func TestMPTLiquidate(t *testing.T) { t.Parallel() item := asset.Futures pair, err := currency.NewPairFromStrings("BTC", "1231") assert.NoError(t, err) e := &MultiPositionTracker{ exchange: testExchange, exchangePNLCalculation: &FakePNL{}, asset: item, orderPositions: make(map[string]*PositionTracker), } err = e.Liquidate(decimal.Zero, time.Time{}) assert.ErrorIs(t, err, ErrPositionNotFound) setup := &PositionTrackerSetup{ Pair: pair, Asset: item, } _, err = SetupPositionTracker(setup) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) setup.Exchange = "exch" _, err = SetupPositionTracker(setup) assert.NoError(t, err) tt := time.Now() err = e.TrackNewOrder(&order.Detail{ Date: tt, Exchange: testExchange, Pair: pair, AssetType: item, Side: order.Long, OrderID: "lol", Price: 1, Amount: 1, }) assert.NoError(t, err) err = e.Liquidate(decimal.Zero, time.Time{}) assert.ErrorIs(t, err, order.ErrCannotLiquidate) err = e.Liquidate(decimal.Zero, tt) assert.NoError(t, err) if e.positions[0].status != order.Liquidated { t.Errorf("received '%v' expected '%v'", e.positions[0].status, order.Liquidated) } if !e.positions[0].exposure.IsZero() { t.Errorf("received '%v' expected '%v'", e.positions[0].exposure, 0) } e = nil err = e.Liquidate(decimal.Zero, tt) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestPositionLiquidate(t *testing.T) { t.Parallel() item := asset.Futures pair, err := currency.NewPairFromStrings("BTC", "1231") assert.NoError(t, err) p := &PositionTracker{ contractPair: pair, asset: item, exchange: testExchange, PNLCalculation: &PNLCalculator{}, status: order.Open, openingDirection: order.Long, } tt := time.Now() err = p.TrackNewOrder(&order.Detail{ Date: tt, Exchange: testExchange, Pair: pair, AssetType: item, Side: order.Long, OrderID: "lol", Price: 1, Amount: 1, }, false) assert.NoError(t, err) err = p.Liquidate(decimal.Zero, time.Time{}) assert.ErrorIs(t, err, order.ErrCannotLiquidate) err = p.Liquidate(decimal.Zero, tt) assert.NoError(t, err) if p.status != order.Liquidated { t.Errorf("received '%v' expected '%v'", p.status, order.Liquidated) } if !p.exposure.IsZero() { t.Errorf("received '%v' expected '%v'", p.exposure, 0) } p = nil err = p.Liquidate(decimal.Zero, tt) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestGetOpenPosition(t *testing.T) { t.Parallel() pc := SetupPositionController() cp := currency.NewPair(currency.BTC, currency.PERP) tn := time.Now() _, err := pc.GetOpenPosition("", asset.Futures, cp) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) _, err = pc.GetOpenPosition(testExchange, asset.Futures, cp) assert.ErrorIs(t, err, ErrPositionNotFound) err = pc.TrackNewOrder(&order.Detail{ Date: tn, Exchange: testExchange, Pair: cp, AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1337, Amount: 1337, }) assert.NoError(t, err) _, err = pc.GetOpenPosition(testExchange, asset.Futures, cp) assert.NoError(t, err) } func TestGetAllOpenPositions(t *testing.T) { t.Parallel() pc := SetupPositionController() _, err := pc.GetAllOpenPositions() assert.ErrorIs(t, err, ErrNoPositionsFound) cp := currency.NewPair(currency.BTC, currency.PERP) tn := time.Now() err = pc.TrackNewOrder(&order.Detail{ Date: tn, Exchange: testExchange, Pair: cp, AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1337, Amount: 1337, }) assert.NoError(t, err) _, err = pc.GetAllOpenPositions() assert.NoError(t, err) } func TestPCTrackFundingDetails(t *testing.T) { t.Parallel() pc := SetupPositionController() err := pc.TrackFundingDetails(nil) assert.ErrorIs(t, err, common.ErrNilPointer) p := currency.NewPair(currency.BTC, currency.PERP) rates := &fundingrate.HistoricalRates{ Asset: asset.Futures, Pair: p, } err = pc.TrackFundingDetails(rates) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) rates.Exchange = testExchange err = pc.TrackFundingDetails(rates) assert.ErrorIs(t, err, ErrPositionNotFound) tn := time.Now() err = pc.TrackNewOrder(&order.Detail{ Date: tn, Exchange: testExchange, Pair: p, AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1337, Amount: 1337, }) assert.NoError(t, err) rates.StartDate = tn.Add(-time.Hour) rates.EndDate = tn rates.FundingRates = []fundingrate.Rate{ { Time: tn, Rate: decimal.NewFromInt(1337), Payment: decimal.NewFromInt(1337), }, } mapKey := key.NewExchangeAssetPair(testExchange, asset.Futures, p) pc.multiPositionTrackers[mapKey].orderPositions["lol"].openingDate = tn.Add(-time.Hour) pc.multiPositionTrackers[mapKey].orderPositions["lol"].lastUpdated = tn err = pc.TrackFundingDetails(rates) assert.NoError(t, err) } func TestMPTTrackFundingDetails(t *testing.T) { t.Parallel() mpt := &MultiPositionTracker{ orderPositions: make(map[string]*PositionTracker), } err := mpt.TrackFundingDetails(nil) assert.ErrorIs(t, err, common.ErrNilPointer) cp := currency.NewPair(currency.BTC, currency.PERP) rates := &fundingrate.HistoricalRates{ Asset: asset.Futures, Pair: cp, } err = mpt.TrackFundingDetails(rates) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) mpt.exchange = testExchange rates = &fundingrate.HistoricalRates{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, } err = mpt.TrackFundingDetails(rates) assert.ErrorIs(t, err, errAssetMismatch) mpt.asset = rates.Asset mpt.pair = cp err = mpt.TrackFundingDetails(rates) assert.ErrorIs(t, err, ErrPositionNotFound) tn := time.Now() err = mpt.TrackNewOrder(&order.Detail{ Date: tn, Exchange: testExchange, Pair: cp, AssetType: asset.Futures, Side: order.Long, OrderID: "lol", Price: 1337, Amount: 1337, }) assert.NoError(t, err) rates.StartDate = tn.Add(-time.Hour) rates.EndDate = tn rates.FundingRates = []fundingrate.Rate{ { Time: tn, Rate: decimal.NewFromInt(1337), Payment: decimal.NewFromInt(1337), }, } mpt.orderPositions["lol"].openingDate = tn.Add(-time.Hour) mpt.orderPositions["lol"].lastUpdated = tn rates.Exchange = "lol" err = mpt.TrackFundingDetails(rates) assert.ErrorIs(t, err, errExchangeNameMismatch) } func TestPTTrackFundingDetails(t *testing.T) { t.Parallel() p := &PositionTracker{} err := p.TrackFundingDetails(nil) assert.ErrorIs(t, err, common.ErrNilPointer) cp := currency.NewPair(currency.BTC, currency.PERP) rates := &fundingrate.HistoricalRates{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, } err = p.TrackFundingDetails(rates) assert.ErrorIs(t, err, errDoesntMatch) p.exchange = testExchange p.asset = asset.Futures p.contractPair = cp err = p.TrackFundingDetails(rates) assert.ErrorIs(t, err, common.ErrDateUnset) rates.StartDate = time.Now().Add(-time.Hour) rates.EndDate = time.Now() p.openingDate = rates.StartDate err = p.TrackFundingDetails(rates) assert.ErrorIs(t, err, ErrNoPositionsFound) p.pnlHistory = append(p.pnlHistory, PNLResult{ Time: rates.EndDate, UnrealisedPNL: decimal.NewFromInt(1337), RealisedPNLBeforeFees: decimal.NewFromInt(1337), Price: decimal.NewFromInt(1337), Exposure: decimal.NewFromInt(1337), Fee: decimal.NewFromInt(1337), }) err = p.TrackFundingDetails(rates) assert.NoError(t, err) rates.FundingRates = []fundingrate.Rate{ { Time: rates.StartDate, Rate: decimal.NewFromInt(1337), Payment: decimal.NewFromInt(1337), }, } err = p.TrackFundingDetails(rates) assert.NoError(t, err) err = p.TrackFundingDetails(rates) assert.NoError(t, err) rates.StartDate = rates.StartDate.Add(-time.Hour) err = p.TrackFundingDetails(rates) assert.NoError(t, err) rates.Exchange = "" err = p.TrackFundingDetails(rates) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) p = nil err = p.TrackFundingDetails(rates) assert.ErrorIs(t, err, common.ErrNilPointer) } func TestAreFundingRatePrerequisitesMet(t *testing.T) { t.Parallel() err := CheckFundingRatePrerequisites(false, false, false) assert.NoError(t, err) err = CheckFundingRatePrerequisites(true, false, false) assert.NoError(t, err) err = CheckFundingRatePrerequisites(true, true, false) assert.NoError(t, err) err = CheckFundingRatePrerequisites(true, true, true) assert.NoError(t, err) err = CheckFundingRatePrerequisites(true, false, true) assert.NoError(t, err) err = CheckFundingRatePrerequisites(false, false, true) assert.ErrorIs(t, err, ErrGetFundingDataRequired) err = CheckFundingRatePrerequisites(false, true, true) assert.ErrorIs(t, err, ErrGetFundingDataRequired) err = CheckFundingRatePrerequisites(false, true, false) assert.ErrorIs(t, err, ErrGetFundingDataRequired) } func TestLastUpdated(t *testing.T) { t.Parallel() p := &PositionController{} tm, err := p.LastUpdated() assert.NoError(t, err) if !tm.IsZero() { t.Errorf("received '%v' expected '%v", tm, time.Time{}) } p.updated = time.Now() tm, err = p.LastUpdated() assert.NoError(t, err) if !tm.Equal(p.updated) { t.Errorf("received '%v' expected '%v", tm, p.updated) } p = nil _, err = p.LastUpdated() assert.ErrorIs(t, err, common.ErrNilPointer) } func TestGetCurrencyForRealisedPNL(t *testing.T) { p := PNLCalculator{} code, a, err := p.GetCurrencyForRealisedPNL(asset.Spot, currency.NewPair(currency.DOGE, currency.XRP)) if err != nil { t.Error(err) } if !code.Equal(currency.DOGE) { t.Errorf("received '%v' expected '%v", code, currency.DOGE) } if a != asset.Spot { t.Errorf("received '%v' expected '%v", a, asset.Spot) } } func TestCheckTrackerPrerequisitesLowerExchange(t *testing.T) { t.Parallel() _, err := checkTrackerPrerequisitesLowerExchange("", asset.Spot, currency.EMPTYPAIR) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) upperExch := "IM UPPERCASE" _, err = checkTrackerPrerequisitesLowerExchange(upperExch, asset.Spot, currency.EMPTYPAIR) assert.ErrorIs(t, err, ErrNotFuturesAsset) _, err = checkTrackerPrerequisitesLowerExchange(upperExch, asset.Futures, currency.EMPTYPAIR) assert.ErrorIs(t, err, order.ErrPairIsEmpty) lowerExch, err := checkTrackerPrerequisitesLowerExchange(upperExch, asset.Futures, currency.NewBTCUSDT()) assert.NoError(t, err) if lowerExch != "im uppercase" { t.Error("expected lowercase") } }