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 }