Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: scale map to asset decimals before comparisons in estimate-posit… #11620

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading