From 2e15bb72f67580ec636e12154bf7a495ef42cdcc Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Mon, 11 Mar 2024 17:10:53 +0000 Subject: [PATCH] fix: ensure we hit leave auction trigger on mark-price calc, and handle zero length auction intervals in perpetual --- core/execution/common/mark_price.go | 8 +- core/execution/future/market.go | 4 +- core/integration/features/perpetual.feature | 2 +- .../price-mon-trigger-leaving-auction.feature | 107 ++++++++++++++++++ core/products/perpetual.go | 20 +++- core/products/perpetual_auctions_test.go | 26 +++++ 6 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 core/integration/features/price-mon-trigger-leaving-auction.feature diff --git a/core/execution/common/mark_price.go b/core/execution/common/mark_price.go index e7458dbf2d..2eaeac842e 100644 --- a/core/execution/common/mark_price.go +++ b/core/execution/common/mark_price.go @@ -281,11 +281,11 @@ func (mpc *CompositePriceCalculator) updateMarkPriceIfNotInAuction(ctx context.C return nil } priceMonitor.CheckPrice(ctx, as, []*types.Trade{{Price: mpcCandidate, Size: 1}}, true, true) - if !as.InAuction() { - mpc.price = mpcCandidate - return nil + if as.InAuction() || as.AuctionStart() { + return fmt.Errorf("price monitoring failed for the new mark price") } - return fmt.Errorf("price monitoring failed for the new mark price") + mpc.price = mpcCandidate + return nil } // CalculateMarkPrice is called at the end of each mark price calculation interval and calculates the mark price diff --git a/core/execution/future/market.go b/core/execution/future/market.go index d76f2b741a..4c10a56e82 100644 --- a/core/execution/future/market.go +++ b/core/execution/future/market.go @@ -1580,7 +1580,6 @@ func (m *Market) leaveAuction(ctx context.Context, now time.Time) { m.mkt.State = types.MarketStateActive m.mkt.TradingMode = types.MarketTradingModeContinuous - m.tradableInstrument.Instrument.UpdateAuctionState(ctx, false) m.broker.Send(events.NewMarketUpdatedEvent(ctx, *m.mkt)) m.updateLiquidityFee(ctx) @@ -1612,6 +1611,9 @@ func (m *Market) leaveAuction(ctx context.Context, now time.Time) { // update auction state, so we know what the new tradeMode ought to be endEvt := m.as.Left(ctx, now) + // we tell the perp that we've left auction, we might re-enter just a bit down but thats fine as + // we will at least keep the in/out orders in sync + m.tradableInstrument.Instrument.UpdateAuctionState(ctx, false) for _, uncrossedOrder := range uncrossedOrders { updatedOrders = append(updatedOrders, uncrossedOrder.Order) diff --git a/core/integration/features/perpetual.feature b/core/integration/features/perpetual.feature index c01e06844f..185b81adfc 100644 --- a/core/integration/features/perpetual.feature +++ b/core/integration/features/perpetual.feature @@ -226,7 +226,7 @@ Feature: Simple test creating a perpetual market. | 976 | TRADING_MODE_CONTINUOUS | AUCTION_TRIGGER_UNSPECIFIED | 1 | And the following funding period events should be emitted: | start | end | internal twap | external twap | - | 1575072002 | | 9760000000000000 | | + | 1575072002 | | | | # perps payment doesn't happen in the absence of oracle data When the oracles broadcast data with block time signed with "0xCAFECAFE1": diff --git a/core/integration/features/price-mon-trigger-leaving-auction.feature b/core/integration/features/price-mon-trigger-leaving-auction.feature new file mode 100644 index 0000000000..8357a60bc0 --- /dev/null +++ b/core/integration/features/price-mon-trigger-leaving-auction.feature @@ -0,0 +1,107 @@ +Feature: replicating the incentive panic from issue 10858. + Background: + Given the following network parameters are set: + | name | value | + | network.markPriceUpdateMaximumFrequency | 1s | + And the price monitoring named "my-price-monitoring": + | horizon | probability | auction extension | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 30 | 0.99999 | 1 | + | 120 | 0.9999999 | 5 | + | 120 | 0.9999999 | 5 | + | 120 | 0.9999999 | 5 | + | 120 | 0.9999999 | 5 | + | 120 | 0.9999999 | 5 | + | 200 | 0.9999999 | 5 | + | 200 | 0.9999999 | 5 | + | 200 | 0.9999999 | 5 | + | 200 | 0.9999999 | 5 | + | 200 | 0.9999999 | 5 | + And the liquidity monitoring parameters: + | name | triggering ratio | time window | scaling factor | + | lqm-params | 0.00 | 24h | 1e-9 | + And the simple risk model named "simple-risk-model": + | long | short | max move up | min move down | probability of trading | + | 0.1 | 0.1 | 100 | -100 | 0.2 | + + # this is just an example of setting up oracles + And the composite price oracles from "0xCAFECAFE1": + | name | price property | price type | price decimals | + | oracle1 | price1.USD.value | TYPE_INTEGER | 0 | + | oracle2 | price2.USD.value | TYPE_INTEGER | 0 | + | oracle3 | price3.USD.value | TYPE_INTEGER | 0 | + + And the markets: + | id | quote name | asset | liquidity monitoring | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | price type | decay weight | decay power | cash amount | source weights | source staleness tolerance | oracle1 | oracle2 | oracle3 | + | ETH/FEB23 | ETH | USD | lqm-params | simple-risk-model | default-margin-calculator | 1 | default-none | my-price-monitoring | default-eth-for-future | 0.25 | 0 | default-futures | weight | 1 | 1 | 0 | 0,0,0,1,0 | 1m0s,1m0s,1m0s,1m0s,1m0s | oracle1 | oracle2 | oracle3 | + + @MPP + Scenario: Oracle data significantly higher than trade price, submitted before leaving opening auction + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | buySideProvider | USD | 100000000000 | + | sellSideProvider | USD | 100000000000 | + | party | USD | 948050 | + + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | buySideProvider | ETH/FEB23 | buy | 10 | 14900 | 0 | TYPE_LIMIT | TIF_GTC | | + | buySideProvider | ETH/FEB23 | buy | 1 | 15000 | 0 | TYPE_LIMIT | TIF_GTC | | + | buySideProvider | ETH/FEB23 | buy | 3 | 15900 | 0 | TYPE_LIMIT | TIF_GTC | | + | party | ETH/FEB23 | sell | 3 | 15900 | 0 | TYPE_LIMIT | TIF_GTC | | + | sellSideProvider | ETH/FEB23 | sell | 2 | 15920 | 0 | TYPE_LIMIT | TIF_GTC | sell-2 | + | sellSideProvider | ETH/FEB23 | sell | 1 | 15940 | 0 | TYPE_LIMIT | TIF_GTC | sell-3 | + | sellSideProvider | ETH/FEB23 | sell | 3 | 15960 | 0 | TYPE_LIMIT | TIF_GTC | sell-4 | + | sellSideProvider | ETH/FEB23 | sell | 5 | 15990 | 0 | TYPE_LIMIT | TIF_GTC | sell-5 | + | sellSideProvider | ETH/FEB23 | sell | 2 | 16000 | 0 | TYPE_LIMIT | TIF_GTC | sell-7 | + | sellSideProvider | ETH/FEB23 | sell | 4 | 16020 | 0 | TYPE_LIMIT | TIF_GTC | sell-8 | + | sellSideProvider | ETH/FEB23 | sell | 1 | 100000 | 0 | TYPE_LIMIT | TIF_GTC | | + + + # AC 0009-MRKP-012 + When the network moves ahead "2" blocks + Then the mark price should be "15900" for the market "ETH/FEB23" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/FEB23" + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | buySideProvider | ETH/FEB23 | buy | 1 | 10 | 0 | TYPE_LIMIT | TIF_GTC | cancel1 | + | sellSideProvider | ETH/FEB23 | sell | 1 | 10 | 0 | TYPE_LIMIT | TIF_GTC | cancel2 | + + #When the network moves ahead "1" blocks + #Then the mark price should be "15900" for the market "ETH/FEB23" + + And the trading mode should be "TRADING_MODE_MONITORING_AUCTION" for the market "ETH/FEB23" + + # Broadcast significantly higher prices via the oracle + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | price1.USD.value | 26000 | 0s | + | price2.USD.value | 25900 | 0s | + | price3.USD.value | 25940 | 0s | + + Then the parties cancel the following orders: + | party | reference | + | buySideProvider | cancel1 | + | sellSideProvider | cancel2 | + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | buySideProvider | ETH/FEB23 | buy | 1 | 15900 | 0 | TYPE_LIMIT | TIF_GTC | cancel1 | + | sellSideProvider | ETH/FEB23 | sell | 1 | 15900 | 0 | TYPE_LIMIT | TIF_GTC | cancel2 | + + When the network moves ahead "10" blocks + Then the mark price should be "15900" for the market "ETH/FEB23" + + When the network moves ahead "2" blocks + Then the mark price should be "15900" for the market "ETH/FEB23" + + + \ No newline at end of file diff --git a/core/products/perpetual.go b/core/products/perpetual.go index 6405a17942..7a9f6b7918 100644 --- a/core/products/perpetual.go +++ b/core/products/perpetual.go @@ -74,7 +74,25 @@ func (a *auctionIntervals) update(t int64, enter bool) { } if enter { - a.auctionStart = t + if len(a.auctions) == 0 { + a.auctionStart = t + return + } + + st, nd := a.auctions[len(a.auctions)-2], a.auctions[len(a.auctions)-1] + if t != nd { + a.auctionStart = t + return + } + + a.auctions = slices.Delete(a.auctions, len(a.auctions)-2, len(a.auctions)) + a.auctionStart = st + return + } + + if t == a.auctionStart { + // left an auction as soon as we entered it, no need to log it + a.auctionStart = 0 return } diff --git a/core/products/perpetual_auctions_test.go b/core/products/perpetual_auctions_test.go index a4c67862ce..19c190b06c 100644 --- a/core/products/perpetual_auctions_test.go +++ b/core/products/perpetual_auctions_test.go @@ -235,6 +235,32 @@ func testPastFundingPayment(t *testing.T) { assert.Equal(t, int64(expectedTWAP), fundingPayment.Int64()) } +func TestZeroLengthAuctionPeriods(t *testing.T) { + perp := testPerpetual(t) + defer perp.ctrl.Finish() + + // set of the data points such that difference in averages is 0 + points := getTestDataPoints(t) + + // tell the perpetual that we are ready to accept settlement stuff + whenLeaveOpeningAuction(t, perp, points[0].t) + + // enter auction + whenAuctionStateChanges(t, perp, points[0].t, true) + + // send in some data points with a TWAP difference + submitDataWithDifference(t, perp, points, 10) + + // leave auction + whenAuctionStateChanges(t, perp, points[len(points)-1].t, false) + + // but then enter again straight away + whenAuctionStateChanges(t, perp, points[len(points)-1].t, true) + + fundingPayment := whenTheFundingPeriodEnds(t, perp, points[len(points)-1].t) + assert.Equal(t, "0", fundingPayment.String()) +} + func TestFairgroundPanic(t *testing.T) { perp := testPerpetual(t) defer perp.ctrl.Finish()