code.vegaprotocol.io/vega@v0.79.0/datanode/sqlsubscribers/positions_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package sqlsubscribers_test
    17  
    18  // No race condition checks on these tests, the channels are buffered to avoid actual issues
    19  // we are aware that the tests themselves can be written in an unsafe way, but that's the tests
    20  // not the code itsel. The behaviour of the tests is 100% reliable.
    21  import (
    22  	"context"
    23  	"testing"
    24  
    25  	"code.vegaprotocol.io/vega/core/events"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/datanode/entities"
    28  	"code.vegaprotocol.io/vega/datanode/sqlsubscribers"
    29  	"code.vegaprotocol.io/vega/datanode/sqlsubscribers/mocks"
    30  	"code.vegaprotocol.io/vega/libs/num"
    31  
    32  	"github.com/golang/mock/gomock"
    33  	"github.com/stretchr/testify/assert"
    34  )
    35  
    36  type expect struct {
    37  	AverageEntryPrice *num.Uint
    38  	OpenVolume        int64
    39  	RealisedPNL       num.Decimal
    40  	UnrealisedPNL     num.Decimal
    41  }
    42  
    43  type positionEventBase interface {
    44  	events.Event
    45  	PartyID() string
    46  	MarketID() string
    47  	Timestamp() int64
    48  }
    49  
    50  type positionSettlement interface {
    51  	positionEventBase
    52  	Price() *num.Uint
    53  	PositionFactor() num.Decimal
    54  	Trades() []events.TradeSettlement
    55  }
    56  
    57  func TestPositionSpecSuite(t *testing.T) {
    58  	market := "market-id"
    59  	ctx := context.Background()
    60  	testcases := []struct {
    61  		run    string
    62  		pos    positionSettlement
    63  		expect expect
    64  	}{
    65  		{
    66  			run: "Long gets more long",
    67  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
    68  				tradeStub{
    69  					size:  100,
    70  					price: num.NewUint(50),
    71  				},
    72  				tradeStub{
    73  					size:  25,
    74  					price: num.NewUint(100),
    75  				},
    76  			}, 1, num.DecimalFromFloat(1)),
    77  			expect: expect{
    78  				AverageEntryPrice: num.NewUint(60),
    79  				OpenVolume:        125,
    80  				UnrealisedPNL:     num.NewDecimalFromFloat(5000.0),
    81  				RealisedPNL:       num.NewDecimalFromFloat(0.0),
    82  			},
    83  		},
    84  		{
    85  			run: "Long gets less long",
    86  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
    87  				tradeStub{
    88  					size:  100,
    89  					price: num.NewUint(50),
    90  				},
    91  				tradeStub{
    92  					size:  -25,
    93  					price: num.NewUint(100),
    94  				},
    95  			}, 1, num.DecimalFromFloat(1)),
    96  			expect: expect{
    97  				AverageEntryPrice: num.NewUint(50),
    98  				OpenVolume:        75,
    99  				UnrealisedPNL:     num.NewDecimalFromFloat(3750),
   100  				RealisedPNL:       num.NewDecimalFromFloat(1250),
   101  			},
   102  		},
   103  		{
   104  			run: "Long gets closed",
   105  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   106  				tradeStub{
   107  					size:  100,
   108  					price: num.NewUint(50),
   109  				},
   110  				tradeStub{
   111  					size:  -100,
   112  					price: num.NewUint(100),
   113  				},
   114  			}, 1, num.DecimalFromFloat(1)),
   115  			expect: expect{
   116  				OpenVolume:        0,
   117  				AverageEntryPrice: num.UintZero(),
   118  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   119  				RealisedPNL:       num.NewDecimalFromFloat(5000),
   120  			},
   121  		},
   122  		{
   123  			run: "Long gets turned short",
   124  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   125  				tradeStub{
   126  					size:  100,
   127  					price: num.NewUint(50),
   128  				},
   129  				tradeStub{
   130  					size:  -125,
   131  					price: num.NewUint(100),
   132  				},
   133  			}, 1, num.DecimalFromFloat(1)),
   134  			expect: expect{
   135  				OpenVolume:        -25,
   136  				AverageEntryPrice: num.NewUint(100),
   137  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   138  				RealisedPNL:       num.NewDecimalFromFloat(5000),
   139  			},
   140  		},
   141  		{
   142  			run: "Short gets more short",
   143  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   144  				tradeStub{
   145  					size:  -100,
   146  					price: num.NewUint(50),
   147  				},
   148  				tradeStub{
   149  					size:  -25,
   150  					price: num.NewUint(100),
   151  				},
   152  			}, 1, num.DecimalFromFloat(1)),
   153  			expect: expect{
   154  				OpenVolume:        -125,
   155  				AverageEntryPrice: num.NewUint(60),
   156  				UnrealisedPNL:     num.NewDecimalFromFloat(-5000),
   157  				RealisedPNL:       num.NewDecimalFromFloat(0),
   158  			},
   159  		},
   160  		{
   161  			run: "short gets less short",
   162  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   163  				tradeStub{
   164  					size:  -100,
   165  					price: num.NewUint(50),
   166  				},
   167  				tradeStub{
   168  					size:  25,
   169  					price: num.NewUint(100),
   170  				},
   171  			}, 1, num.DecimalFromFloat(1)),
   172  			expect: expect{
   173  				OpenVolume:        -75,
   174  				AverageEntryPrice: num.NewUint(50),
   175  				UnrealisedPNL:     num.NewDecimalFromFloat(-3750),
   176  				RealisedPNL:       num.NewDecimalFromFloat(-1250),
   177  			},
   178  		},
   179  		{
   180  			run: "Short gets closed",
   181  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   182  				tradeStub{
   183  					size:  -100,
   184  					price: num.NewUint(50),
   185  				},
   186  				tradeStub{
   187  					size:  100,
   188  					price: num.NewUint(100),
   189  				},
   190  			}, 1, num.DecimalFromFloat(1)),
   191  			expect: expect{
   192  				OpenVolume:        0,
   193  				AverageEntryPrice: num.UintZero(),
   194  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   195  				RealisedPNL:       num.NewDecimalFromFloat(-5000),
   196  			},
   197  		},
   198  		{
   199  			run: "Short gets turned long",
   200  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   201  				tradeStub{
   202  					size:  -100,
   203  					price: num.NewUint(50),
   204  				},
   205  				tradeStub{
   206  					size:  125,
   207  					price: num.NewUint(100),
   208  				},
   209  			}, 1, num.DecimalFromFloat(1)),
   210  			expect: expect{
   211  				OpenVolume:        25,
   212  				AverageEntryPrice: num.NewUint(100),
   213  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   214  				RealisedPNL:       num.NewDecimalFromFloat(-5000),
   215  			},
   216  		},
   217  		{
   218  			run: "Long trade up and down",
   219  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(75), []events.TradeSettlement{
   220  				tradeStub{
   221  					size:  100,
   222  					price: num.NewUint(100),
   223  				},
   224  				tradeStub{
   225  					size:  -25,
   226  					price: num.NewUint(25),
   227  				},
   228  				tradeStub{
   229  					size:  50,
   230  					price: num.NewUint(50),
   231  				},
   232  				tradeStub{
   233  					size:  -100,
   234  					price: num.NewUint(75),
   235  				},
   236  			}, 1, num.DecimalFromFloat(1)),
   237  			expect: expect{
   238  				OpenVolume:        25,
   239  				AverageEntryPrice: num.NewUint(80),
   240  				UnrealisedPNL:     num.NewDecimalFromFloat(-125),
   241  				RealisedPNL:       num.NewDecimalFromFloat(-2375),
   242  			},
   243  		},
   244  		{
   245  			run: "Profit before and after turning (start long)",
   246  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   247  				tradeStub{
   248  					size:  100,
   249  					price: num.NewUint(50),
   250  				},
   251  				tradeStub{
   252  					size:  -150,
   253  					price: num.NewUint(100),
   254  				},
   255  				tradeStub{
   256  					size:  50,
   257  					price: num.NewUint(25),
   258  				},
   259  			}, 1, num.DecimalFromFloat(1)),
   260  			expect: expect{
   261  				OpenVolume:        0,
   262  				AverageEntryPrice: num.UintZero(),
   263  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   264  				RealisedPNL:       num.NewDecimalFromFloat(8750),
   265  			},
   266  		},
   267  		{
   268  			run: "Profit before and after turning (start short)",
   269  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   270  				tradeStub{
   271  					size:  -100,
   272  					price: num.NewUint(100),
   273  				},
   274  				tradeStub{
   275  					size:  150,
   276  					price: num.NewUint(25),
   277  				},
   278  				tradeStub{
   279  					size:  -50,
   280  					price: num.NewUint(50),
   281  				},
   282  			}, 1, num.DecimalFromFloat(1)),
   283  			expect: expect{
   284  				OpenVolume:        0,
   285  				AverageEntryPrice: num.UintZero(),
   286  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   287  				RealisedPNL:       num.NewDecimalFromFloat(8750),
   288  			},
   289  		},
   290  		{
   291  			run: "Profit before and loss after turning (start long)",
   292  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   293  				tradeStub{
   294  					size:  100,
   295  					price: num.NewUint(50),
   296  				},
   297  				tradeStub{
   298  					size:  -150,
   299  					price: num.NewUint(100),
   300  				},
   301  				tradeStub{
   302  					size:  50,
   303  					price: num.NewUint(250),
   304  				},
   305  			}, 1, num.DecimalFromFloat(1)),
   306  			expect: expect{
   307  				OpenVolume:        0,
   308  				AverageEntryPrice: num.UintZero(),
   309  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   310  				RealisedPNL:       num.NewDecimalFromFloat(-2500),
   311  			},
   312  		},
   313  		{
   314  			run: "Profit before and loss after turning (start short)",
   315  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   316  				tradeStub{
   317  					size:  -100,
   318  					price: num.NewUint(100),
   319  				},
   320  				tradeStub{
   321  					size:  150,
   322  					price: num.NewUint(50),
   323  				},
   324  				tradeStub{
   325  					size:  -50,
   326  					price: num.NewUint(25),
   327  				},
   328  			}, 1, num.DecimalFromFloat(1)),
   329  			expect: expect{
   330  				OpenVolume:        0,
   331  				AverageEntryPrice: num.UintZero(),
   332  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   333  				RealisedPNL:       num.NewDecimalFromFloat(3750),
   334  			},
   335  		},
   336  		{
   337  			run: "Scenario from Tamlyn's spreadsheet on Google Drive at https://drive.google.com/open?id=1XJESwh5cypALqlYludWobAOEH1Pz-1xS",
   338  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(1010), []events.TradeSettlement{
   339  				tradeStub{
   340  					size:  5,
   341  					price: num.NewUint(1000),
   342  				},
   343  				tradeStub{
   344  					size:  2,
   345  					price: num.NewUint(1050),
   346  				},
   347  				tradeStub{
   348  					size:  -4,
   349  					price: num.NewUint(900),
   350  				},
   351  				tradeStub{
   352  					size:  -3,
   353  					price: num.NewUint(1070),
   354  				},
   355  				tradeStub{
   356  					size:  3,
   357  					price: num.NewUint(1060),
   358  				},
   359  				tradeStub{
   360  					size:  -5,
   361  					price: num.NewUint(1010),
   362  				},
   363  				tradeStub{
   364  					size:  -3,
   365  					price: num.NewUint(980),
   366  				},
   367  				tradeStub{
   368  					size:  2,
   369  					price: num.NewUint(1030),
   370  				},
   371  				tradeStub{
   372  					size:  3,
   373  					price: num.NewUint(982),
   374  				},
   375  				tradeStub{
   376  					size:  -4,
   377  					price: num.NewUint(1020),
   378  				},
   379  				tradeStub{
   380  					size:  6,
   381  					price: num.NewUint(1010),
   382  				},
   383  			}, 1, num.DecimalFromFloat(1)),
   384  			expect: expect{
   385  				OpenVolume:        2,
   386  				AverageEntryPrice: num.NewUint(1010),
   387  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   388  				RealisedPNL:       num.NewDecimalFromFloat(-446),
   389  			},
   390  		},
   391  		{
   392  			run: "Scenario from jeremy",
   393  			pos: events.NewSettlePositionEvent(ctx, "party1", market, num.NewUint(100), []events.TradeSettlement{
   394  				tradeStub{
   395  					size:  1,
   396  					price: num.NewUint(1931),
   397  				},
   398  				tradeStub{
   399  					size:  4,
   400  					price: num.NewUint(1931),
   401  				},
   402  				tradeStub{
   403  					size:  -1,
   404  					price: num.NewUint(1923),
   405  				},
   406  				tradeStub{
   407  					size:  -4,
   408  					price: num.NewUint(1923),
   409  				},
   410  				tradeStub{
   411  					size:  7,
   412  					price: num.NewUint(1927),
   413  				},
   414  				tradeStub{
   415  					size:  -2,
   416  					price: num.NewUint(1926),
   417  				},
   418  				tradeStub{
   419  					size:  -1,
   420  					price: num.NewUint(1926),
   421  				},
   422  				tradeStub{
   423  					size:  -4,
   424  					price: num.NewUint(1926),
   425  				},
   426  				tradeStub{
   427  					size:  1,
   428  					price: num.NewUint(1934),
   429  				},
   430  				tradeStub{
   431  					size:  7,
   432  					price: num.NewUint(1933),
   433  				},
   434  				tradeStub{
   435  					size:  1,
   436  					price: num.NewUint(1932),
   437  				},
   438  				tradeStub{
   439  					size:  1,
   440  					price: num.NewUint(1932),
   441  				},
   442  				tradeStub{
   443  					size:  -8,
   444  					price: num.NewUint(1926),
   445  				},
   446  				tradeStub{
   447  					size:  -2,
   448  					price: num.NewUint(1926),
   449  				},
   450  			}, 1, num.DecimalFromFloat(1)),
   451  			expect: expect{
   452  				OpenVolume:        0,
   453  				AverageEntryPrice: num.UintZero(),
   454  				UnrealisedPNL:     num.NewDecimalFromFloat(0),
   455  				RealisedPNL:       num.NewDecimalFromFloat(-116),
   456  			},
   457  		},
   458  	}
   459  
   460  	for _, tc := range testcases {
   461  		t.Run(tc.run, func(t *testing.T) {
   462  			ps := tc.pos
   463  			sub, store := getSubscriberAndStore(t)
   464  			sub.Push(context.Background(), ps)
   465  			pp, err := store.GetByMarket(ctx, market)
   466  			assert.NoError(t, err)
   467  			assert.NotZero(t, len(pp))
   468  			// average entry price should be 1k
   469  			assert.Equal(t, tc.expect.AverageEntryPrice, pp[0].AverageEntryPriceUint(), "invalid average entry price")
   470  			assert.Equal(t, tc.expect.OpenVolume, pp[0].OpenVolume, "invalid open volume")
   471  			assert.Equal(t, tc.expect.UnrealisedPNL.String(), pp[0].UnrealisedPnl.Round(0).String(), "invalid unrealised pnl")
   472  			assert.Equal(t, tc.expect.RealisedPNL.String(), pp[0].RealisedPnl.Round(0).String(), "invalid realised pnl")
   473  		})
   474  	}
   475  }
   476  
   477  func getSubscriberAndStore(t *testing.T) (*sqlsubscribers.Position, sqlsubscribers.PositionStore) {
   478  	t.Helper()
   479  	ctrl := gomock.NewController(t)
   480  
   481  	store := mocks.NewMockPositionStore(ctrl)
   482  	mkt := mocks.NewMockMarketSvc(ctrl)
   483  
   484  	var lastPos entities.Position
   485  	recordPos := func(_ context.Context, pos entities.Position) error {
   486  		lastPos = pos
   487  		return nil
   488  	}
   489  
   490  	getByMarket := func(_ context.Context, _ string) ([]entities.Position, error) {
   491  		return []entities.Position{lastPos}, nil
   492  	}
   493  
   494  	getByMarketAndParty := func(_ context.Context, _ string, _ string) (entities.Position, error) {
   495  		return lastPos, nil
   496  	}
   497  
   498  	store.EXPECT().Add(gomock.Any(), gomock.Any()).DoAndReturn(recordPos)
   499  	store.EXPECT().GetByMarket(gomock.Any(), gomock.Any()).DoAndReturn(getByMarket)
   500  	store.EXPECT().GetByMarketAndParty(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(getByMarketAndParty)
   501  	mkt.EXPECT().GetMarketScalingFactor(gomock.Any(), gomock.Any()).AnyTimes().Return(num.DecimalFromInt64(1), true)
   502  
   503  	p := sqlsubscribers.NewPosition(store, mkt)
   504  	return p, store
   505  }
   506  
   507  func TestPositionsForSpots(t *testing.T) {
   508  	t.Helper()
   509  	ctrl := gomock.NewController(t)
   510  
   511  	store := mocks.NewMockPositionStore(ctrl)
   512  	mkt := mocks.NewMockMarketSvc(ctrl)
   513  	mkt.EXPECT().IsSpotMarket(gomock.Any(), gomock.Any()).Times(1).Return(true)
   514  	var lastPos *entities.Position
   515  
   516  	getByMarket := func(_ context.Context, _ string) ([]entities.Position, error) {
   517  		if lastPos == nil {
   518  			return nil, nil
   519  		}
   520  		return []entities.Position{*lastPos}, nil
   521  	}
   522  
   523  	store.EXPECT().GetByMarket(gomock.Any(), gomock.Any()).DoAndReturn(getByMarket).AnyTimes()
   524  	p := sqlsubscribers.NewPosition(store, mkt)
   525  	// spot trade, expect no position
   526  	tradeEvent := events.NewTradeEvent(context.Background(), types.Trade{MarketID: "1", MarketPrice: num.NewUint(100), Price: num.NewUint(1000)})
   527  	p.Push(context.Background(), tradeEvent)
   528  	pp, err := store.GetByMarket(context.Background(), "1")
   529  	assert.NoError(t, err)
   530  	assert.Zero(t, len(pp))
   531  
   532  	// futures trade, expect position
   533  	recordPos := func(_ context.Context, pos entities.Position) error {
   534  		lastPos = &pos
   535  		return nil
   536  	}
   537  	getByMarketAndParty := func(_ context.Context, _ string, _ string) (*entities.Position, error) {
   538  		return lastPos, nil
   539  	}
   540  	store.EXPECT().Add(gomock.Any(), gomock.Any()).DoAndReturn(recordPos).AnyTimes()
   541  	mkt.EXPECT().GetMarketScalingFactor(gomock.Any(), gomock.Any()).AnyTimes().Return(num.DecimalFromInt64(1), true)
   542  	store.EXPECT().GetByMarketAndParty(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(getByMarketAndParty).AnyTimes()
   543  	mkt.EXPECT().IsSpotMarket(gomock.Any(), gomock.Any()).Times(1).Return(false)
   544  	tradeEvent = events.NewTradeEvent(context.Background(), types.Trade{MarketID: "2", MarketPrice: num.NewUint(100), Price: num.NewUint(1000)})
   545  	p.Push(context.Background(), tradeEvent)
   546  	pp, err = store.GetByMarket(context.Background(), "2")
   547  	assert.NoError(t, err)
   548  	assert.NotZero(t, len(pp))
   549  }
   550  
   551  type tradeStub struct {
   552  	size  int64
   553  	price *num.Uint
   554  }
   555  
   556  func (t tradeStub) Size() int64 {
   557  	return t.size
   558  }
   559  
   560  func (t tradeStub) Price() *num.Uint {
   561  	return t.price.Clone()
   562  }
   563  
   564  func (t tradeStub) MarketPrice() *num.Uint {
   565  	return t.price.Clone()
   566  }