diff --git a/core/integration/features/settlement/0053-PERP-027-twap-calculation.feature b/core/integration/features/settlement/0053-PERP-027-twap-calculation.feature new file mode 100644 index 00000000000..bceec835584 --- /dev/null +++ b/core/integration/features/settlement/0053-PERP-027-twap-calculation.feature @@ -0,0 +1,143 @@ +Feature: Test internal and external twap calculation + + Background: + # epoch time is 1602806400 + Given time is updated to "2020-10-16T00:00:00Z" + And the perpetual oracles from "0xCAFECAFE1": + | name | asset | settlement property | settlement type | schedule property | schedule type | margin funding factor | interest rate | clamp lower bound | clamp upper bound | quote name | settlement decimals | + | perp-oracle | USD | perp.ETH.value | TYPE_INTEGER | perp.funding.cue | TYPE_TIMESTAMP | 0.5 | 0.00 | 0.0 | 0.0 | ETH | 1 | + And the liquidity sla params named "SLA": + | price range | commitment min time fraction | performance hysteresis epochs | sla competition factor | + | 1.0 | 0.5 | 1 | 1.0 | + + And the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | position decimal places | market type | sla params | + | ETH/DEC19 | ETH | USD | default-simple-risk-model-3 | default-margin-calculator | 1 | default-none | default-none | perp-oracle | 1e6 | 1e6 | -3 | perp | default-futures | + + And the following network parameters are set: + | name | value | + | market.auction.minimumDuration | 1 | + | limits.markets.maxPeggedOrders | 2 | + And the following network parameters are set: + | name | value | + | network.markPriceUpdateMaximumFrequency | 0s | + And the average block duration is "1" + + @Perpetual @twap + Scenario: 0053-PERP-027 Internal and External TWAP calculation + Given the following network parameters are set: + | name | value | + | network.markPriceUpdateMaximumFrequency | 120s | + And the parties deposit on asset's general account the following amount: + | party | asset | amount | + | party1 | USD | 10000000 | + | party2 | USD | 10000000 | + | party3 | USD | 10000000 | + | aux | USD | 100000000 | + | aux2 | USD | 100000000 | + | lpprov | USD | 100000000 | + + When the parties submit the following liquidity provision: + | id | party | market id | commitment amount | fee | lp type | + | lp1 | lpprov | ETH/DEC19 | 100000 | 0.001 | submission | + + # move market to continuous + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | lpprov | ETH/DEC19 | buy | 1000 | 5 | 0 | TYPE_LIMIT | TIF_GTC | + | aux | ETH/DEC19 | buy | 1 | 5 | 0 | TYPE_LIMIT | TIF_GTC | + | lpprov | ETH/DEC19 | sell | 1000 | 15 | 0 | TYPE_LIMIT | TIF_GTC | + | aux | ETH/DEC19 | sell | 1 | 15 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | ETH/DEC19 | buy | 1 | 11 | 0 | TYPE_LIMIT | TIF_GTC | + | aux | ETH/DEC19 | sell | 1 | 11 | 0 | TYPE_LIMIT | TIF_GTC | + + And the market data for the market "ETH/DEC19" should be: + | target stake | supplied stake | + | 12100 | 100000 | + Then the opening auction period ends for market "ETH/DEC19" + + Given the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/DEC19" + When time is updated to "2020-10-16T00:02:00Z" + # 1602806400 + 120s = 1602806520 + # funding period is ended with perp.funding.cue + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 110 | -1s | + | perp.funding.cue | 1602806520 | 0s | + + # 1 min in to the next funding period + Given time is updated to "2020-10-16T00:03:00Z" + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | party1 | ETH/DEC19 | buy | 1 | 11 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/DEC19 | sell | 1 | 11 | 1 | TYPE_LIMIT | TIF_GTC | + + And the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 90 | -1s | + + # 3 min in to the next funding period + Given time is updated to "2020-10-16T00:05:00Z" + And the mark price should be "11" for the market "ETH/DEC19" + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | party1 | ETH/DEC19 | buy | 1 | 10 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/DEC19 | sell | 1 | 10 | 1 | TYPE_LIMIT | TIF_GTC | + + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 100 | -1s | + + + # 5 min in to the next funding period + Given time is updated to "2020-10-16T00:07:00Z" + And the mark price should be "10" for the market "ETH/DEC19" + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | party1 | ETH/DEC19 | buy | 1 | 9 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/DEC19 | sell | 1 | 9 | 1 | TYPE_LIMIT | TIF_GTC | + + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 120 | -1s | + + # 6 min in to the funding period emit spot price + Given time is updated to "2020-10-16T00:08:00Z" + And the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 110 | -1s | + + # 7 mins in to the funding period + When time is updated to "2020-10-16T00:09:00Z" + + And the mark price should be "9" for the market "ETH/DEC19" + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | party1 | ETH/DEC19 | buy | 1 | 8 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/DEC19 | sell | 1 | 8 | 1 | TYPE_LIMIT | TIF_GTC | + + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 80 | -1s | + + # 9 min in to the next funding period + Given time is updated to "2020-10-16T00:11:00Z" + And the mark price should be "8" for the market "ETH/DEC19" + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | party1 | ETH/DEC19 | buy | 1 | 7 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/DEC19 | sell | 1 | 7 | 1 | TYPE_LIMIT | TIF_GTC | + + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.ETH.value | 140 | -1s | + + # if the funding period ended here, check the twap + Given time is updated to "2020-10-16T00:12:00Z" + + # in theory internal TWAP = 9.3 external TWAP = 10.3 + # but these are type int so the decimal is truncated + Then the product data for the market "ETH/DEC19" should be: + | internal twap | external twap | + | 9 | 10 | diff --git a/core/integration/main_test.go b/core/integration/main_test.go index 05291ecca79..2c7f4bd7914 100644 --- a/core/integration/main_test.go +++ b/core/integration/main_test.go @@ -491,6 +491,9 @@ func InitializeScenario(s *godog.ScenarioContext) { s.Step(`^the market data for the market "([^"]+)" should be:$`, func(marketID string, table *godog.Table) error { return steps.TheMarketDataShouldBe(execsetup.executionEngine, marketID, table) }) + s.Step(`^the product data for the market "([^"]+)" should be:$`, func(marketID string, table *godog.Table) error { + return steps.TheProductDataShouldBe(execsetup.executionEngine, marketID, table) + }) s.Step(`the auction ends with a traded volume of "([^"]+)" at a price of "([^"]+)"`, func(vol, price string) error { now := execsetup.timeService.GetTimeNow() return steps.TheAuctionTradedVolumeAndPriceShouldBe(execsetup.broker, vol, price, now) diff --git a/core/integration/steps/the_product_data_should_be.go b/core/integration/steps/the_product_data_should_be.go new file mode 100644 index 00000000000..99f3ab036a5 --- /dev/null +++ b/core/integration/steps/the_product_data_should_be.go @@ -0,0 +1,100 @@ +// Copyright (C) 2023 Gobalsky Labs Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package steps + +import ( + "fmt" + + "code.vegaprotocol.io/vega/core/types" + + "github.com/cucumber/godog" +) + +func TheProductDataShouldBe(engine Execution, mID string, data *godog.Table) error { + actual, err := engine.GetMarketData(mID) + if err != nil { + return err + } + for _, row := range parseProductDataTable(data) { + expect := ProductDataWrapper{ + row: row, + } + err := checkProductData(*actual.ProductData, expect) + if err != nil { + return err + } + } + return nil +} + +func checkProductData(pd types.ProductData, row ProductDataWrapper) error { + expectedInternalTwap := row.InternalTWAP() + actualInternalTwap := pd.Data.IntoProto().GetPerpetualData().InternalTwap + + if expectedInternalTwap != actualInternalTwap { + return fmt.Errorf("expected '%s' for InternalTWAP, instead got '%s'", expectedInternalTwap, actualInternalTwap) + } + + expectedExternalTwap := row.ExternalTWAP() + actualExternalTwap := pd.Data.IntoProto().GetPerpetualData().ExternalTwap + if expectedExternalTwap != actualExternalTwap { + return fmt.Errorf("expected '%s' for InternalTWAP, instead got '%s'", expectedExternalTwap, actualExternalTwap) + } + + expectedFundingPayment, b := row.FundingPayment() + actualFundingPayment := pd.Data.IntoProto().GetPerpetualData().FundingPayment + if b && expectedFundingPayment != actualFundingPayment { + return fmt.Errorf("expected '%s' for InternalTWAP, instead got '%s'", expectedFundingPayment, actualFundingPayment) + } + + expectedFundingRate, b := row.FundingRate() + actualFundingRate := pd.Data.IntoProto().GetPerpetualData().FundingRate + if b && expectedFundingRate != actualFundingRate { + return fmt.Errorf("expected '%s' for InternalTWAP, instead got '%s'", expectedFundingRate, actualFundingRate) + } + + return nil +} + +func parseProductDataTable(table *godog.Table) []RowWrapper { + return StrictParseTable(table, []string{ + "internal twap", + "external twap", + }, []string{ + "funding payment", + "funding rate", + }) +} + +type ProductDataWrapper struct { + row RowWrapper +} + +func (f ProductDataWrapper) InternalTWAP() string { + return f.row.MustStr("internal twap") +} + +func (f ProductDataWrapper) ExternalTWAP() string { + return f.row.MustStr("external twap") +} + +func (f ProductDataWrapper) FundingPayment() (string, bool) { + return f.row.StrB("funding payment") +} + +func (f ProductDataWrapper) FundingRate() (string, bool) { + return f.row.StrB("funding rate") +}