Skip to content

Commit

Permalink
Merge pull request #11620 from vegaprotocol/11619-capped-estimates
Browse files Browse the repository at this point in the history
fix: scale map to asset decimals before comparisons in estimate-posit…
  • Loading branch information
wwestgarth authored Aug 28, 2024
2 parents ecd9fe4 + 6929dc6 commit 94d5319
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 31 additions & 11 deletions datanode/api/trading_data_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))

Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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))
Expand All @@ -3995,6 +4015,7 @@ func calcPositionMarginCappedAndFullyCollateralised(
size := int64(v.TrueRemaining)
positionSize += size
totalVolume += size

ongoing = ongoing.Add(v.Price.Mul(num.DecimalFromInt64(size)))
}

Expand Down Expand Up @@ -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 {
Expand Down
131 changes: 131 additions & 0 deletions datanode/api/trading_data_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 94d5319

Please sign in to comment.