diff --git a/.spelling b/.spelling index a15619220fd..aed5508655e 100644 --- a/.spelling +++ b/.spelling @@ -23,6 +23,7 @@ cleanup Cleanup clef codegen +collateralised cometbft config cyclomatic diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ef46c469d..7a0fff162fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - [11026](https://github.com/vegaprotocol/vega/issues/11026) - Add API flag to get paid liquidity fees for a `vAMM` using the parent key. - [11027](https://github.com/vegaprotocol/vega/issues/11027) - Add API filters to get fees and rewards by market, across epochs. - [10360](https://github.com/vegaprotocol/vega/issues/10360) - Scale funding payment by fraction of period spent outside of auction. +- [11452](https://github.com/vegaprotocol/vega/issues/11452) - Implement alternative estimations for fully collateralised capped future. ### 🐛 Fixes @@ -49,17 +50,17 @@ - [11319](https://github.com/vegaprotocol/vega/issues/11319) - Do not leak Ethereum client secrets in the logs. - [11336](https://github.com/vegaprotocol/vega/issues/11336) - Add support for decay factor in governance recurring transfers and report the proposal amount rather than 0 when the proposal gets enacted. - [11368](https://github.com/vegaprotocol/vega/issues/11368) - Add support for update vesting stats in REST API and fix summing the quantum balance for vesting stats. -- [11380](https://github.com/vegaprotocol/vega/issues/11380) - Handle broken stop orders in prepare proposal. +- [11380](https://github.com/vegaprotocol/vega/issues/11380) - Handle broken stop orders in prepare proposal. - [11136](https://github.com/vegaprotocol/vega/issues/11136) - Fix premature invocation of post commit hooks in case of fee stats event. -- [11409](https://github.com/vegaprotocol/vega/issues/11409) - When updating a capped market - copy the cap from the existing market definition. -- [11415](https://github.com/vegaprotocol/vega/issues/11415) - End long block auction when expired. -- [11419](https://github.com/vegaprotocol/vega/issues/11419) - Fix long block auction extension to be calculated from current time. -- [11438](https://github.com/vegaprotocol/vega/issues/11438) - Add missing assignment to epoch to and from in `gameTeamScores` resolvers. -- [11442](https://github.com/vegaprotocol/vega/issues/11442) - Add validation to `submitAMM` to ensure the curves can be successfully generated. -- [11448](https://github.com/vegaprotocol/vega/issues/11448) - Fix team game score query with epoch filter. -- [11457](https://github.com/vegaprotocol/vega/issues/11457) - Fix cursor column ordering for game scores. -- [11454](https://github.com/vegaprotocol/vega/issues/11454) - Ensure ended transfers proper handling. - +- [11409](https://github.com/vegaprotocol/vega/issues/11409) - When updating a capped market - copy the cap from the existing market definition. +- [11415](https://github.com/vegaprotocol/vega/issues/11415) - End long block auction when expired. +- [11419](https://github.com/vegaprotocol/vega/issues/11419) - Fix long block auction extension to be calculated from current time. +- [11438](https://github.com/vegaprotocol/vega/issues/11438) - Add missing assignment to epoch to and from in `gameTeamScores` resolvers. +- [11442](https://github.com/vegaprotocol/vega/issues/11442) - Add validation to `submitAMM` to ensure the curves can be successfully generated. +- [11448](https://github.com/vegaprotocol/vega/issues/11448) - Fix team game score query with epoch filter. +- [11457](https://github.com/vegaprotocol/vega/issues/11457) - Fix cursor column ordering for game scores. +- [11454](https://github.com/vegaprotocol/vega/issues/11454) - Ensure ended transfers proper handling. + ## 0.76.1 diff --git a/core/risk/liquidation_calculation.go b/core/risk/liquidation_calculation.go index 3ba6dfcf974..c2ce3b741a3 100644 --- a/core/risk/liquidation_calculation.go +++ b/core/risk/liquidation_calculation.go @@ -28,6 +28,16 @@ type OrderInfo struct { IsMarketOrder bool } +// Clone - Not really necessary, just added to avoid future pointers +// copy if some were added. +func (o *OrderInfo) Clone() *OrderInfo { + return &OrderInfo{ + TrueRemaining: o.TrueRemaining, + Price: o.Price.Copy(), + IsMarketOrder: o.IsMarketOrder, + } +} + func CalculateLiquidationPriceWithSlippageFactors(sizePosition int64, buyOrders, sellOrders []*OrderInfo, currentPrice, collateralAvailable num.Decimal, positionFactor, linearSlippageFactor, quadraticSlippageFactor, riskFactorLong, riskFactorShort, fundingPaymentPerUnitPosition num.Decimal, isolatedMarginMode bool, marginFactor num.Decimal) (liquidationPriceForOpenVolume, liquidationPriceWithBuyOrders, liquidationPriceWithSellOrders num.Decimal, err error) { openVolume := num.DecimalFromInt64(sizePosition).Div(positionFactor) diff --git a/datanode/api/trading_data_v2.go b/datanode/api/trading_data_v2.go index 7a8438d66f7..a375c9016e1 100644 --- a/datanode/api/trading_data_v2.go +++ b/datanode/api/trading_data_v2.go @@ -3526,6 +3526,9 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est if err != nil { return nil, formatE(ErrMarketServiceGetByID, err) } + + cap, hasCap := mkt.HasCap() + collateralAvailable := marginAccountBalance crossMarginMode := req.MarginMode == types.MarginModeCrossMargin if crossMarginMode { @@ -3666,6 +3669,8 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est req.MarginMode, dMarginFactor, auctionPrice, + cap, + avgEntryPrice, ) marginEstimate := &v2.MarginEstimate{ @@ -3729,6 +3734,36 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est wWithSell = t.scaleDecimalFromAssetToMarketPrice(wWithSell, dPriceFactor) } + f := func(volume int64) (worst, best decimal.Decimal) { + // if party is long, then liquidation is 0 + if volume >= 0 { + return num.DecimalZero(), num.DecimalZero() + } + + // if its short we use the size of capPrice + return num.MustDecimalFromString(cap.MaxPrice), num.MustDecimalFromString(cap.MaxPrice) // can't fail coming from the DB + } + + // no worst or best case in this case, so just setting the same on boths + if hasCap && cap.FullyCollateralised != nil && *cap.FullyCollateralised { + // openVolume first + wPositionOnly, bPositionOnly = f(req.OpenVolume) + + // then including buyOrders + incBuyOrders := req.OpenVolume + for _, v := range buyOrders { + incBuyOrders += int64(v.TrueRemaining) + } + wWithBuy, bWithBuy = f(incBuyOrders) + + // then including sellOrders + incSellOrders := req.OpenVolume + for _, v := range sellOrders { + incSellOrders += int64(v.TrueRemaining) + } + wWithSell, bWithSell = f(incSellOrders) + } + liquidationEstimate := &v2.LiquidationEstimate{ WorstCase: &v2.LiquidationPrice{ OpenVolumeOnly: wPositionOnly.Round(0).String(), @@ -3761,6 +3796,8 @@ func (t *TradingDataServiceV2) computeMarginRange( auction bool, marginMode vega.MarginMode, marginFactor, auctionPrice num.Decimal, + cap *vega.FutureCap, + averageEntryPrice num.Decimal, ) (num.Decimal, num.Decimal, num.Decimal) { bOrders, sOrders := buyOrders, sellOrders orderMargin := num.DecimalZero() @@ -3776,6 +3813,7 @@ func (t *TradingDataServiceV2) computeMarginRange( } } sOrders = []*risk.OrderInfo{} + sNonMarketOrders := []*risk.OrderInfo{} for _, o := range sellOrders { if o.IsMarketOrder { @@ -3784,15 +3822,121 @@ func (t *TradingDataServiceV2) computeMarginRange( sNonMarketOrders = append(sNonMarketOrders, o) } } - orderMargin = risk.CalcOrderMarginIsolatedMode(openVolume, bNonMarketOrders, sNonMarketOrders, positionFactor, marginFactor, auctionPrice) + + // this is a special case for fully collateralised capped future markets + if cap != nil && cap.FullyCollateralised != nil && *cap.FullyCollateralised { + orderMargin = calcOrderMarginIsolatedModeCappedAndFullyCollateralised(bNonMarketOrders, sNonMarketOrders, cap) + } else { + orderMargin = risk.CalcOrderMarginIsolatedMode(openVolume, bNonMarketOrders, sNonMarketOrders, positionFactor, marginFactor, auctionPrice) + } } - worst := risk.CalculateMaintenanceMarginWithSlippageFactors(openVolume, bOrders, sOrders, marketObservable, positionFactor, linearSlippageFactor, quadraticSlippageFactor, riskFactors.Long, riskFactors.Short, fundingPaymentPerUnitPosition, auction, auctionPrice) - best := risk.CalculateMaintenanceMarginWithSlippageFactors(openVolume, bOrders, sOrders, marketObservable, positionFactor, num.DecimalZero(), num.DecimalZero(), riskFactors.Long, riskFactors.Short, fundingPaymentPerUnitPosition, auction, auctionPrice) + var worst, best num.Decimal + // this is a special case for fully collateralised capped future markets + if cap != nil && cap.FullyCollateralised != nil && *cap.FullyCollateralised { + worst = calcPositionMarginCappedAndFullyCollateralised(bOrders, sOrders, cap, openVolume, averageEntryPrice) + best = worst + } else { + worst = risk.CalculateMaintenanceMarginWithSlippageFactors(openVolume, bOrders, sOrders, marketObservable, positionFactor, linearSlippageFactor, quadraticSlippageFactor, riskFactors.Long, riskFactors.Short, fundingPaymentPerUnitPosition, auction, auctionPrice) + best = risk.CalculateMaintenanceMarginWithSlippageFactors(openVolume, bOrders, sOrders, marketObservable, positionFactor, num.DecimalZero(), num.DecimalZero(), riskFactors.Long, riskFactors.Short, fundingPaymentPerUnitPosition, auction, auctionPrice) + } return worst, best, orderMargin } +func calcPositionMarginCappedAndFullyCollateralised( + buyOrders []*risk.OrderInfo, + sellOrders []*risk.OrderInfo, + cap *vega.FutureCap, + openVolume int64, + openVolumeAverageEntryPrice decimal.Decimal, +) decimal.Decimal { + // get average entry price over all orders + // and the final volume. + // then if long: + // - averageEntryPrice * positionSize + // if short: + // - (priceCap - averageEntryPrice) * positionSize + + priceCap := num.MustDecimalFromString(cap.MaxPrice) + + positionSize := openVolume + totalVolume := openVolume + ongoing := openVolumeAverageEntryPrice.Mul(num.DecimalFromInt64(openVolume)) + for _, v := range buyOrders { + price := v.Price + if price.GreaterThan(priceCap) { + price = priceCap + } + + size := int64(v.TrueRemaining) + positionSize += size + totalVolume += size + ongoing = ongoing.Add(v.Price.Mul(num.DecimalFromInt64(size))) + } + + for _, v := range sellOrders { + price := v.Price + if price.GreaterThan(priceCap) { + price = priceCap + } + + size := int64(v.TrueRemaining) + positionSize -= size // only thing changing here really + totalVolume += size + ongoing = ongoing.Add(v.Price.Mul(num.DecimalFromInt64(size))) + } + + averageEntryPrice := ongoing.Div(num.DecimalFromInt64(totalVolume)) + + if positionSize < 0 { + // short position + positionSize = -positionSize + return priceCap.Sub(averageEntryPrice).Mul(num.DecimalFromInt64(positionSize)) + } + + return priceCap.Mul(num.DecimalFromInt64(positionSize)) +} + +func calcOrderMarginIsolatedModeCappedAndFullyCollateralised( + buyOrders []*risk.OrderInfo, + sellOrders []*risk.OrderInfo, + cap *vega.FutureCap, +) decimal.Decimal { + // long order margin: + // - price * positionSize + // short order marign: + // - (cappedPrice - price) * positionSize + + cappedPrice := num.MustDecimalFromString(cap.MaxPrice) + marginBuy, marginSell := num.DecimalZero(), num.DecimalZero() + + for _, v := range buyOrders { + price := v.Price + if v.Price.GreaterThan(cappedPrice) { + price = cappedPrice + } + + marginBuy.Add(price.Mul(num.DecimalFromInt64(int64(v.TrueRemaining)))) + } + + for _, v := range sellOrders { + price := v.Price + if v.Price.GreaterThan(cappedPrice) { + price = cappedPrice + } + + price = cappedPrice.Sub(price) + marginSell.Add(price.Mul(num.DecimalFromInt64(int64(v.TrueRemaining)))) + } + + if marginBuy.GreaterThan(marginSell) { + return marginBuy + } + + return marginSell +} + // ListNetworkParameters returns a list of network parameters. func (t *TradingDataServiceV2) ListNetworkParameters(ctx context.Context, req *v2.ListNetworkParametersRequest) (*v2.ListNetworkParametersResponse, error) { defer metrics.StartAPIRequestAndTimeGRPC("ListNetworkParametersV2")() diff --git a/datanode/entities/market.go b/datanode/entities/market.go index 003ab72e62c..122317cac84 100644 --- a/datanode/entities/market.go +++ b/datanode/entities/market.go @@ -73,6 +73,18 @@ type Market struct { EnableTXReordering bool } +func (m *Market) HasCap() (cap *vega.FutureCap, hasCap bool) { + if inst := m.TradableInstrument.Instrument; inst != nil { + if fut := inst.GetFuture(); fut != nil { + if cap := fut.GetCap(); cap != nil { + return cap, true + } + } + } + + return nil, false +} + type MarketCursor struct { VegaTime time.Time `json:"vegaTime"` ID MarketID `json:"id"`