From c7ded40cf4efcd874c174a0996c454b004ca7a5d Mon Sep 17 00:00:00 2001 From: jeremyletang Date: Sun, 13 Sep 2020 15:30:28 +0100 Subject: [PATCH] ensure margin accounts funds are release each time a position goes back completely to 0. --- collateral/engine.go | 33 +++++++ collateral/engine_test.go | 32 +++++++ execution/market.go | 33 +++++++ .../ensure-funds-are-released.feature | 92 +++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 integration/features/ensure-funds-are-released.feature diff --git a/collateral/engine.go b/collateral/engine.go index bc46a4bb52..ce819ac856 100644 --- a/collateral/engine.go +++ b/collateral/engine.go @@ -1317,6 +1317,39 @@ func (e *Engine) RemoveDistressed(ctx context.Context, traders []events.MarketPo return &resp, nil } +func (e *Engine) ClearPartyMarginAccount(ctx context.Context, party, market, asset string) (*types.TransferResponse, error) { + acc, err := e.GetAccountByID(e.accountID(market, party, asset, types.AccountType_ACCOUNT_TYPE_MARGIN)) + if err != nil { + return nil, err + } + resp := types.TransferResponse{ + Transfers: []*types.LedgerEntry{}, + } + + if acc.Balance > 0 { + genAcc, err := e.GetAccountByID(e.accountID(noMarket, party, asset, types.AccountType_ACCOUNT_TYPE_GENERAL)) + if err != nil { + return nil, err + } + + resp.Transfers = append(resp.Transfers, &types.LedgerEntry{ + FromAccount: acc.Id, + ToAccount: genAcc.Id, + Amount: acc.Balance, + Reference: types.TransferType_TRANSFER_TYPE_MARGIN_HIGH.String(), + Type: types.TransferType_TRANSFER_TYPE_MARGIN_HIGH.String(), + Timestamp: e.currentTime, + }) + if err := e.IncrementBalance(ctx, genAcc.Id, acc.Balance); err != nil { + return nil, err + } + if err := e.UpdateBalance(ctx, acc.Id, 0); err != nil { + return nil, err + } + } + return &resp, nil +} + // CreateMarketAccounts will create all required accounts for a market once // a new market is accepted through the network func (e *Engine) CreateMarketAccounts(ctx context.Context, marketID, asset string, insurance uint64) (insuranceID, settleID string, err error) { diff --git a/collateral/engine_test.go b/collateral/engine_test.go index 4b3a571543..dd27300c72 100644 --- a/collateral/engine_test.go +++ b/collateral/engine_test.go @@ -42,6 +42,7 @@ func TestCollateralTransfer(t *testing.T) { t.Run("test collecting sells - cases where settle account is full + where insurance pool is tapped", testDistributeWin) t.Run("test collecting both buys and sells - Successfully collect buy and sell in a single call", testProcessBoth) t.Run("test distribution insufficient funds - Transfer losses (partial), distribute wins pro-rate", testProcessBothProRated) + t.Run("test releas party margin account", testReleasePartyMarginAccount) } func TestCollateralMarkToMarket(t *testing.T) { @@ -98,6 +99,36 @@ func testFeesTransferContinuousNoTransfer(t *testing.T) { assert.Nil(t, err) } +func testReleasePartyMarginAccount(t *testing.T) { + eng := getTestEngine(t, "test-market", 0) + defer eng.Finish() + + trader := "mytrader" + // create trader + eng.broker.EXPECT().Send(gomock.Any()).Times(4) + gen, err := eng.Engine.CreatePartyGeneralAccount(context.Background(), trader, testMarketAsset) + mar, err := eng.Engine.CreatePartyMarginAccount(context.Background(), trader, testMarketID, testMarketAsset) + assert.Nil(t, err) + + // add funds + eng.broker.EXPECT().Send(gomock.Any()).Times(1) + err = eng.Engine.UpdateBalance(context.Background(), gen, 100) + assert.Nil(t, err) + eng.broker.EXPECT().Send(gomock.Any()).Times(1) + err = eng.Engine.UpdateBalance(context.Background(), mar, 500) + assert.Nil(t, err) + + eng.broker.EXPECT().Send(gomock.Any()).Times(2) + _, err = eng.ClearPartyMarginAccount( + context.Background(), trader, testMarketID, testMarketAsset) + assert.NoError(t, err) + generalAcc, _ := eng.GetAccountByID(gen) + assert.Equal(t, 600, int(generalAcc.Balance)) + marginAcc, _ := eng.GetAccountByID(mar) + assert.Equal(t, 0, int(marginAcc.Balance)) + +} + func testFeeTransferContinuousNoFunds(t *testing.T) { eng := getTestEngine(t, "test-market", 0) defer eng.Finish() @@ -127,6 +158,7 @@ func testFeeTransferContinuousNoFunds(t *testing.T) { context.Background(), testMarketID, testMarketAsset, transferFeesReq) assert.Nil(t, transfers) assert.EqualError(t, err, collateral.ErrInsufficientFundsToPayFees.Error()) + } func testFeeTransferContinuousNotEnoughFunds(t *testing.T) { diff --git a/execution/market.go b/execution/market.go index 444070bcec..80c79acfd5 100644 --- a/execution/market.go +++ b/execution/market.go @@ -630,6 +630,33 @@ func (m *Market) validateAccounts(ctx context.Context, order *types.Order) error return nil } +func (m *Market) releaseMarginExcess(ctx context.Context, partyID string) { + // if this position went 0 + pos, ok := m.position.GetPositionByPartyID(partyID) + if !ok { + // position was never created or party went distressed and don't exist + // all good we can return + return + } + + // now chec if all buy/sell/size are 0 + if pos.Buy() != 0 || pos.Sell() != 0 || pos.Size() != 0 || pos.VWBuy() != 0 || pos.VWSell() != 0 { + // position is not 0, nothing to release surely + return + } + + asset, _ := m.mkt.GetAsset() + transfers, err := m.collateral.ClearPartyMarginAccount( + ctx, partyID, m.GetID(), asset) + if err != nil { + m.log.Error("unable to clear party margin account", logging.Error(err)) + return + } + evt := events.NewTransferResponse( + ctx, []*types.TransferResponse{transfers}) + m.broker.Send(evt) +} + // SubmitOrder submits the given order func (m *Market) SubmitOrder(ctx context.Context, order *types.Order) (*types.OrderConfirmation, error) { timer := metrics.NewTimeCounter(m.mkt.Id, "market", "SubmitOrder") @@ -691,6 +718,10 @@ func (m *Market) SubmitOrder(ctx context.Context, order *types.Order) (*types.Or return nil, ErrMarginCheckFailed } + // from here we may have assigned some margin. + // we add the check to roll it back in case we have a 0 positions after this + defer m.releaseMarginExcess(ctx, order.PartyID) + // If we are not in an opening auction, apply fees var trades []*types.Trade if m.mkt.OpeningAuction == nil && @@ -1536,6 +1567,8 @@ func (m *Market) CancelOrder(ctx context.Context, partyID, orderID string) (*typ return nil, types.ErrInvalidPartyID } + defer m.releaseMarginExcess(ctx, partyID) + cancellation, err := m.matching.CancelOrder(order) if cancellation == nil || err != nil { if m.log.GetLevel() == logging.DebugLevel { diff --git a/integration/features/ensure-funds-are-released.feature b/integration/features/ensure-funds-are-released.feature new file mode 100644 index 0000000000..c91a5d6608 --- /dev/null +++ b/integration/features/ensure-funds-are-released.feature @@ -0,0 +1,92 @@ +Feature: Test margins releases on position = 0 + + Background: + Given the insurance pool initial balance for the markets is "0": + And the executon engine have these markets: + | name | baseName | quoteName | asset | markprice | risk model | lamd/long | tau/short | mu | r | sigma | release factor | initial factor | search factor | settlementPrice | openAuction | trading mode | makerFee | infrastructureFee | liquidityFee | + | ETH/DEC19 | ETH | BTC | BTC | 94 | simple | 0.2 | 0.1 | 0 | 0.016 | 2.0 | 5 | 4 | 3.2 | 42 | 0 | continuous | 0 | 0 | 0 | + + Scenario: No margin left for fok order as first order +# setup accounts + Given the following traders: + | name | amount | + | traderGuy | 1000000000 | + Then I Expect the traders to have new general account: + | name | asset | + | traderGuy | BTC | + +# setup previous mark price + Then traders place following orders: + | trader | id | type | volume | price | resulting trades | type | tif | + + Then traders place following orders: + | trader | id | type | volume | price | resulting trades | type | tif | + | traderGuy | ETH/DEC19 | buy | 13 | 15000 | 0 | TYPE_LIMIT | TIF_FOK | + +# checking margins + Then I expect the trader to have a margin: + | trader | asset | id | margin | general | + | traderGuy | BTC | ETH/DEC19 | 0 | 1000000000 | + + Scenario: No margin left for wash trade +# setup accounts + Given the following traders: + | name | amount | + | traderGuy | 1000000000 | + Then I Expect the traders to have new general account: + | name | asset | + | traderGuy | BTC | + +# setup previous mark price + Then traders place following orders: + | trader | id | type | volume | price | resulting trades | type | tif | + + Then traders place following orders: + | trader | id | type | volume | price | resulting trades | type | tif | + | traderGuy | ETH/DEC19 | buy | 13 | 15000 | 0 | TYPE_LIMIT | TIF_GTC | + +# checking margins + Then I expect the trader to have a margin: + | trader | asset | id | margin | general | + | traderGuy | BTC | ETH/DEC19 | 980 | 999999020 | + +# now we place an order which would wash trade and see + Then traders place following orders: + | trader | id | type | volume | price | resulting trades | type | tif | + | traderGuy | ETH/DEC19 | sell | 13 | 15000 | 0 | TYPE_LIMIT | TIF_GTC | + +# checking margins, should have the margins required for the current order + Then I expect the trader to have a margin: + | trader | asset | id | margin | general | + | traderGuy | BTC | ETH/DEC19 | 980 | 999999020 | + + Scenario: No margin left after cancelling order and getting back to 0 position +# setup accounts + Given the following traders: + | name | amount | + | traderGuy | 1000000000 | + Then I Expect the traders to have new general account: + | name | asset | + | traderGuy | BTC | + +# setup previous mark price + Then traders place following orders: + | trader | id | type | volume | price | resulting trades | type | tif | + + Then traders place following orders with references: + | trader | id | type | volume | price | resulting trades | type | tif | reference | + | traderGuy | ETH/DEC19 | buy | 13 | 15000 | 0 | TYPE_LIMIT | TIF_GTC | ref-1 | + +# checking margins + Then I expect the trader to have a margin: + | trader | asset | id | margin | general | + | traderGuy | BTC | ETH/DEC19 | 980 | 999999020 | + +# cancel the order + Then traders cancels the following orders reference: + | trader | reference | + | traderGuy | ref-1 | + + Then I expect the trader to have a margin: + | trader | asset | id | margin | general | + | traderGuy | BTC | ETH/DEC19 | 0 | 1000000000 |