code.vegaprotocol.io/vega@v0.79.0/core/execution/future/equity_share_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 future_test
    17  
    18  import (
    19  	"context"
    20  	"testing"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/events"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	vegacontext "code.vegaprotocol.io/vega/libs/context"
    26  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  )
    32  
    33  type equityShareMarket struct {
    34  	t       *testing.T
    35  	tm      *testMarket
    36  	parties map[string]struct{}
    37  
    38  	Now       time.Time
    39  	ClosingAt time.Time
    40  }
    41  
    42  func newEquityShareMarket(t *testing.T) *equityShareMarket {
    43  	t.Helper()
    44  	now := time.Unix(10, 0)
    45  	closingAt := time.Unix(10000000000, 0)
    46  
    47  	return &equityShareMarket{
    48  		t:         t,
    49  		tm:        getTestMarket(t, now, nil, &types.AuctionDuration{Duration: 1}),
    50  		parties:   map[string]struct{}{},
    51  		Now:       now,
    52  		ClosingAt: closingAt,
    53  	}
    54  }
    55  
    56  func (esm *equityShareMarket) TestMarket() *testMarket { return esm.tm }
    57  
    58  func (esm *equityShareMarket) BuildOrder(id, party string, side types.Side, price uint64) *types.Order {
    59  	return &types.Order{
    60  		Type:        types.OrderTypeLimit,
    61  		TimeInForce: types.OrderTimeInForceGTT,
    62  		Status:      types.OrderStatusActive,
    63  		ID:          id,
    64  		Side:        side,
    65  		Party:       party,
    66  		MarketID:    esm.tm.market.GetID(),
    67  		Size:        1,
    68  		Price:       num.NewUint(price),
    69  		Remaining:   1,
    70  		CreatedAt:   esm.Now.UnixNano(),
    71  		ExpiresAt:   esm.ClosingAt.UnixNano(),
    72  	}
    73  }
    74  
    75  func (esm *equityShareMarket) createPartyIfMissing(t *testing.T, party string) {
    76  	t.Helper()
    77  	if _, ok := esm.parties[party]; !ok {
    78  		esm.parties[party] = struct{}{}
    79  		addAccount(t, esm.tm, party)
    80  	}
    81  }
    82  
    83  func (esm *equityShareMarket) SubmitOrder(t *testing.T, ctx context.Context, order *types.Order) (*types.OrderConfirmation, error) {
    84  	t.Helper()
    85  	esm.createPartyIfMissing(t, order.Party)
    86  	return esm.tm.market.SubmitOrder(ctx, order)
    87  }
    88  
    89  func (esm *equityShareMarket) WithSubmittedOrder(t *testing.T, id, party string, side types.Side, price uint64) *equityShareMarket {
    90  	t.Helper()
    91  	ctx := context.Background()
    92  	order := esm.BuildOrder(id, party, side, price)
    93  
    94  	_, err := esm.SubmitOrder(t, ctx, order)
    95  	require.NoError(esm.t, err)
    96  	return esm
    97  }
    98  
    99  func (esm *equityShareMarket) WithSubmittedLiquidityProvision(t *testing.T, party, id string, amount uint64, fee string) *equityShareMarket {
   100  	t.Helper()
   101  	esm.createPartyIfMissing(t, party)
   102  	esm.tm.WithSubmittedLiquidityProvision(esm.t, party, amount, fee)
   103  	return esm
   104  }
   105  
   106  func (esm *equityShareMarket) LiquidityFeeAccount() *types.Account {
   107  	acc, err := esm.tm.collateralEngine.GetMarketLiquidityFeeAccount(
   108  		esm.tm.market.GetID(), esm.tm.asset,
   109  	)
   110  	require.NoError(esm.t, err)
   111  	return acc
   112  }
   113  
   114  func (esm *equityShareMarket) PartyGeneralAccount(party string) *types.Account {
   115  	acc, err := esm.tm.collateralEngine.GetPartyGeneralAccount(
   116  		party, esm.tm.asset,
   117  	)
   118  	require.NoError(esm.t, err)
   119  	return acc
   120  }
   121  
   122  func (esm *equityShareMarket) PartyLiquidityFeeAccount(party string) *types.Account {
   123  	acc, err := esm.tm.collateralEngine.GetPartyLiquidityFeeAccount(
   124  		esm.tm.market.GetID(), party, esm.tm.asset,
   125  	)
   126  	require.NoError(esm.t, err)
   127  	return acc
   128  }
   129  
   130  func (esm *equityShareMarket) PartyMarginAccount(party string) *types.Account {
   131  	acc, err := esm.tm.collateralEngine.GetPartyMarginAccount(
   132  		esm.tm.market.GetID(), party, esm.tm.asset,
   133  	)
   134  	require.NoError(esm.t, err)
   135  	return acc
   136  }
   137  
   138  func TestWithinMarket(t *testing.T) {
   139  	var (
   140  		ctx = vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash())
   141  		// as we will split fees in 1/3 and 2/3
   142  		// we use 900000 cause we need this number to be divisible by 3
   143  		matchingPrice = uint64(900000)
   144  		one           = uint64(1)
   145  	)
   146  
   147  	// Setup a market with a set of non-matching orders and Liquidity Provision
   148  	// Submissions from 2 parties.
   149  	esm := newEquityShareMarket(t).
   150  		WithSubmittedOrder(t, "some-id-1", "party1", types.SideSell, matchingPrice+one).
   151  		WithSubmittedOrder(t, "some-id-2", "party2", types.SideBuy, matchingPrice-one).
   152  		WithSubmittedOrder(t, "some-id-3", "party1", types.SideSell, matchingPrice).
   153  		WithSubmittedOrder(t, "some-id-4", "party2", types.SideBuy, matchingPrice). // Need to generate a trade to leave opening auction
   154  		// party1 (commitment: 2000) should get 2/3 of the fee
   155  		WithSubmittedLiquidityProvision(t, "party1", "lp-id-1", 2000000, "0.5").
   156  		// party2 (commitment: 1000) should get 1/3 of the fee
   157  		WithSubmittedLiquidityProvision(t, "party2", "lp-id-2", 1000000, "0.5")
   158  
   159  	// tm is the testMarket instance
   160  	var (
   161  		tm      = esm.TestMarket()
   162  		curTime = esm.Now
   163  	)
   164  
   165  	// End opening auction
   166  	curTime = curTime.Add(2 * time.Second)
   167  	tm.now = curTime
   168  	tm.market.OnTick(ctx, curTime)
   169  
   170  	md := esm.tm.market.GetMarketData()
   171  	require.NotNil(t, md)
   172  	require.Equal(t, types.MarketTradingModeContinuous, md.MarketTradingMode)
   173  
   174  	t.Run("WhenNoTrades", func(t *testing.T) {
   175  		// clean up previous events
   176  		tm.events = []events.Event{}
   177  
   178  		// Trigger Fee distribution
   179  		curTime = curTime.Add(1 * time.Second)
   180  		tm.now = curTime
   181  		tm.market.OnTick(ctx, curTime)
   182  
   183  		// Assert the event
   184  		var evt *events.LedgerMovements
   185  		for _, e := range tm.events {
   186  			if e.Type() == events.LedgerMovementsEvent {
   187  				evt = e.(*events.LedgerMovements)
   188  			}
   189  		}
   190  		require.Nil(t, evt, "should receive no TransferEvent")
   191  	})
   192  
   193  	// Match a pair of orders (same price) to trigger a fee distribution.
   194  	conf, err := esm.
   195  		WithSubmittedOrder(t, "some-id-3", "party1", types.SideSell, matchingPrice).
   196  		SubmitOrder(t, context.Background(), esm.BuildOrder("some-id-4", "party2", types.SideBuy, matchingPrice))
   197  	require.NoError(t, err)
   198  	require.Len(t, conf.Trades, 1)
   199  
   200  	// Retrieve both MarketLiquidityFee account balance and Party Balance
   201  	// before the fee distribution.
   202  
   203  	originalBalance := esm.LiquidityFeeAccount().Balance.Clone()
   204  
   205  	curTime = curTime.Add(1 * time.Second)
   206  	tm.now = curTime
   207  	tm.market.OnTick(ctx, curTime)
   208  
   209  	md = esm.tm.market.GetMarketData()
   210  	require.NotNil(t, md)
   211  	require.Equal(t, types.MarketTradingModeContinuous, md.MarketTradingMode)
   212  
   213  	oneU := num.NewUint(1)
   214  	assert.True(t, esm.LiquidityFeeAccount().Balance.EQ(oneU),
   215  		"LiquidityFeeAccount should have a balance of 1 (remainder)")
   216  
   217  	// exp = originalBalance*(2/3)
   218  	exp := num.UintZero().Mul(num.Sum(oneU, oneU), originalBalance)
   219  	exp = exp.Div(exp, num.Sum(oneU, oneU, oneU))
   220  	actual := esm.PartyLiquidityFeeAccount("party1").Balance
   221  	assert.True(t,
   222  		exp.EQ(actual),
   223  		"party1 should get 2/3 of the fees (got %s expected %s)", actual.String(), exp.String(),
   224  	)
   225  
   226  	// exp = originalBalance*(1/3)
   227  	exp = num.UintZero().Div(originalBalance, num.Sum(oneU, oneU, oneU))
   228  	// minus the remainder
   229  	exp.Sub(exp, oneU)
   230  	actual = esm.PartyLiquidityFeeAccount("party2").Balance
   231  	assert.True(t,
   232  		exp.EQ(actual),
   233  		"party2 should get 2/3 of the fees (got %s expected %s)", actual.String(), exp.String(),
   234  	)
   235  }