code.vegaprotocol.io/vega@v0.79.0/core/settlement/engine_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 settlement_test 17 18 import ( 19 "bytes" 20 "context" 21 "errors" 22 "sync" 23 "testing" 24 "time" 25 26 bmocks "code.vegaprotocol.io/vega/core/broker/mocks" 27 "code.vegaprotocol.io/vega/core/events" 28 "code.vegaprotocol.io/vega/core/settlement" 29 "code.vegaprotocol.io/vega/core/settlement/mocks" 30 "code.vegaprotocol.io/vega/core/types" 31 "code.vegaprotocol.io/vega/libs/num" 32 "code.vegaprotocol.io/vega/libs/proto" 33 "code.vegaprotocol.io/vega/logging" 34 snapshot "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 35 36 "github.com/golang/mock/gomock" 37 "github.com/stretchr/testify/assert" 38 "github.com/stretchr/testify/require" 39 ) 40 41 type testEngine struct { 42 *settlement.SnapshotEngine 43 ctrl *gomock.Controller 44 prod *mocks.MockProduct 45 positions []*mocks.MockMarketPosition 46 tsvc *mocks.MockTimeService 47 broker *bmocks.MockBroker 48 market string 49 } 50 51 type posValue struct { 52 party string 53 price *num.Uint // absolute Mark price 54 size int64 55 } 56 57 type marginVal struct { 58 events.MarketPosition 59 asset, marketID string 60 margin, orderMargin, general, marginShortFall uint64 61 } 62 63 func TestMarketExpiry(t *testing.T) { 64 t.Run("Settle at market expiry - success", testSettleExpiredSuccess) 65 t.Run("Settle at market expiry - error", testSettleExpiryFail) 66 t.Run("Settle at market expiry - rounding", testSettleRoundingSuccess) 67 } 68 69 func TestMarkToMarket(t *testing.T) { 70 t.Run("No settle positions if none were on channel", testMarkToMarketEmpty) 71 t.Run("Settle positions are pushed onto the slice channel in order", testMarkToMarketOrdered) 72 t.Run("Trade adds new party to market, no MTM settlement because markPrice is the same", testAddNewParty) 73 // add this test case because we had a runtime panic on the trades map earlier 74 t.Run("Trade adds new party, immediately closing out with themselves", testAddNewPartySelfTrade) 75 t.Run("Test MTM settle when the network is closed out", testMTMNetworkZero) 76 t.Run("Test settling a funding period", testSettlingAFundingPeriod) 77 t.Run("Test settling a funding period with rounding error", testSettlingAFundingPeriodRoundingError) 78 t.Run("Test settling a funding period with small win amounts (zero transfers)", testSettlingAFundingPeriodExcessSmallLoss) 79 t.Run("Test settling a funding period with rounding error (single party rounding)", testSettlingAFundingPeriodExcessLoss) 80 t.Run("Test settling a funding period with zero win amounts (loss exceeds 1)", testSettlingAFundingPeriodExcessLostExceedsOneLoss) 81 } 82 83 func TestMTMWinDistribution(t *testing.T) { 84 t.Run("A MTM loss party with a loss of value 1, with several parties needing a win", testMTMWinOneExcess) 85 t.Run("Distribute win excess in a scenario where no transfer amount is < 1", testMTMWinNoZero) 86 t.Run("Distribute loss excess in a scenario where no transfer amount is < 1", testMTMWinWithZero) 87 t.Run("A MTM case where win total > loss total", testMTMWinGTLoss) 88 } 89 90 func testSettlingAFundingPeriod(t *testing.T) { 91 engine := getTestEngine(t) 92 defer engine.Finish() 93 ctx := context.Background() 94 95 testPositions := []testPos{ 96 { 97 party: "party1", 98 size: 10, 99 }, 100 { 101 party: "party2", 102 size: -10, 103 }, 104 { 105 party: "party3", 106 size: 0, 107 }, 108 } 109 positions := make([]events.MarketPosition, 0, len(testPositions)) 110 for _, p := range testPositions { 111 positions = append(positions, p) 112 } 113 114 // 0 funding paymenet produces 0 transfers 115 transfers, round := engine.SettleFundingPeriod(ctx, positions, num.IntZero()) 116 assert.Len(t, transfers, 0) 117 assert.Nil(t, round) 118 119 // no positions produces no transfers 120 121 // positive funding payement, shorts pay long 122 fundingPayment, _ := num.IntFromString("10", 10) 123 transfers, round = engine.SettleFundingPeriod(ctx, positions, fundingPayment) 124 require.Len(t, transfers, 2) 125 require.True(t, round.IsZero()) 126 assert.Equal(t, "100", transfers[0].Transfer().Amount.Amount.String()) 127 assert.Equal(t, types.TransferTypePerpFundingLoss, transfers[0].Transfer().Type) 128 assert.Equal(t, "100", transfers[1].Transfer().Amount.Amount.String()) 129 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[1].Transfer().Type) 130 131 // negative funding payement, long pays short, also expect loss to come before win 132 fundingPayment, _ = num.IntFromString("-10", 10) 133 transfers, round = engine.SettleFundingPeriod(ctx, positions, fundingPayment) 134 require.True(t, round.IsZero()) 135 require.Len(t, transfers, 2) 136 assert.Equal(t, "100", transfers[0].Transfer().Amount.Amount.String()) 137 assert.Equal(t, types.TransferTypePerpFundingLoss, transfers[0].Transfer().Type) 138 assert.Equal(t, "100", transfers[1].Transfer().Amount.Amount.String()) 139 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[1].Transfer().Type) 140 141 // no positions produces no transfers 142 transfers, round = engine.SettleFundingPeriod(ctx, []events.MarketPosition{}, fundingPayment) 143 require.Nil(t, round) 144 assert.Len(t, transfers, 0) 145 } 146 147 func testSettlingAFundingPeriodRoundingError(t *testing.T) { 148 engine := getTestEngineWithFactor(t, 100) 149 defer engine.Finish() 150 ctx := context.Background() 151 152 testPositions := []testPos{ 153 { 154 party: "party1", 155 size: 1000010, 156 }, 157 { 158 party: "party3", 159 size: -1000005, 160 }, 161 { 162 party: "party4", 163 size: -1000005, 164 }, 165 } 166 167 positions := make([]events.MarketPosition, 0, len(testPositions)) 168 for _, p := range testPositions { 169 positions = append(positions, p) 170 } 171 172 fundingPayment, _ := num.IntFromString("10", 10) 173 transfers, round := engine.SettleFundingPeriod(ctx, positions, fundingPayment) 174 require.Len(t, transfers, 3) 175 assert.Equal(t, "100001", transfers[0].Transfer().Amount.Amount.String()) 176 assert.Equal(t, types.TransferTypePerpFundingLoss, transfers[0].Transfer().Type) 177 assert.Equal(t, "100000", transfers[1].Transfer().Amount.Amount.String()) 178 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[1].Transfer().Type) 179 assert.Equal(t, "100000", transfers[2].Transfer().Amount.Amount.String()) 180 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[2].Transfer().Type) 181 182 // here 100001 will be sent to the settlement account, but only 200000 we be paid out due to rounding, 183 // so we expect a remainder of 1 184 require.Equal(t, "1", round.String()) 185 } 186 187 func testSettlingAFundingPeriodExcessLostExceedsOneLoss(t *testing.T) { 188 engine := getTestEngineWithFactor(t, 100) 189 defer engine.Finish() 190 ctx := context.Background() 191 192 testPositions := []testPos{ 193 { 194 party: "party1", 195 size: 23, 196 }, 197 { 198 party: "party3", 199 size: -3, 200 }, 201 { 202 party: "party4", 203 size: -5, 204 }, 205 { 206 party: "party5", 207 size: -5, 208 }, 209 { 210 party: "party6", 211 size: -5, 212 }, 213 { 214 party: "party7", 215 size: -5, 216 }, 217 } 218 219 positions := make([]events.MarketPosition, 0, len(testPositions)) 220 for _, p := range testPositions { 221 positions = append(positions, p) 222 } 223 224 fundingPayment, _ := num.IntFromString("10", 10) 225 transfers, round := engine.SettleFundingPeriod(ctx, positions, fundingPayment) 226 require.Len(t, transfers, 6) 227 assert.Equal(t, "2", transfers[0].Transfer().Amount.Amount.String()) 228 assert.Equal(t, types.TransferTypePerpFundingLoss, transfers[0].Transfer().Type) 229 assert.Equal(t, "0", transfers[1].Transfer().Amount.Amount.String()) 230 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[1].Transfer().Type) 231 assert.Equal(t, "0", transfers[2].Transfer().Amount.Amount.String()) 232 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[2].Transfer().Type) 233 assert.Equal(t, "0", transfers[3].Transfer().Amount.Amount.String()) 234 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[3].Transfer().Type) 235 assert.Equal(t, "0", transfers[4].Transfer().Amount.Amount.String()) 236 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[4].Transfer().Type) 237 assert.Equal(t, "0", transfers[5].Transfer().Amount.Amount.String()) 238 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[5].Transfer().Type) 239 240 require.Equal(t, "2", round.String()) 241 } 242 243 func testSettlingAFundingPeriodExcessSmallLoss(t *testing.T) { 244 engine := getTestEngineWithFactor(t, 100) 245 defer engine.Finish() 246 ctx := context.Background() 247 248 testPositions := []testPos{ 249 { 250 party: "party1", 251 size: 11, 252 }, 253 { 254 party: "party3", 255 size: -3, 256 }, 257 { 258 party: "party4", 259 size: -3, 260 }, 261 { 262 party: "party5", 263 size: -5, 264 }, 265 } 266 267 positions := make([]events.MarketPosition, 0, len(testPositions)) 268 for _, p := range testPositions { 269 positions = append(positions, p) 270 } 271 272 fundingPayment, _ := num.IntFromString("10", 10) 273 transfers, round := engine.SettleFundingPeriod(ctx, positions, fundingPayment) 274 require.Len(t, transfers, 4) 275 assert.Equal(t, "1", transfers[0].Transfer().Amount.Amount.String()) 276 assert.Equal(t, types.TransferTypePerpFundingLoss, transfers[0].Transfer().Type) 277 assert.Equal(t, "0", transfers[1].Transfer().Amount.Amount.String()) 278 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[1].Transfer().Type) 279 assert.Equal(t, "0", transfers[2].Transfer().Amount.Amount.String()) 280 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[2].Transfer().Type) 281 assert.Equal(t, "0", transfers[3].Transfer().Amount.Amount.String()) 282 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[3].Transfer().Type) 283 284 require.Equal(t, "1", round.String()) 285 } 286 287 func testSettlingAFundingPeriodExcessLoss(t *testing.T) { 288 engine := getTestEngineWithFactor(t, 100) 289 defer engine.Finish() 290 ctx := context.Background() 291 292 testPositions := []testPos{ 293 { 294 party: "party1", 295 size: 1000010, 296 }, 297 { 298 party: "party3", 299 size: -1000005, 300 }, 301 } 302 303 positions := make([]events.MarketPosition, 0, len(testPositions)) 304 for _, p := range testPositions { 305 positions = append(positions, p) 306 } 307 308 fundingPayment, _ := num.IntFromString("10", 10) 309 transfers, round := engine.SettleFundingPeriod(ctx, positions, fundingPayment) 310 require.Len(t, transfers, 2) 311 assert.Equal(t, "100001", transfers[0].Transfer().Amount.Amount.String()) 312 assert.Equal(t, types.TransferTypePerpFundingLoss, transfers[0].Transfer().Type) 313 assert.Equal(t, "100000", transfers[1].Transfer().Amount.Amount.String()) 314 assert.Equal(t, types.TransferTypePerpFundingWin, transfers[1].Transfer().Type) 315 316 // here 100001 will be sent to the settlement account, but only 200000 we be paid out due to rounding, 317 // so we expect a remainder of 1 318 require.Equal(t, "1", round.String()) 319 } 320 321 func testMTMWinNoZero(t *testing.T) { 322 // cheat by setting the factor to some specific value, makes it easier to create a scenario where win/loss amounts don't match 323 engine := getTestEngineWithFactor(t, 1) 324 defer engine.Finish() 325 326 price := num.NewUint(100000) 327 one := num.NewUint(1) 328 ctx := context.Background() 329 330 initPos := []testPos{ 331 { 332 price: price.Clone(), 333 party: "party1", 334 size: 10, 335 }, 336 { 337 price: price.Clone(), 338 party: "party2", 339 size: 23, 340 }, 341 { 342 price: price.Clone(), 343 party: "party3", 344 size: -32, 345 }, 346 { 347 price: price.Clone(), 348 party: "party4", 349 size: 1, 350 }, 351 { 352 price: price.Clone(), 353 party: "party5", 354 size: -29, 355 }, 356 { 357 price: price.Clone(), 358 party: "party6", 359 size: 27, 360 }, 361 } 362 363 init := make([]events.MarketPosition, 0, len(initPos)) 364 for _, p := range initPos { 365 init = append(init, p) 366 } 367 368 newPrice := num.Sum(price, one, one, one) 369 somePrice := num.Sum(price, one) 370 newParty := testPos{ 371 size: 30, 372 price: newPrice.Clone(), 373 party: "party4", 374 } 375 376 trades := []*types.Trade{ 377 { 378 Size: 10, 379 Buyer: newParty.party, 380 Seller: initPos[0].party, 381 Price: somePrice.Clone(), 382 }, 383 { 384 Size: 10, 385 Buyer: newParty.party, 386 Seller: initPos[1].party, 387 Price: somePrice.Clone(), 388 }, 389 { 390 Size: 10, 391 Buyer: newParty.party, 392 Seller: initPos[2].party, 393 Price: newPrice.Clone(), 394 }, 395 } 396 updates := make([]events.MarketPosition, 0, len(initPos)+2) 397 for _, trade := range trades { 398 for i, p := range initPos { 399 if p.party == trade.Seller { 400 p.size -= int64(trade.Size) 401 } 402 p.price = trade.Price.Clone() 403 initPos[i] = p 404 } 405 } 406 for _, p := range initPos { 407 updates = append(updates, p) 408 } 409 updates = append(updates, newParty) 410 engine.Update(init) 411 for _, trade := range trades { 412 engine.AddTrade(trade) 413 } 414 transfers := engine.SettleMTM(ctx, newPrice.Clone(), updates) 415 require.NotEmpty(t, transfers) 416 } 417 418 func testMTMWinOneExcess(t *testing.T) { 419 engine := getTestEngineWithFactor(t, 1) 420 defer engine.Finish() 421 422 price := num.NewUint(10000) 423 one := num.NewUint(1) 424 ctx := context.Background() 425 426 initPos := []testPos{ 427 { 428 price: price.Clone(), 429 party: "party1", 430 size: 10, 431 }, 432 { 433 price: price.Clone(), 434 party: "party2", 435 size: 20, 436 }, 437 { 438 price: price.Clone(), 439 party: "party3", 440 size: -29, 441 }, 442 { 443 price: price.Clone(), 444 party: "party4", 445 size: 1, 446 }, 447 { 448 price: price.Clone(), 449 party: "party5", 450 size: -1, 451 }, 452 { 453 price: price.Clone(), 454 party: "party5", 455 size: 1, 456 }, 457 } 458 459 init := make([]events.MarketPosition, 0, len(initPos)) 460 for _, p := range initPos { 461 init = append(init, p) 462 } 463 464 newPrice := num.Sum(price, one) 465 newParty := testPos{ 466 size: 30, 467 price: newPrice.Clone(), 468 party: "party4", 469 } 470 471 trades := []*types.Trade{ 472 { 473 Size: 10, 474 Buyer: newParty.party, 475 Seller: initPos[0].party, 476 Price: newPrice.Clone(), 477 }, 478 { 479 Size: 10, 480 Buyer: newParty.party, 481 Seller: initPos[1].party, 482 Price: newPrice.Clone(), 483 }, 484 { 485 Size: 10, 486 Buyer: newParty.party, 487 Seller: initPos[2].party, 488 Price: newPrice.Clone(), 489 }, 490 } 491 updates := make([]events.MarketPosition, 0, len(initPos)+2) 492 for _, trade := range trades { 493 for i, p := range initPos { 494 if p.party == trade.Seller { 495 p.size -= int64(trade.Size) 496 } 497 p.price = trade.Price.Clone() 498 initPos[i] = p 499 } 500 } 501 for _, p := range initPos { 502 updates = append(updates, p) 503 } 504 updates = append(updates, newParty) 505 engine.Update(init) 506 for _, trade := range trades { 507 engine.AddTrade(trade) 508 } 509 transfers := engine.SettleMTM(ctx, newPrice.Clone(), updates) 510 require.NotEmpty(t, transfers) 511 } 512 513 func testSettleRoundingSuccess(t *testing.T) { 514 engine := getTestEngineWithFactor(t, 10) 515 defer engine.Finish() 516 // these are mark prices, product will provide the actual value 517 // total wins = 554, total losses = 555 518 pr := num.NewUint(1000) 519 data := []posValue{ 520 { 521 party: "party1", 522 price: pr, // winning 523 size: 1101, // 5 * 110.1 = 550.5 -> 550 524 }, 525 { 526 party: "party2", 527 price: pr, // losing 528 size: -550, // 5 * 55 = 275 529 }, 530 { 531 party: "party3", 532 price: pr, // losing 533 size: -560, // 5 * 56 = 280 534 }, 535 { 536 party: "party4", 537 price: pr, // winning 538 size: 9, // 5 * .9 = 4.5-> 4 539 }, 540 } 541 expect := []*types.Transfer{ 542 { 543 Owner: data[1].party, 544 Amount: &types.FinancialAmount{ 545 Amount: num.NewUint(275), 546 }, 547 Type: types.TransferTypeLoss, 548 }, 549 { 550 Owner: data[2].party, 551 Amount: &types.FinancialAmount{ 552 Amount: num.NewUint(280), 553 }, 554 Type: types.TransferTypeLoss, 555 }, 556 { 557 Owner: data[0].party, 558 Amount: &types.FinancialAmount{ 559 Amount: num.NewUint(550), 560 }, 561 Type: types.TransferTypeWin, 562 }, 563 { 564 Owner: data[3].party, 565 Amount: &types.FinancialAmount{ 566 Amount: num.NewUint(4), 567 }, 568 Type: types.TransferTypeWin, 569 }, 570 } 571 oraclePrice := num.NewUint(1005) 572 settleF := func(price *num.Uint, settlementData *num.Uint, size num.Decimal) (*types.FinancialAmount, bool, num.Decimal, error) { 573 amt, neg := num.UintZero().Delta(oraclePrice, price) 574 if size.IsNegative() { 575 size = size.Neg() 576 neg = !neg 577 } 578 579 amount, rem := num.UintFromDecimalWithFraction(amt.ToDecimal().Mul(size)) 580 return &types.FinancialAmount{ 581 Amount: amount, 582 }, neg, rem, nil 583 } 584 positions := engine.getExpiryPositions(data...) 585 // we expect settle calls for each position 586 engine.prod.EXPECT().Settle(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(settleF).AnyTimes() 587 // ensure positions are set 588 engine.Update(positions) 589 // now settle: 590 got, round, err := engine.Settle(time.Now(), oraclePrice) 591 assert.NoError(t, err) 592 assert.Equal(t, len(expect), len(got)) 593 assert.True(t, round.EQ(num.NewUint(1))) 594 for i, p := range got { 595 e := expect[i] 596 assert.Equal(t, e.Type, p.Type) 597 assert.Equal(t, e.Amount.Amount, p.Amount.Amount) 598 } 599 } 600 601 func testSettleExpiredSuccess(t *testing.T) { 602 engine := getTestEngine(t) 603 defer engine.Finish() 604 // these are mark prices, product will provide the actual value 605 pr := num.NewUint(1000) 606 data := []posValue{ // {{{2 607 { 608 party: "party1", 609 price: pr, // winning 610 size: 10, 611 }, 612 { 613 party: "party2", 614 price: pr, // losing 615 size: -5, 616 }, 617 { 618 party: "party3", 619 price: pr, // losing 620 size: -5, 621 }, 622 } 623 half := num.NewUint(500) 624 expect := []*types.Transfer{ 625 { 626 Owner: data[1].party, 627 Amount: &types.FinancialAmount{ 628 Amount: half, 629 }, 630 Type: types.TransferTypeLoss, 631 }, 632 { 633 Owner: data[2].party, 634 Amount: &types.FinancialAmount{ 635 Amount: half, 636 }, 637 Type: types.TransferTypeLoss, 638 }, 639 { 640 Owner: data[0].party, 641 Amount: &types.FinancialAmount{ 642 Amount: pr, 643 }, 644 Type: types.TransferTypeWin, 645 }, 646 } // }}} 647 oraclePrice := num.NewUint(1100) 648 settleF := func(price *num.Uint, settlementData *num.Uint, size num.Decimal) (*types.FinancialAmount, bool, num.Decimal, error) { 649 amt, neg := num.UintZero().Delta(oraclePrice, price) 650 if size.IsNegative() { 651 size = size.Neg() 652 neg = !neg 653 } 654 655 amount, rem := num.UintFromDecimalWithFraction(amt.ToDecimal().Mul(size)) 656 return &types.FinancialAmount{ 657 Amount: amount, 658 }, neg, rem, nil 659 } 660 positions := engine.getExpiryPositions(data...) 661 // we expect settle calls for each position 662 engine.prod.EXPECT().Settle(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(settleF).AnyTimes() 663 // ensure positions are set 664 engine.Update(positions) 665 // now settle: 666 got, _, err := engine.Settle(time.Now(), oraclePrice) 667 assert.NoError(t, err) 668 assert.Equal(t, len(expect), len(got)) 669 for i, p := range got { 670 e := expect[i] 671 assert.Equal(t, e.Type, p.Type) 672 assert.Equal(t, e.Amount.Amount, p.Amount.Amount) 673 } 674 } 675 676 func testSettleExpiryFail(t *testing.T) { 677 engine := getTestEngine(t) 678 defer engine.Finish() 679 // these are mark prices, product will provide the actual value 680 data := []posValue{ 681 { 682 party: "party1", 683 price: num.NewUint(1000), 684 size: 10, 685 }, 686 } 687 errExp := errors.New("product.Settle error") 688 positions := engine.getExpiryPositions(data...) 689 engine.prod.EXPECT().Settle(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil, false, num.DecimalZero(), errExp) 690 engine.Update(positions) 691 empty, _, err := engine.Settle(time.Now(), num.UintZero()) 692 assert.Empty(t, empty) 693 assert.Error(t, err) 694 assert.Equal(t, errExp, err) 695 } 696 697 func testMarkToMarketEmpty(t *testing.T) { 698 markPrice := num.NewUint(10000) 699 // there's only 1 trade to test here 700 data := posValue{ 701 price: markPrice, 702 size: 1, 703 party: "test", 704 } 705 engine := getTestEngine(t) 706 defer engine.Finish() 707 pos := mocks.NewMockMarketPosition(engine.ctrl) 708 pos.EXPECT().Party().AnyTimes().Return(data.party) 709 pos.EXPECT().Size().AnyTimes().Return(data.size) 710 pos.EXPECT().Price().AnyTimes().Return(markPrice) 711 engine.Update([]events.MarketPosition{pos}) 712 result := engine.SettleMTM(context.Background(), markPrice, []events.MarketPosition{pos}) 713 assert.Empty(t, result) 714 } 715 716 func testAddNewPartySelfTrade(t *testing.T) { 717 engine := getTestEngine(t) 718 defer engine.Finish() 719 markPrice := num.NewUint(1000) 720 t1 := testPos{ 721 price: markPrice.Clone(), 722 party: "party1", 723 size: 5, 724 } 725 init := []events.MarketPosition{ 726 t1, 727 testPos{ 728 price: markPrice.Clone(), 729 party: "party2", 730 size: -5, 731 }, 732 } 733 // let's not change the markPrice 734 // just add a party to the market, buying from an existing party 735 trade := &types.Trade{ 736 Buyer: "party3", 737 Seller: "party3", 738 Price: markPrice.Clone(), 739 Size: 1, 740 } 741 // the first party is the seller 742 // so these are the new positions after the trade 743 t1.size-- 744 positions := []events.MarketPosition{ 745 t1, 746 init[1], 747 testPos{ 748 party: "party3", 749 size: 0, 750 price: markPrice.Clone(), 751 }, 752 } 753 engine.Update(init) 754 engine.AddTrade(trade) 755 noTransfers := engine.SettleMTM(context.Background(), markPrice, positions) 756 assert.Len(t, noTransfers, 1) 757 assert.Nil(t, noTransfers[0].Transfer()) 758 } 759 760 func TestNetworkPartyCloseout(t *testing.T) { 761 engine := getTestEngine(t) 762 currentMP := num.NewUint(1000) 763 // network trade 764 nTrade := &types.Trade{ 765 Buyer: types.NetworkParty, 766 Seller: types.NetworkParty, 767 Price: currentMP.Clone(), 768 MarketPrice: currentMP.Clone(), 769 Size: 10, 770 } 771 nPosition := testPos{ 772 party: types.NetworkParty, 773 size: 10, 774 price: currentMP.Clone(), 775 } 776 sellPrice := num.NewUint(990) 777 // now trade with health party, at some different price 778 // trigger a loss for the network. 779 cTrade := &types.Trade{ 780 Buyer: "party1", 781 Seller: types.NetworkParty, 782 Size: 2, 783 Price: sellPrice.Clone(), 784 } 785 init := []events.MarketPosition{ 786 nPosition, 787 testPos{ 788 party: "party1", 789 size: 0, 790 price: currentMP.Clone(), 791 }, 792 } 793 positions := []events.MarketPosition{ 794 testPos{ 795 party: types.NetworkParty, 796 size: 8, 797 price: currentMP.Clone(), 798 }, 799 testPos{ 800 party: "party1", 801 size: 2, 802 price: currentMP.Clone(), 803 }, 804 } 805 engine.Update(init) 806 engine.AddTrade(nTrade) 807 engine.AddTrade(cTrade) 808 transfers := engine.SettleMTM(context.Background(), currentMP, positions) 809 assert.NotEmpty(t, transfers) 810 assert.Len(t, transfers, 2) 811 } 812 813 func TestNetworkCloseoutZero(t *testing.T) { 814 engine := getTestEngine(t) 815 currentMP := num.NewUint(1000) 816 // network trade 817 nTrade := &types.Trade{ 818 Buyer: types.NetworkParty, 819 Seller: types.NetworkParty, 820 Price: currentMP.Clone(), 821 MarketPrice: currentMP.Clone(), 822 Size: 10, 823 } 824 nPosition := testPos{ 825 party: types.NetworkParty, 826 size: 10, 827 price: currentMP.Clone(), 828 } 829 sellPrice := num.NewUint(999) 830 // now trade with health party, at some different price 831 // trigger a loss for the network. 832 cTrades := []*types.Trade{ 833 { 834 Buyer: "party1", 835 Seller: types.NetworkParty, 836 Size: 1, 837 Price: sellPrice.Clone(), 838 }, 839 { 840 Buyer: "party2", 841 Seller: types.NetworkParty, 842 Size: 1, 843 Price: sellPrice.Clone(), 844 }, 845 } 846 init := []events.MarketPosition{ 847 nPosition, 848 testPos{ 849 party: "party1", 850 size: 0, 851 price: currentMP.Clone(), 852 }, 853 testPos{ 854 party: "party2", 855 size: 0, 856 price: currentMP.Clone(), 857 }, 858 testPos{ 859 party: "party3", 860 size: -5, 861 price: currentMP.Clone(), 862 }, 863 testPos{ 864 party: "party4", 865 size: -5, 866 price: currentMP.Clone(), 867 }, 868 } 869 positions := []events.MarketPosition{ 870 testPos{ 871 party: types.NetworkParty, 872 size: 8, 873 price: currentMP.Clone(), 874 }, 875 testPos{ 876 party: "party1", 877 size: 1, 878 price: currentMP.Clone(), 879 }, 880 testPos{ 881 party: "party2", 882 size: 1, 883 price: currentMP.Clone(), 884 }, 885 testPos{ 886 party: "party3", 887 size: -5, 888 price: currentMP.Clone(), 889 }, 890 testPos{ 891 party: "party4", 892 size: -5, 893 price: currentMP.Clone(), 894 }, 895 } 896 engine.Update(init) 897 engine.AddTrade(nTrade) 898 for _, cTrade := range cTrades { 899 engine.AddTrade(cTrade) 900 } 901 transfers := engine.SettleMTM(context.Background(), currentMP, positions) 902 assert.NotEmpty(t, transfers) 903 // now that the network has an established long position, make short positions close out and mark to market 904 // party 3 closes their position, lowering the mark price 905 newMP := num.NewUint(990) 906 trade := &types.Trade{ 907 Buyer: "party3", 908 Seller: "party1", 909 Price: newMP, 910 Size: 1, 911 } 912 positions = []events.MarketPosition{ 913 testPos{ 914 party: types.NetworkParty, 915 size: 8, 916 price: newMP.Clone(), 917 }, 918 testPos{ 919 party: "party1", 920 size: 0, 921 price: newMP.Clone(), 922 }, 923 testPos{ 924 party: "party2", 925 size: 1, 926 price: newMP.Clone(), 927 }, 928 testPos{ 929 party: "party3", 930 size: -4, 931 price: newMP.Clone(), 932 }, 933 testPos{ 934 party: "party4", 935 size: -5, 936 price: newMP.Clone(), 937 }, 938 } 939 engine.AddTrade(trade) 940 transfers = engine.SettleMTM(context.Background(), newMP, positions) 941 assert.NotEmpty(t, transfers) 942 // now make it look like party2 got distressed because of this MTM settlement 943 nTrade = &types.Trade{ 944 Price: newMP.Clone(), 945 MarketPrice: newMP.Clone(), 946 Size: 1, 947 Buyer: types.NetworkParty, 948 Seller: types.NetworkParty, 949 } 950 positions = []events.MarketPosition{ 951 testPos{ 952 party: types.NetworkParty, 953 size: 9, 954 price: newMP.Clone(), 955 }, 956 testPos{ 957 party: "party3", 958 size: -4, 959 price: newMP.Clone(), 960 }, 961 testPos{ 962 party: "party4", 963 size: -5, 964 price: newMP.Clone(), 965 }, 966 } 967 engine.AddTrade(nTrade) 968 transfers = engine.SettleMTM(context.Background(), newMP, positions) 969 assert.NotEmpty(t, transfers) 970 newMP = num.NewUint(995) 971 positions = []events.MarketPosition{ 972 testPos{ 973 party: types.NetworkParty, 974 size: 9, 975 price: newMP.Clone(), 976 }, 977 testPos{ 978 party: "party3", 979 size: -4, 980 price: newMP.Clone(), 981 }, 982 testPos{ 983 party: "party4", 984 size: -5, 985 price: newMP.Clone(), 986 }, 987 } 988 transfers = engine.SettleMTM(context.Background(), newMP.Clone(), positions) 989 assert.NotEmpty(t, transfers) 990 // now the same, but network loses 991 newMP = num.NewUint(990) 992 positions = []events.MarketPosition{ 993 testPos{ 994 party: types.NetworkParty, 995 size: 9, 996 price: newMP.Clone(), 997 }, 998 testPos{ 999 party: "party3", 1000 size: -4, 1001 price: newMP.Clone(), 1002 }, 1003 testPos{ 1004 party: "party4", 1005 size: -5, 1006 price: newMP.Clone(), 1007 }, 1008 } 1009 transfers = engine.SettleMTM(context.Background(), newMP.Clone(), positions) 1010 assert.NotEmpty(t, transfers) 1011 // assume no trades occurred, but the mark price has changed (shouldn't happen, but this could end up with a situation where network profits without trading) 1012 // network disposes of its position and profits 1013 disposePrice := num.NewUint(1010) 1014 trades := []*types.Trade{ 1015 { 1016 Seller: types.NetworkParty, 1017 Buyer: "party3", 1018 Price: disposePrice.Clone(), 1019 Size: 4, 1020 }, 1021 { 1022 Seller: types.NetworkParty, 1023 Buyer: "party4", 1024 Price: disposePrice.Clone(), 1025 Size: 5, 1026 }, 1027 } 1028 positions = []events.MarketPosition{ 1029 testPos{ 1030 party: types.NetworkParty, 1031 size: 0, 1032 price: newMP.Clone(), 1033 }, 1034 testPos{ 1035 party: "party3", 1036 size: 0, 1037 price: newMP.Clone(), 1038 }, 1039 testPos{ 1040 party: "party4", 1041 size: 0, 1042 price: newMP.Clone(), 1043 }, 1044 } 1045 for _, tr := range trades { 1046 engine.AddTrade(tr) 1047 } 1048 transfers = engine.SettleMTM(context.Background(), num.NewUint(1000), positions) 1049 assert.NotEmpty(t, transfers) 1050 } 1051 1052 func testAddNewParty(t *testing.T) { 1053 engine := getTestEngine(t) 1054 defer engine.Finish() 1055 markPrice := num.NewUint(1000) 1056 t1 := testPos{ 1057 price: markPrice.Clone(), 1058 party: "party1", 1059 size: 5, 1060 } 1061 init := []events.MarketPosition{ 1062 t1, 1063 testPos{ 1064 price: markPrice.Clone(), 1065 party: "party2", 1066 size: -5, 1067 }, 1068 } 1069 // let's not change the markPrice 1070 // just add a party to the market, buying from an existing party 1071 trade := &types.Trade{ 1072 Buyer: "party3", 1073 Seller: t1.party, 1074 Price: markPrice.Clone(), 1075 Size: 1, 1076 } 1077 // the first party is the seller 1078 // so these are the new positions after the trade 1079 t1.size-- 1080 positions := []events.MarketPosition{ 1081 t1, 1082 init[1], 1083 testPos{ 1084 party: "party3", 1085 size: 1, 1086 price: markPrice.Clone(), 1087 }, 1088 } 1089 engine.Update(init) 1090 engine.AddTrade(trade) 1091 noTransfers := engine.SettleMTM(context.Background(), markPrice, positions) 1092 assert.Len(t, noTransfers, 2) 1093 for _, v := range noTransfers { 1094 assert.Nil(t, v.Transfer()) 1095 } 1096 } 1097 1098 // This tests MTM results put losses first, trades tested are Long going longer, short going shorter 1099 // and long going short, short going long, and a third party who's not trading at all. 1100 func testMarkToMarketOrdered(t *testing.T) { 1101 engine := getTestEngine(t) 1102 defer engine.Finish() 1103 pr := num.NewUint(10000) 1104 positions := []posValue{ 1105 { 1106 price: pr, 1107 size: 1, 1108 party: "party1", // mocks will create 2 parties (long & short) 1109 }, 1110 { 1111 price: pr.Clone(), 1112 size: -1, 1113 party: "party2", 1114 }, 1115 } 1116 markPrice := pr.Clone() 1117 markPrice = markPrice.Add(markPrice, num.NewUint(1000)) 1118 neutral := testPos{ 1119 party: "neutral", 1120 size: 5, 1121 price: pr.Clone(), 1122 } 1123 init := []events.MarketPosition{ 1124 neutral, 1125 testPos{ 1126 price: neutral.price.Clone(), 1127 party: "party1", 1128 size: 1, 1129 }, 1130 testPos{ 1131 price: neutral.price.Clone(), 1132 party: "party2", 1133 size: -1, 1134 }, 1135 } 1136 short, long := make([]events.MarketPosition, 0, 3), make([]events.MarketPosition, 0, 3) 1137 // the SettleMTM data must contain the new mark price already 1138 neutral.price = markPrice.Clone() 1139 short = append(short, neutral) 1140 long = append(long, neutral) 1141 // we have a long and short trade example 1142 trades := map[string]*types.Trade{ 1143 "long": { 1144 Price: markPrice, 1145 Size: 1, 1146 }, 1147 // to go short, the trade has to be 2 1148 "short": { 1149 Price: markPrice, 1150 Size: 2, 1151 }, 1152 } 1153 // creates trades and event slices we'll be needing later on 1154 for _, p := range positions { 1155 if p.size > 0 { 1156 trades["long"].Buyer = p.party 1157 trades["short"].Seller = p.party 1158 long = append(long, testPos{ 1159 party: p.party, 1160 price: markPrice.Clone(), 1161 size: p.size + int64(trades["long"].Size), 1162 }) 1163 short = append(short, testPos{ 1164 party: p.party, 1165 price: markPrice.Clone(), 1166 size: p.size - int64(trades["short"].Size), 1167 }) 1168 } else { 1169 trades["long"].Seller = p.party 1170 trades["short"].Buyer = p.party 1171 long = append(long, testPos{ 1172 party: p.party, 1173 price: markPrice.Clone(), 1174 size: p.size - int64(trades["long"].Size), 1175 }) 1176 short = append(short, testPos{ 1177 party: p.party, 1178 price: markPrice.Clone(), 1179 size: p.size + int64(trades["short"].Size), 1180 }) 1181 } 1182 } 1183 updates := map[string][]events.MarketPosition{ 1184 "long": long, 1185 "short": short, 1186 } 1187 // set up the engine, ready to run the scenario's 1188 // for each data-set we reset the state in the engine, then we check the MTM is performed 1189 // correctly 1190 for k, trade := range trades { 1191 engine.Update(init) 1192 engine.AddTrade(trade) 1193 update := updates[k] 1194 transfers := engine.SettleMTM(context.Background(), markPrice, update) 1195 assert.NotEmpty(t, transfers) 1196 assert.Equal(t, 3, len(transfers)) 1197 // start with losses, end with wins 1198 assert.Equal(t, types.TransferTypeMTMLoss, transfers[0].Transfer().Type) 1199 assert.Equal(t, types.TransferTypeMTMWin, transfers[len(transfers)-1].Transfer().Type) 1200 assert.Equal(t, "party2", transfers[0].Party()) // we expect party2 to have a loss 1201 } 1202 1203 state, _, _ := engine.GetState(engine.market) 1204 engineLoad := getTestEngine(t) 1205 var pl snapshot.Payload 1206 require.NoError(t, proto.Unmarshal(state, &pl)) 1207 payload := types.PayloadFromProto(&pl) 1208 1209 _, err := engineLoad.LoadState(context.Background(), payload) 1210 require.NoError(t, err) 1211 1212 state2, _, _ := engineLoad.GetState(engine.market) 1213 require.True(t, bytes.Equal(state, state2)) 1214 } 1215 1216 func testMTMNetworkZero(t *testing.T) { 1217 t.Skip("not implemented yet") 1218 engine := getTestEngine(t) 1219 defer engine.Finish() 1220 markPrice := num.NewUint(1000) 1221 init := []events.MarketPosition{ 1222 testPos{ 1223 price: markPrice.Clone(), 1224 party: "party1", 1225 size: 5, 1226 }, 1227 testPos{ 1228 price: markPrice.Clone(), 1229 party: "party2", 1230 size: -5, 1231 }, 1232 testPos{ 1233 price: markPrice.Clone(), 1234 party: "party3", 1235 size: 10, 1236 }, 1237 testPos{ 1238 price: markPrice.Clone(), 1239 party: "party4", 1240 size: -10, 1241 }, 1242 } 1243 // initialise the engine with the positions above 1244 engine.Update(init) 1245 // assume party 4 is distressed, network has to trade and buy 10 1246 // ensure the network loses in this scenario: the price has gone up 1247 cPrice := num.Sum(markPrice, num.NewUint(1)) 1248 trade := &types.Trade{ 1249 Buyer: types.NetworkParty, 1250 Seller: "party1", 1251 Size: 5, // party 1 only has 5 on the book, let's pretend we can close him our 1252 Price: cPrice.Clone(), 1253 } 1254 engine.AddTrade(trade) 1255 engine.AddTrade(&types.Trade{ 1256 Buyer: types.NetworkParty, 1257 Seller: "party3", 1258 Size: 2, 1259 Price: cPrice.Clone(), 1260 }) 1261 engine.AddTrade(&types.Trade{ 1262 Buyer: types.NetworkParty, 1263 Seller: "party2", 1264 Size: 3, 1265 Price: cPrice.Clone(), // party 2 is going from -5 to -8 1266 }) 1267 // the new positions of the parties who have traded with the network... 1268 positions := []events.MarketPosition{ 1269 testPos{ 1270 party: "party1", // party 1 was 5 long, sold 5 to network, so closed out 1271 price: markPrice.Clone(), 1272 size: 0, 1273 }, 1274 testPos{ 1275 party: "party3", 1276 size: 8, // long 10, sold 2 1277 price: markPrice.Clone(), 1278 }, 1279 testPos{ 1280 party: "party2", 1281 size: -8, 1282 price: markPrice.Clone(), // party 2 was -5, shorted an additional 3 => -8 1283 }, 1284 } 1285 // new markprice is cPrice 1286 noTransfers := engine.SettleMTM(context.Background(), cPrice, positions) 1287 assert.Len(t, noTransfers, 3) 1288 hasNetwork := false 1289 for i, v := range noTransfers { 1290 assert.NotNil(t, v.Transfer()) 1291 if v.Party() == types.NetworkParty { 1292 // network hás to lose 1293 require.Equal(t, types.TransferTypeMTMLoss, v.Transfer().Type) 1294 // network loss should be at the start of the slice 1295 require.Equal(t, 0, i) 1296 hasNetwork = true 1297 } 1298 } 1299 require.True(t, hasNetwork) 1300 } 1301 1302 func testMTMWinWithZero(t *testing.T) { 1303 // cheat by setting the factor to some specific value, makes it easier to create a scenario where win/loss amounts don't match 1304 engine := getTestEngineWithFactor(t, 5) 1305 defer engine.Finish() 1306 1307 price := num.NewUint(100000) 1308 one := num.NewUint(1) 1309 ctx := context.Background() 1310 1311 initPos := []testPos{ 1312 { 1313 price: price.Clone(), 1314 party: "party1", 1315 size: 1, 1316 }, 1317 { 1318 price: price.Clone(), 1319 party: "party2", 1320 size: 2, 1321 }, 1322 { 1323 price: price.Clone(), 1324 party: "party3", 1325 size: -3, 1326 }, 1327 { 1328 price: price.Clone(), 1329 party: "party4", 1330 size: 1, 1331 }, 1332 { 1333 price: price.Clone(), 1334 party: "party5", 1335 size: -3, 1336 }, 1337 { 1338 price: price.Clone(), 1339 party: "party6", 1340 size: 2, 1341 }, 1342 } 1343 1344 init := make([]events.MarketPosition, 0, len(initPos)) 1345 for _, p := range initPos { 1346 init = append(init, p) 1347 } 1348 1349 newPrice := num.Sum(price, one, one, one) 1350 somePrice := num.Sum(price, one) 1351 newParty := testPos{ 1352 size: 3, 1353 price: newPrice.Clone(), 1354 party: "party4", 1355 } 1356 1357 trades := []*types.Trade{ 1358 { 1359 Size: 1, 1360 Buyer: newParty.party, 1361 Seller: initPos[0].party, 1362 Price: somePrice.Clone(), 1363 }, 1364 { 1365 Size: 1, 1366 Buyer: newParty.party, 1367 Seller: initPos[1].party, 1368 Price: somePrice.Clone(), 1369 }, 1370 { 1371 Size: 1, 1372 Buyer: newParty.party, 1373 Seller: initPos[2].party, 1374 Price: newPrice.Clone(), 1375 }, 1376 } 1377 updates := make([]events.MarketPosition, 0, len(initPos)+2) 1378 for _, trade := range trades { 1379 for i, p := range initPos { 1380 if p.party == trade.Seller { 1381 p.size -= int64(trade.Size) 1382 } 1383 p.price = trade.Price.Clone() 1384 initPos[i] = p 1385 } 1386 } 1387 for _, p := range initPos { 1388 updates = append(updates, p) 1389 } 1390 updates = append(updates, newParty) 1391 engine.Update(init) 1392 for _, trade := range trades { 1393 engine.AddTrade(trade) 1394 } 1395 transfers := engine.SettleMTM(ctx, newPrice.Clone(), updates) 1396 require.NotEmpty(t, transfers) 1397 } 1398 1399 func testMTMWinGTLoss(t *testing.T) { 1400 // cheat by setting the factor to some specific value, makes it easier to create a scenario where win/loss amounts don't match 1401 engine := getTestEngineWithFactor(t, 5) 1402 defer engine.Finish() 1403 1404 price := num.NewUint(100000) 1405 one := num.NewUint(1) 1406 two := num.Sum(one, one) 1407 ctx := context.Background() 1408 1409 initPos := []testPos{ 1410 { 1411 price: price.Clone(), 1412 party: "party-1", 1413 size: 10, 1414 }, 1415 { 1416 price: price.Clone(), 1417 party: "party-2", 1418 size: -4, 1419 }, 1420 { 1421 price: price.Clone(), 1422 party: "party-3", 1423 size: -1, 1424 }, 1425 { 1426 price: price.Clone(), 1427 party: "party-4", 1428 size: -5, 1429 }, 1430 { 1431 price: price.Clone(), 1432 party: "party-6", 1433 size: -1, 1434 }, 1435 { 1436 price: price.Clone(), 1437 party: "party-7", 1438 size: 1, 1439 }, 1440 } 1441 init := make([]events.MarketPosition, 0, len(initPos)) 1442 for _, p := range initPos { 1443 init = append(init, p) 1444 } 1445 1446 somePrice := num.Sum(price, two) 1447 newPrice := num.Sum(price, one) 1448 newParty := testPos{ 1449 size: 3, 1450 price: newPrice.Clone(), 1451 party: "party-5", 1452 } 1453 1454 trades := []*types.Trade{ 1455 { 1456 Size: 1, 1457 Buyer: newParty.party, 1458 Seller: initPos[0].party, 1459 Price: somePrice.Clone(), 1460 }, 1461 { 1462 Size: 1, 1463 Buyer: newParty.party, 1464 Seller: initPos[3].party, 1465 Price: somePrice.Clone(), 1466 }, 1467 { 1468 Size: 1, 1469 Buyer: newParty.party, 1470 Seller: initPos[0].party, 1471 Price: newPrice.Clone(), 1472 }, 1473 } 1474 updates := make([]events.MarketPosition, 0, len(initPos)+1) 1475 for _, trade := range trades { 1476 for i, p := range initPos { 1477 if p.party == trade.Seller { 1478 p.size -= int64(trade.Size) 1479 initPos[i] = p 1480 } 1481 p.price = trade.Price.Clone() 1482 } 1483 } 1484 for _, p := range initPos { 1485 updates = append(updates, p) 1486 } 1487 updates = append(updates, newParty) 1488 engine.Update(init) 1489 for _, trade := range trades { 1490 engine.AddTrade(trade) 1491 } 1492 transfers := engine.SettleMTM(ctx, newPrice.Clone(), updates) 1493 require.NotEmpty(t, transfers) 1494 // just a single transfer of 2 1495 require.True(t, transfers[0].Transfer().Amount.Amount.EQ(two)) 1496 } 1497 1498 // {{{. 1499 func (te *testEngine) getExpiryPositions(positions ...posValue) []events.MarketPosition { 1500 te.positions = make([]*mocks.MockMarketPosition, 0, len(positions)) 1501 mpSlice := make([]events.MarketPosition, 0, len(positions)) 1502 for _, p := range positions { 1503 pos := mocks.NewMockMarketPosition(te.ctrl) 1504 // these values should only be obtained once, and assigned internally 1505 pos.EXPECT().Party().MinTimes(1).AnyTimes().Return(p.party) 1506 pos.EXPECT().Size().MinTimes(1).AnyTimes().Return(p.size) 1507 pos.EXPECT().Price().Times(1).Return(p.price) 1508 te.positions = append(te.positions, pos) 1509 mpSlice = append(mpSlice, pos) 1510 } 1511 return mpSlice 1512 } 1513 1514 func (te *testEngine) getMockMarketPositions(data []posValue) ([]settlement.MarketPosition, []events.MarketPosition) { 1515 raw, evts := make([]settlement.MarketPosition, 0, len(data)), make([]events.MarketPosition, 0, len(data)) 1516 for _, pos := range data { 1517 mock := mocks.NewMockMarketPosition(te.ctrl) 1518 mock.EXPECT().Party().MinTimes(1).Return(pos.party) 1519 mock.EXPECT().Size().MinTimes(1).Return(pos.size) 1520 mock.EXPECT().Price().MinTimes(1).Return(pos.price) 1521 raw = append(raw, mock) 1522 evts = append(evts, mock) 1523 } 1524 return raw, evts 1525 } 1526 1527 func TestRemoveDistressedNoTrades(t *testing.T) { 1528 engine := getTestEngine(t) 1529 defer engine.Finish() 1530 engine.prod.EXPECT().Settle(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(func(markPrice *num.Uint, settlementData *num.Uint, size num.Decimal) (*types.FinancialAmount, bool, num.Decimal, error) { 1531 return &types.FinancialAmount{Amount: num.UintZero()}, false, num.DecimalZero(), nil 1532 }) 1533 1534 data := []posValue{ 1535 { 1536 party: "testparty1", 1537 price: num.NewUint(1234), 1538 size: 100, 1539 }, 1540 { 1541 party: "testparty2", 1542 price: num.NewUint(1235), 1543 size: 0, 1544 }, 1545 } 1546 raw, evts := engine.getMockMarketPositions(data) 1547 // margin evt 1548 marginEvts := make([]events.Margin, 0, len(raw)) 1549 for _, pe := range raw { 1550 marginEvts = append(marginEvts, marginVal{ 1551 MarketPosition: pe, 1552 }) 1553 } 1554 1555 assert.False(t, engine.HasTraded()) 1556 engine.Update(evts) 1557 engine.RemoveDistressed(context.Background(), marginEvts) 1558 assert.False(t, engine.HasTraded()) 1559 } 1560 1561 func TestConcurrent(t *testing.T) { 1562 const N = 10 1563 1564 engine := getTestEngine(t) 1565 defer engine.Finish() 1566 engine.prod.EXPECT().Settle(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(func(markPrice *num.Uint, settlementData *num.Uint, size num.Decimal) (*types.FinancialAmount, bool, num.Decimal, error) { 1567 return &types.FinancialAmount{Amount: num.UintZero()}, false, num.DecimalZero(), nil 1568 }) 1569 1570 cfg := engine.Config 1571 cfg.Level.Level = logging.DebugLevel 1572 engine.ReloadConf(cfg) 1573 cfg.Level.Level = logging.InfoLevel 1574 engine.ReloadConf(cfg) 1575 1576 var wg sync.WaitGroup 1577 1578 now := time.Now() 1579 wg.Add(N * 3) 1580 for i := 0; i < N; i++ { 1581 data := []posValue{ 1582 { 1583 party: "testparty1", 1584 price: num.NewUint(1234), 1585 size: 100, 1586 }, 1587 { 1588 party: "testparty2", 1589 price: num.NewUint(1235), 1590 size: 0, 1591 }, 1592 } 1593 raw, evts := engine.getMockMarketPositions(data) 1594 // margin evt 1595 marginEvts := make([]events.Margin, 0, len(raw)) 1596 for _, pe := range raw { 1597 marginEvts = append(marginEvts, marginVal{ 1598 MarketPosition: pe, 1599 }) 1600 } 1601 1602 go func() { 1603 defer wg.Done() 1604 // Update requires posMu 1605 engine.Update(evts) 1606 }() 1607 go func() { 1608 defer wg.Done() 1609 // RemoveDistressed requires posMu and closedMu 1610 engine.RemoveDistressed(context.Background(), marginEvts) 1611 }() 1612 go func() { 1613 defer wg.Done() 1614 // Settle requires posMu 1615 _, _, err := engine.Settle(now, num.UintZero()) 1616 assert.NoError(t, err) 1617 }() 1618 } 1619 1620 wg.Wait() 1621 } 1622 1623 // Finish - call finish on controller, remove test state (positions). 1624 func (te *testEngine) Finish() { 1625 te.ctrl.Finish() 1626 te.positions = nil 1627 } 1628 1629 // Quick mock implementation of the events.MarketPosition interface. 1630 type testPos struct { 1631 party string 1632 size, buy, sell int64 1633 price *num.Uint 1634 buySumProduct, sellSumProduct uint64 1635 } 1636 1637 func (t testPos) AverageEntryPrice() *num.Uint { 1638 return num.UintZero() 1639 } 1640 1641 func (t testPos) Party() string { 1642 return t.party 1643 } 1644 1645 func (t testPos) Size() int64 { 1646 return t.size 1647 } 1648 1649 func (t testPos) Buy() int64 { 1650 return t.buy 1651 } 1652 1653 func (t testPos) Sell() int64 { 1654 return t.sell 1655 } 1656 1657 func (t testPos) Price() *num.Uint { 1658 if t.price == nil { 1659 return num.UintZero() 1660 } 1661 return t.price 1662 } 1663 1664 func (t testPos) BuySumProduct() *num.Uint { 1665 return num.NewUint(t.buySumProduct) 1666 } 1667 1668 func (t testPos) SellSumProduct() *num.Uint { 1669 return num.NewUint(t.sellSumProduct) 1670 } 1671 1672 func (t testPos) VWBuy() *num.Uint { 1673 if t.buy == 0 { 1674 return num.UintZero() 1675 } 1676 return num.NewUint(t.buySumProduct / uint64(t.buy)) 1677 } 1678 1679 func (t testPos) VWSell() *num.Uint { 1680 if t.sell == 0 { 1681 return num.UintZero() 1682 } 1683 return num.NewUint(t.sellSumProduct / uint64(t.sell)) 1684 } 1685 1686 func (t testPos) ClearPotentials() {} 1687 1688 func getTestEngineWithFactor(t *testing.T, f float64) *testEngine { 1689 t.Helper() 1690 ctrl := gomock.NewController(t) 1691 conf := settlement.NewDefaultConfig() 1692 prod := mocks.NewMockProduct(ctrl) 1693 tsvc := mocks.NewMockTimeService(ctrl) 1694 tsvc.EXPECT().GetTimeNow().AnyTimes() 1695 broker := bmocks.NewMockBroker(ctrl) 1696 broker.EXPECT().SendBatch(gomock.Any()).AnyTimes() 1697 market := "BTC/DEC19" 1698 prod.EXPECT().GetAsset().AnyTimes().Do(func() string { return "BTC" }) 1699 return &testEngine{ 1700 SnapshotEngine: settlement.NewSnapshotEngine(logging.NewTestLogger(), conf, prod, market, tsvc, broker, num.NewDecimalFromFloat(f)), 1701 ctrl: ctrl, 1702 prod: prod, 1703 tsvc: tsvc, 1704 broker: broker, 1705 positions: nil, 1706 market: market, 1707 } 1708 } 1709 1710 func getTestEngine(t *testing.T) *testEngine { 1711 t.Helper() 1712 return getTestEngineWithFactor(t, 1) 1713 } // }}} 1714 1715 func (m marginVal) Asset() string { 1716 return m.asset 1717 } 1718 1719 func (m marginVal) MarketID() string { 1720 return m.marketID 1721 } 1722 1723 func (m marginVal) MarginBalance() *num.Uint { 1724 return num.NewUint(m.margin) 1725 } 1726 1727 func (m marginVal) OrderMarginBalance() *num.Uint { 1728 return num.NewUint(m.orderMargin) 1729 } 1730 1731 func (m marginVal) GeneralBalance() *num.Uint { 1732 return num.NewUint(m.general) 1733 } 1734 1735 func (m marginVal) GeneralAccountBalance() *num.Uint { 1736 return num.NewUint(m.general) 1737 } 1738 1739 func (m marginVal) BondBalance() *num.Uint { 1740 return num.UintZero() 1741 } 1742 1743 func (m marginVal) MarginShortFall() *num.Uint { 1744 return num.NewUint(m.marginShortFall) 1745 } 1746 1747 // vim: set ts=4 sw=4 tw=0 foldlevel=1 foldmethod=marker noet :