diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b4779bb36..c66c9533b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - [11542](https://github.com/vegaprotocol/vega/issues/11542) - Fix non determinism in lottery ranking. - [11616](https://github.com/vegaprotocol/vega/issues/11616) - `AMM` tradable volume now calculated purely in positions to prevent loss of precision. - [11544](https://github.com/vegaprotocol/vega/issues/11544) - Fix empty candles stream. +- [11619](https://github.com/vegaprotocol/vega/issues/11619) - Fix `EstimatePositions` API for capped futures. - [11579](https://github.com/vegaprotocol/vega/issues/11579) - Spot calculate fee on amend, use order price if no amended price is provided. - [11585](https://github.com/vegaprotocol/vega/issues/11585) - Initialise rebate stats service in API. - [11592](https://github.com/vegaprotocol/vega/issues/11592) - Fix the order of calls at end of epoch between rebate engine and market tracker. diff --git a/datanode/api/trading_data_v2.go b/datanode/api/trading_data_v2.go index d37c7930e9..7c0c77ff66 100644 --- a/datanode/api/trading_data_v2.go +++ b/datanode/api/trading_data_v2.go @@ -3650,8 +3650,6 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est return nil, formatE(ErrMarketServiceGetByID, err) } - cap, hasCap := mkt.HasCap() - collateralAvailable := marginAccountBalance crossMarginMode := req.MarginMode == types.MarginModeCrossMargin if crossMarginMode { @@ -3681,6 +3679,15 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est dPriceFactor := priceFactor + var mdpCap num.Decimal + cap, hasCap := mkt.HasCap() + if hasCap { + mdpCap, err = num.DecimalFromString(cap.MaxPrice) + if err != nil { + formatE(ErrMarketServiceGetByID, err) + } + } + buyOrders := make([]*risk.OrderInfo, 0, len(req.Orders)) sellOrders := make([]*risk.OrderInfo, 0, len(req.Orders)) @@ -3698,6 +3705,10 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est return nil, ErrInvalidOrderPrice } + if hasCap && p.GreaterThanOrEqual(mdpCap) { + return nil, formatE(ErrInvalidOrderPrice, errors.New("outside of market-cap range")) + } + price = t.scaleDecimalFromMarketToAssetPrice(p, dPriceFactor) switch o.Side { @@ -3794,8 +3805,8 @@ func (t *TradingDataServiceV2) EstimatePosition(ctx context.Context, req *v2.Est auctionPrice, cap, avgEntryPrice, + dPriceFactor, ) - marginEstimate := &v2.MarginEstimate{ WorstCase: implyMarginLevels(wMaintenance, orderMargin, dMarginFactor, mkt.TradableInstrument.MarginCalculator.ScalingFactors, "", req.MarketId, asset, isolatedMarginMode), BestCase: implyMarginLevels(bMaintenance, orderMargin, dMarginFactor, mkt.TradableInstrument.MarginCalculator.ScalingFactors, "", req.MarketId, asset, isolatedMarginMode), @@ -3921,6 +3932,7 @@ func (t *TradingDataServiceV2) computeMarginRange( marginFactor, auctionPrice num.Decimal, cap *vega.FutureCap, averageEntryPrice num.Decimal, + priceFactor num.Decimal, ) (num.Decimal, num.Decimal, num.Decimal) { bOrders, sOrders := buyOrders, sellOrders orderMargin := num.DecimalZero() @@ -3948,7 +3960,12 @@ func (t *TradingDataServiceV2) computeMarginRange( // this is a special case for fully collateralised capped future markets if cap != nil && cap.FullyCollateralised != nil && *cap.FullyCollateralised { - orderMargin = calcOrderMarginIsolatedModeCappedAndFullyCollateralised(bNonMarketOrders, sNonMarketOrders, cap) + cappedPrice := t.scaleDecimalFromMarketToAssetPrice( + num.MustDecimalFromString(cap.MaxPrice), + priceFactor, + ) + + orderMargin = calcOrderMarginIsolatedModeCappedAndFullyCollateralised(bNonMarketOrders, sNonMarketOrders, cappedPrice) } else { orderMargin = risk.CalcOrderMarginIsolatedMode(openVolume, bNonMarketOrders, sNonMarketOrders, positionFactor, marginFactor, auctionPrice) } @@ -3957,7 +3974,12 @@ func (t *TradingDataServiceV2) computeMarginRange( 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) + cappedPrice := t.scaleDecimalFromMarketToAssetPrice( + num.MustDecimalFromString(cap.MaxPrice), + priceFactor, + ) + + worst = calcPositionMarginCappedAndFullyCollateralised(bOrders, sOrders, cappedPrice, openVolume, averageEntryPrice) best = worst } else { worst = risk.CalculateMaintenanceMarginWithSlippageFactors(openVolume, bOrders, sOrders, marketObservable, positionFactor, linearSlippageFactor, quadraticSlippageFactor, riskFactors.Long, riskFactors.Short, fundingPaymentPerUnitPosition, auction, auctionPrice) @@ -3970,7 +3992,7 @@ func (t *TradingDataServiceV2) computeMarginRange( func calcPositionMarginCappedAndFullyCollateralised( buyOrders []*risk.OrderInfo, sellOrders []*risk.OrderInfo, - cap *vega.FutureCap, + priceCap num.Decimal, openVolume int64, openVolumeAverageEntryPrice decimal.Decimal, ) decimal.Decimal { @@ -3981,8 +4003,6 @@ func calcPositionMarginCappedAndFullyCollateralised( // if short: // - (priceCap - averageEntryPrice) * positionSize - priceCap := num.MustDecimalFromString(cap.MaxPrice) - positionSize := openVolume totalVolume := openVolume ongoing := openVolumeAverageEntryPrice.Mul(num.DecimalFromInt64(openVolume)) @@ -3995,6 +4015,7 @@ func calcPositionMarginCappedAndFullyCollateralised( size := int64(v.TrueRemaining) positionSize += size totalVolume += size + ongoing = ongoing.Add(v.Price.Mul(num.DecimalFromInt64(size))) } @@ -4022,20 +4043,19 @@ func calcPositionMarginCappedAndFullyCollateralised( return priceCap.Sub(averageEntryPrice).Mul(num.DecimalFromInt64(positionSize)) } - return priceCap.Mul(num.DecimalFromInt64(positionSize)) + return averageEntryPrice.Mul(num.DecimalFromInt64(positionSize)) } func calcOrderMarginIsolatedModeCappedAndFullyCollateralised( buyOrders []*risk.OrderInfo, sellOrders []*risk.OrderInfo, - cap *vega.FutureCap, + cappedPrice num.Decimal, ) 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 { diff --git a/datanode/api/trading_data_v2_test.go b/datanode/api/trading_data_v2_test.go index d0c8b959d2..4ec746ebe9 100644 --- a/datanode/api/trading_data_v2_test.go +++ b/datanode/api/trading_data_v2_test.go @@ -295,6 +295,137 @@ func TestEstimateFees(t *testing.T) { require.Equal(t, "50000", estimate.Fee.TreasuryFee) } +func TestEstimatePositionCappedFuture(t *testing.T) { + ctrl := gomock.NewController(t) + ctx := context.Background() + assetId := "assetID" + marketId := "marketID" + + assetDecimals := 8 + marketDecimals := 3 + positionDecimalPlaces := 2 + initialMarginScalingFactor := 1.5 + linearSlippageFactor := num.DecimalFromFloat(0.005) + quadraticSlippageFactor := num.DecimalZero() + rfLong := num.DecimalFromFloat(0.1) + rfShort := num.DecimalFromFloat(0.2) + + auctionEnd := int64(0) + fundingPayment := 1234.56789 + + asset := entities.Asset{ + Decimals: assetDecimals, + } + + tickSize := num.DecimalOne() + + mkt := entities.Market{ + DecimalPlaces: marketDecimals, + PositionDecimalPlaces: positionDecimalPlaces, + LinearSlippageFactor: &linearSlippageFactor, + QuadraticSlippageFactor: &quadraticSlippageFactor, + TradableInstrument: entities.TradableInstrument{ + TradableInstrument: &vega.TradableInstrument{ + Instrument: &vega.Instrument{ + Product: &vega.Instrument_Future{ + Future: &vega.Future{ + SettlementAsset: assetId, + Cap: &vega.FutureCap{ + MaxPrice: floatToStringWithDp(100, marketDecimals), + FullyCollateralised: ptr.From(true), + }, + }, + }, + }, + MarginCalculator: &vega.MarginCalculator{ + ScalingFactors: &vega.ScalingFactors{ + SearchLevel: initialMarginScalingFactor * 0.9, + InitialMargin: initialMarginScalingFactor, + CollateralRelease: initialMarginScalingFactor * 1.1, + }, + }, + }, + }, + TickSize: &tickSize, + } + + rf := entities.RiskFactor{ + Long: rfLong, + Short: rfShort, + } + + assetService := mocks.NewMockAssetService(ctrl) + marketService := mocks.NewMockMarketsService(ctrl) + riskFactorService := mocks.NewMockRiskFactorService(ctrl) + + assetService.EXPECT().GetByID(ctx, assetId).Return(asset, nil).AnyTimes() + marketService.EXPECT().GetByID(ctx, marketId).Return(mkt, nil).AnyTimes() + riskFactorService.EXPECT().GetMarketRiskFactors(ctx, marketId).Return(rf, nil).AnyTimes() + + mktData := entities.MarketData{ + MarkPrice: num.DecimalFromFloat(123.456 * math.Pow10(marketDecimals)), + AuctionEnd: auctionEnd, + ProductData: &entities.ProductData{ + ProductData: &vega.ProductData{ + Data: &vega.ProductData_PerpetualData{ + PerpetualData: &vega.PerpetualData{ + FundingPayment: fmt.Sprintf("%f", fundingPayment), + FundingRate: "0.05", + }, + }, + }, + }, + } + marketDataService := mocks.NewMockMarketDataService(ctrl) + marketDataService.EXPECT().GetMarketDataByID(ctx, marketId).Return(mktData, nil).AnyTimes() + + apiService := api.TradingDataServiceV2{ + AssetService: assetService, + MarketsService: marketService, + MarketDataService: marketDataService, + RiskFactorService: riskFactorService, + } + + req := &v2.EstimatePositionRequest{ + MarketId: marketId, + OpenVolume: 0, + AverageEntryPrice: "0", + Orders: []*v2.OrderInfo{ + { + Side: entities.SideBuy, + Price: floatToStringWithDp(100, marketDecimals), + Remaining: uint64(1 * math.Pow10(positionDecimalPlaces)), + IsMarketOrder: false, + }, + }, + MarginAccountBalance: fmt.Sprintf("%f", 100*math.Pow10(assetDecimals)), + GeneralAccountBalance: fmt.Sprintf("%f", 1000*math.Pow10(assetDecimals)), + OrderMarginAccountBalance: "0", + MarginMode: vega.MarginMode_MARGIN_MODE_CROSS_MARGIN, + MarginFactor: ptr.From("0"), + } + + // error because hypothetical order is outide of max range + _, err := apiService.EstimatePosition(ctx, req) + require.Error(t, err) + + req.Orders = []*v2.OrderInfo{ + { + Side: entities.SideBuy, + Price: floatToStringWithDp(50, marketDecimals), + Remaining: uint64(1 * math.Pow10(positionDecimalPlaces)), + IsMarketOrder: false, + }, + } + + // error because hypothetical order is outide of max range + resp, err := apiService.EstimatePosition(ctx, req) + require.NoError(t, err) + + assert.Equal(t, "500000000000", resp.Margin.BestCase.MaintenanceMargin) + assert.Equal(t, "500000000000", resp.Margin.WorstCase.MaintenanceMargin) +} + func TestEstimatePosition(t *testing.T) { ctrl := gomock.NewController(t) ctx := context.TODO()