diff --git a/plugin/evm/orderbook/config_service.go b/plugin/evm/orderbook/config_service.go index f5d0660c06..85c686df23 100644 --- a/plugin/evm/orderbook/config_service.go +++ b/plugin/evm/orderbook/config_service.go @@ -18,6 +18,7 @@ type IConfigService interface { getMinSizeRequirement(market Market) *big.Int GetActiveMarketsCount() int64 GetUnderlyingPrices() []*big.Int + GetMidPrices() []*big.Int GetCollaterals() []hu.Collateral GetLastPremiumFraction(market Market, trader *common.Address) *big.Int GetCumulativePremiumFraction(market Market) *big.Int @@ -84,6 +85,10 @@ func (cs *ConfigService) GetUnderlyingPrices() []*big.Int { return bibliophile.GetUnderlyingPrices(cs.getStateAtCurrentBlock()) } +func (cs *ConfigService) GetMidPrices() []*big.Int { + return bibliophile.GetMidPrices(cs.getStateAtCurrentBlock()) +} + func (cs *ConfigService) GetCollaterals() []hu.Collateral { return bibliophile.GetCollaterals(cs.getStateAtCurrentBlock()) } diff --git a/plugin/evm/orderbook/hubbleutils/data_structures.go b/plugin/evm/orderbook/hubbleutils/data_structures.go index fbae81a41e..8bca2d54a0 100644 --- a/plugin/evm/orderbook/hubbleutils/data_structures.go +++ b/plugin/evm/orderbook/hubbleutils/data_structures.go @@ -23,9 +23,6 @@ type Market = int type Position struct { OpenNotional *big.Int `json:"open_notional"` Size *big.Int `json:"size"` - // UnrealisedFunding *big.Int `json:"unrealised_funding"` - // LastPremiumFraction *big.Int `json:"last_premium_fraction"` - // LiquidationThreshold *big.Int `json:"liquidation_threshold"` } type Trader struct { diff --git a/plugin/evm/orderbook/hubbleutils/margin_math.go b/plugin/evm/orderbook/hubbleutils/margin_math.go index 3bd8bdec53..e36230727b 100644 --- a/plugin/evm/orderbook/hubbleutils/margin_math.go +++ b/plugin/evm/orderbook/hubbleutils/margin_math.go @@ -1,15 +1,17 @@ package hubbleutils import ( + "math" "math/big" ) type HubbleState struct { Assets []Collateral OraclePrices map[Market]*big.Int - LastPrices map[Market]*big.Int + MidPrices map[Market]*big.Int ActiveMarkets []Market MinAllowableMargin *big.Int + MaintenanceMargin *big.Int } type UserState struct { @@ -29,6 +31,14 @@ func GetAvailableMargin_(notionalPosition, margin, reservedMargin, minAllowableM return Sub(Sub(margin, utilisedMargin), reservedMargin) } +func GetMarginFraction(hState *HubbleState, userState *UserState) *big.Int { + notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Maintenance_Margin) + if notionalPosition.Sign() == 0 { + return big.NewInt(math.MaxInt64) + } + return Div(Mul1e6(margin), notionalPosition) +} + func GetNotionalPositionAndMargin(hState *HubbleState, userState *UserState, marginMode MarginMode) (*big.Int, *big.Int) { margin := Sub(GetNormalizedMargin(hState.Assets, userState.Margins), userState.PendingFunding) notionalPosition, unrealizedPnl := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode) @@ -52,8 +62,8 @@ func GetOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, mar } // based on last price - notionalPosition, unrealizedPnl, lastPriceBasedMF := GetPositionMetadata( - hState.LastPrices[market], + notionalPosition, unrealizedPnl, midPriceBasedMF := GetPositionMetadata( + hState.MidPrices[market], position.OpenNotional, position.Size, margin, @@ -67,8 +77,8 @@ func GetOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, mar margin, ) - if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(lastPriceBasedMF) == 1) || // for liquidations - (marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(lastPriceBasedMF) == -1) { // for increasing leverage + if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == 1) || // for liquidations + (marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == -1) { // for increasing leverage return oracleBasedNotional, oracleBasedUnrealizedPnl } return notionalPosition, unrealizedPnl @@ -80,7 +90,7 @@ func GetPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, m if notionalPosition.Sign() == 0 { return big.NewInt(0), big.NewInt(0), big.NewInt(0) } - if size.Cmp(big.NewInt(0)) > 0 { + if size.Sign() > 0 { uPnL = Sub(notionalPosition, openNotional) } else { uPnL = Sub(openNotional, notionalPosition) @@ -90,7 +100,7 @@ func GetPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, m } func GetNotionalPosition(price *big.Int, size *big.Int) *big.Int { - return big.NewInt(0).Abs(Div1e18(Mul(size, price))) + return big.NewInt(0).Abs(Div1e18(Mul(price, size))) } func GetNormalizedMargin(assets []Collateral, margins []*big.Int) *big.Int { diff --git a/plugin/evm/orderbook/hubbleutils/margin_math_test.go b/plugin/evm/orderbook/hubbleutils/margin_math_test.go new file mode 100644 index 0000000000..f7970eab1d --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/margin_math_test.go @@ -0,0 +1,134 @@ +package hubbleutils + +import ( + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWeightedAndSpotCollateral(t *testing.T) { + assets := []Collateral{ + { + Price: big.NewInt(80500000), // 80.5 + Weight: big.NewInt(800000), // 0.8 + Decimals: 6, + }, + { + Price: big.NewInt(410000), // 0.41 + Weight: big.NewInt(900000), // 0.9 + Decimals: 6, + }, + } + margins := []*big.Int{ + big.NewInt(3500000), // 3.5 + big.NewInt(1040000000), // 1040 + } + expectedWeighted := big.NewInt(609160000) // 609.16 + expectedSpot := big.NewInt(708150000) // 708.15 + resultWeighted, resultSpot := WeightedAndSpotCollateral(assets, margins) + fmt.Println(resultWeighted, resultSpot) + assert.Equal(t, expectedWeighted, resultWeighted) + assert.Equal(t, expectedSpot, resultSpot) + + normalisedMargin := GetNormalizedMargin(assets, margins) + assert.Equal(t, expectedWeighted, normalisedMargin) + +} + +func TestGetNotionalPosition(t *testing.T) { + price := Scale(big.NewInt(1200), 6) + size := Scale(big.NewInt(5), 18) + expected := Scale(big.NewInt(6000), 6) + + result := GetNotionalPosition(price, size) + + assert.Equal(t, expected, result) +} + +func TestGetPositionMetadata(t *testing.T) { + price := big.NewInt(20250000) // 20.25 + openNotional := big.NewInt(75369000) // 75.369 (size * 18.5) + size := Scale(big.NewInt(40740), 14) // 4.074 + margin := big.NewInt(20000000) // 20 + + notionalPosition, unrealisedPnl, marginFraction := GetPositionMetadata(price, openNotional, size, margin) + + expectedNotionalPosition := big.NewInt(82498500) // 82.4985 + expectedUnrealisedPnl := big.NewInt(7129500) // 7.1295 + expectedMarginFraction := big.NewInt(328848) // 0.328848 + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUnrealisedPnl, unrealisedPnl) + assert.Equal(t, expectedMarginFraction, marginFraction) + + // ------ when size is negative ------ + size = Scale(big.NewInt(-40740), 14) // -4.074 + openNotional = big.NewInt(75369000) // 75.369 (size * 18.5) + notionalPosition, unrealisedPnl, marginFraction = GetPositionMetadata(price, openNotional, size, margin) + fmt.Println("notionalPosition", notionalPosition, "unrealisedPnl", unrealisedPnl, "marginFraction", marginFraction) + + expectedNotionalPosition = big.NewInt(82498500) // 82.4985 + expectedUnrealisedPnl = big.NewInt(-7129500) // -7.1295 + expectedMarginFraction = big.NewInt(156008) // 0.156008 + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUnrealisedPnl, unrealisedPnl) + assert.Equal(t, expectedMarginFraction, marginFraction) +} + +func TestGetOptimalPnl(t *testing.T) { + hState := &HubbleState{ + Assets: []Collateral{ + { + Price: big.NewInt(101000000), // 101 + Weight: big.NewInt(900000), // 0.9 + Decimals: 6, + }, + { + Price: big.NewInt(54360000), // 54.36 + Weight: big.NewInt(700000), // 0.7 + Decimals: 6, + }, + }, + MidPrices: map[Market]*big.Int{ + 0: big.NewInt(1545340000), // 1545.34 + }, + OraclePrices: map[Market]*big.Int{ + 0: big.NewInt(1545210000), // 1545.21 + }, + ActiveMarkets: []Market{ + 0, + }, + MinAllowableMargin: big.NewInt(100000), // 0.1 + MaintenanceMargin: big.NewInt(200000), // 0.2 + } + position := &Position{ + Size: Scale(big.NewInt(582), 14), // 0.0582 + OpenNotional: big.NewInt(87500000), // 87.5 + } + margin := big.NewInt(20000000) // 20 + market := 0 + marginMode := Maintenance_Margin + + notionalPosition, uPnL := GetOptimalPnl(hState, position, margin, market, marginMode) + + expectedNotionalPosition := big.NewInt(89938788) + expectedUPnL := big.NewInt(2438788) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + + notionalPosition, uPnL = GetOptimalPnl(hState, position, margin, market, marginMode) + + expectedNotionalPosition = big.NewInt(89931222) + expectedUPnL = big.NewInt(2431222) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} diff --git a/plugin/evm/orderbook/liquidations.go b/plugin/evm/orderbook/liquidations.go index 0dd97b903f..0bdd7caa24 100644 --- a/plugin/evm/orderbook/liquidations.go +++ b/plugin/evm/orderbook/liquidations.go @@ -19,17 +19,17 @@ type LiquidablePosition struct { } func (liq LiquidablePosition) GetUnfilledSize() *big.Int { - return big.NewInt(0).Sub(liq.Size, liq.FilledSize) + return new(big.Int).Sub(liq.Size, liq.FilledSize) } -func calcMarginFraction(trader *Trader, pendingFunding *big.Int, assets []hu.Collateral, oraclePrices map[Market]*big.Int, lastPrices map[Market]*big.Int, markets []Market) *big.Int { - margin := new(big.Int).Sub(getNormalisedMargin(trader, assets), pendingFunding) - notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(trader, margin, hu.Maintenance_Margin, oraclePrices, lastPrices, markets) - if notionalPosition.Sign() == 0 { - return big.NewInt(math.MaxInt64) +func calcMarginFraction(trader *Trader, hState *hu.HubbleState) *big.Int { + userState := &hu.UserState{ + Positions: translatePositions(trader.Positions), + Margins: getMargins(trader, len(hState.Assets)), + PendingFunding: getTotalFunding(trader, hState.ActiveMarkets), + ReservedMargin: new(big.Int).Set(trader.Margin.Reserved), } - margin.Add(margin, unrealizePnL) - return new(big.Int).Div(hu.Mul1e6(margin), notionalPosition) + return hu.GetMarginFraction(hState, userState) } func sortLiquidableSliceByMarginFraction(positions []LiquidablePosition) []LiquidablePosition { @@ -45,6 +45,13 @@ func getNormalisedMargin(trader *Trader, assets []hu.Collateral) *big.Int { func getMargins(trader *Trader, numAssets int) []*big.Int { margin := make([]*big.Int, numAssets) + if trader.Margin.Deposited == nil { + return margin + } + numAssets_ := len(trader.Margin.Deposited) + if numAssets_ < numAssets { + numAssets = numAssets_ + } for i := 0; i < numAssets; i++ { margin[i] = trader.Margin.Deposited[Collateral(i)] } @@ -54,7 +61,7 @@ func getMargins(trader *Trader, numAssets int) []*big.Int { func getTotalFunding(trader *Trader, markets []Market) *big.Int { totalPendingFunding := big.NewInt(0) for _, market := range markets { - if trader.Positions[market] != nil { + if trader.Positions[market] != nil && trader.Positions[market].UnrealisedFunding != nil && trader.Positions[market].UnrealisedFunding.Sign() != 0 { totalPendingFunding.Add(totalPendingFunding, trader.Positions[market].UnrealisedFunding) } } @@ -63,11 +70,11 @@ func getTotalFunding(trader *Trader, markets []Market) *big.Int { type MarginMode = hu.MarginMode -func getTotalNotionalPositionAndUnrealizedPnl(trader *Trader, margin *big.Int, marginMode MarginMode, oraclePrices map[Market]*big.Int, lastPrices map[Market]*big.Int, markets []Market) (*big.Int, *big.Int) { +func getTotalNotionalPositionAndUnrealizedPnl(trader *Trader, margin *big.Int, marginMode MarginMode, oraclePrices map[Market]*big.Int, midPrices map[Market]*big.Int, markets []Market) (*big.Int, *big.Int) { return hu.GetTotalNotionalPositionAndUnrealizedPnl( &hu.HubbleState{ OraclePrices: oraclePrices, - LastPrices: lastPrices, + MidPrices: midPrices, ActiveMarkets: markets, }, &hu.UserState{ @@ -89,7 +96,12 @@ func prettifyScaledBigInt(number *big.Int, precision int8) string { func translatePositions(positions map[int]*Position) map[int]*hu.Position { huPositions := make(map[int]*hu.Position) for key, value := range positions { - huPositions[key] = &value.Position + if value != nil { + huPositions[key] = &hu.Position{ + Size: new(big.Int).Set(value.Size), + OpenNotional: new(big.Int).Set(value.OpenNotional), + } + } } return huPositions } diff --git a/plugin/evm/orderbook/liquidations_test.go b/plugin/evm/orderbook/liquidations_test.go index 337f3d5efe..22ca3f2c36 100644 --- a/plugin/evm/orderbook/liquidations_test.go +++ b/plugin/evm/orderbook/liquidations_test.go @@ -15,8 +15,12 @@ func TestGetLiquidableTraders(t *testing.T) { assets := []hu.Collateral{{Price: big.NewInt(1e6), Weight: big.NewInt(1e6), Decimals: 6}} t.Run("When no trader exist", func(t *testing.T) { db := getDatabase() - oraclePrices := map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))} - liquidablePositions, _ := db.GetNaughtyTraders(oraclePrices, assets, []Market{market}) + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))}, + ActiveMarkets: []hu.Market{market}, + } + liquidablePositions, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -33,9 +37,14 @@ func TestGetLiquidableTraders(t *testing.T) { Positions: map[Market]*Position{}, }, } - db.LastPrice = map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(100))} - oraclePrices := map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))} - liquidablePositions, _ := db.GetNaughtyTraders(oraclePrices, assets, []Market{market}) + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(100))}, + ActiveMarkets: []hu.Market{market}, + MaintenanceMargin: db.configService.getMaintenanceMargin(), + } + liquidablePositions, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -61,9 +70,16 @@ func TestGetLiquidableTraders(t *testing.T) { db.TraderMap = map[common.Address]*Trader{ longTraderAddress: &longTrader, } - db.LastPrice = map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(50))} - oraclePrices := map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(49))} + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(49))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(50))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.getMinAllowableMargin(), + MaintenanceMargin: db.configService.getMaintenanceMargin(), + } + oraclePrices := hState.OraclePrices // assertions begin // for long trader _trader := &longTrader @@ -75,23 +91,25 @@ func TestGetLiquidableTraders(t *testing.T) { // oracle price: notional = 49 * 10 = 490, pnl = 490-900 = -410, mf = (500-42-410)/490 = 0.097 // for hu.Min_Allowable_Margin we select the min of 2 hence orale_mf - notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Min_Allowable_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Min_Allowable_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(490)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-410)), unrealizePnL) - availableMargin := getAvailableMargin(_trader, pendingFundingLong, assets, oraclePrices, db.GetLastPrices(), db.configService.getMinAllowableMargin(), []Market{market}) + hState.MinAllowableMargin = db.configService.getMinAllowableMargin() + availableMargin := getAvailableMargin(_trader, hState) // availableMargin = 500 - 42 (pendingFundingLong) - 410 (uPnL) - 490/5 = -50 assert.Equal(t, hu.Mul1e6(big.NewInt(-50)), availableMargin) // for hu.Maintenance_Margin we select the max of 2 hence, last_mf - notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Maintenance_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Maintenance_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(500)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-400)), unrealizePnL) - marginFraction := calcMarginFraction(_trader, pendingFundingLong, assets, oraclePrices, db.GetLastPrices(), []Market{market}) + // marginFraction := calcMarginFraction(_trader, pendingFundingLong, assets, oraclePrices, hState.MidPrices, []Market{market}) + marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(oraclePrices, assets, []Market{market}) + liquidablePositions, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -110,8 +128,15 @@ func TestGetLiquidableTraders(t *testing.T) { db.TraderMap = map[common.Address]*Trader{ longTraderAddress: &longTrader, } - db.LastPrice = map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(49))} - oraclePrices := map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(50))} + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(50))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(49))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.getMinAllowableMargin(), + MaintenanceMargin: db.configService.getMaintenanceMargin(), + } + oraclePrices := hState.OraclePrices // assertions begin // for long trader @@ -124,23 +149,23 @@ func TestGetLiquidableTraders(t *testing.T) { // oracle price: notional = 50 * 10 = 500, pnl = 500-900 = -400, mf = (500-42-400)/500 = 0.116 // for hu.Min_Allowable_Margin we select the min of 2 hence last_mf - notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Min_Allowable_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Min_Allowable_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(490)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-410)), unrealizePnL) - availableMargin := getAvailableMargin(_trader, pendingFundingLong, assets, oraclePrices, db.GetLastPrices(), db.configService.getMinAllowableMargin(), []Market{market}) + availableMargin := getAvailableMargin(_trader, hState) // availableMargin = 500 - 42 (pendingFundingLong) - 410 (uPnL) - 490/5 = -50 assert.Equal(t, hu.Mul1e6(big.NewInt(-50)), availableMargin) // for hu.Maintenance_Margin we select the max of 2 hence, oracle_mf - notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Maintenance_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Maintenance_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(500)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-400)), unrealizePnL) - marginFraction := calcMarginFraction(_trader, pendingFundingLong, assets, oraclePrices, db.GetLastPrices(), []Market{market}) + marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(oraclePrices, assets, []Market{market}) + liquidablePositions, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) }) @@ -167,8 +192,15 @@ func TestGetLiquidableTraders(t *testing.T) { db.TraderMap = map[common.Address]*Trader{ shortTraderAddress: &shortTrader, } - db.LastPrice = map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(142))} - oraclePrices := map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(143))} + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(143))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(142))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.getMinAllowableMargin(), + MaintenanceMargin: db.configService.getMaintenanceMargin(), + } + oraclePrices := hState.OraclePrices // assertions begin _trader := &shortTrader @@ -180,23 +212,23 @@ func TestGetLiquidableTraders(t *testing.T) { // oracle price based notional = 143 * 20 = 2860, pnl = 2100-2860 = -760, mf = (1000+37-760)/2860 = 0.096 // for hu.Min_Allowable_Margin we select the min of 2 hence, oracle_mf - notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Min_Allowable_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Min_Allowable_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(2860)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-760)), unrealizePnL) - availableMargin := getAvailableMargin(_trader, pendingFundingShort, assets, oraclePrices, db.GetLastPrices(), db.configService.getMinAllowableMargin(), []Market{market}) + availableMargin := getAvailableMargin(_trader, hState) // availableMargin = 1000 + 37 (pendingFundingShort) -760 (uPnL) - 2860/5 = -295 assert.Equal(t, hu.Mul1e6(big.NewInt(-295)), availableMargin) // for hu.Maintenance_Margin we select the max of 2 hence, last_mf - notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Maintenance_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Maintenance_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(2840)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-740)), unrealizePnL) - marginFraction := calcMarginFraction(_trader, pendingFundingShort, assets, oraclePrices, db.GetLastPrices(), []Market{market}) + marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(oraclePrices, assets, []Market{market}) + liquidablePositions, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -215,8 +247,15 @@ func TestGetLiquidableTraders(t *testing.T) { db.TraderMap = map[common.Address]*Trader{ shortTraderAddress: &shortTrader, } - db.LastPrice = map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(143))} - oraclePrices := map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(142))} + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(142))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(143))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.getMinAllowableMargin(), + MaintenanceMargin: db.configService.getMaintenanceMargin(), + } + oraclePrices := hState.OraclePrices // assertions begin _trader := &shortTrader @@ -228,23 +267,23 @@ func TestGetLiquidableTraders(t *testing.T) { // oracle price: notional = 142 * 20 = 2840, pnl = 2100-2840 = -740, mf = (1000+37-740)/2840 = 0.104 // for hu.Min_Allowable_Margin we select the min of 2 hence, last_mf - notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Min_Allowable_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Min_Allowable_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(2860)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-760)), unrealizePnL) - availableMargin := getAvailableMargin(_trader, pendingFundingShort, assets, oraclePrices, db.GetLastPrices(), db.configService.getMinAllowableMargin(), []Market{market}) + availableMargin := getAvailableMargin(_trader, hState) // availableMargin = 1000 + 37 (pendingFundingShort) - 760 (uPnL) - 2860/5 = -295 assert.Equal(t, hu.Mul1e6(big.NewInt(-295)), availableMargin) // for hu.Maintenance_Margin we select the max of 2 hence, oracle_mf - notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Maintenance_Margin, oraclePrices, db.GetLastPrices(), []Market{market}) + notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Maintenance_Margin, oraclePrices, hState.MidPrices, []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(2840)), notionalPosition) assert.Equal(t, hu.Mul1e6(big.NewInt(-740)), unrealizePnL) - marginFraction := calcMarginFraction(_trader, pendingFundingShort, assets, oraclePrices, db.GetLastPrices(), []Market{market}) + marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(oraclePrices, assets, []Market{market}) + liquidablePositions, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) }) diff --git a/plugin/evm/orderbook/matching_pipeline.go b/plugin/evm/orderbook/matching_pipeline.go index 818d63a549..8e30991322 100644 --- a/plugin/evm/orderbook/matching_pipeline.go +++ b/plugin/evm/orderbook/matching_pipeline.go @@ -71,17 +71,23 @@ func (pipeline *MatchingPipeline) Run(blockNumber *big.Int) bool { } // fetch the underlying price and run the matching engine - underlyingPrices := pipeline.GetUnderlyingPrices() - assets := pipeline.GetCollaterals() + hState := &hu.HubbleState{ + Assets: pipeline.GetCollaterals(), + OraclePrices: pipeline.GetUnderlyingPrices(), + MidPrices: pipeline.GetMidPrices(), + ActiveMarkets: markets, + MinAllowableMargin: pipeline.configService.getMinAllowableMargin(), + MaintenanceMargin: pipeline.configService.getMaintenanceMargin(), + } // build trader map - liquidablePositions, ordersToCancel := pipeline.db.GetNaughtyTraders(underlyingPrices, assets, markets) + liquidablePositions, ordersToCancel := pipeline.db.GetNaughtyTraders(hState) cancellableOrderIds := pipeline.cancelLimitOrders(ordersToCancel) orderMap := make(map[Market]*Orders) for _, market := range markets { - orderMap[market] = pipeline.fetchOrders(market, underlyingPrices[market], cancellableOrderIds, blockNumber) + orderMap[market] = pipeline.fetchOrders(market, hState.OraclePrices[market], cancellableOrderIds, blockNumber) } - pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices) + pipeline.runLiquidations(liquidablePositions, orderMap, hState.OraclePrices) for _, market := range markets { // @todo should we prioritize matching in any particular market? pipeline.runMatchingEngine(pipeline.lotp, orderMap[market].longOrders, orderMap[market].shortOrders) @@ -119,6 +125,16 @@ func (pipeline *MatchingPipeline) GetUnderlyingPrices() map[Market]*big.Int { return underlyingPrices } +func (pipeline *MatchingPipeline) GetMidPrices() map[Market]*big.Int { + prices := pipeline.configService.GetMidPrices() + log.Info("GetMidPrices", "prices", prices) + midPrices := make(map[Market]*big.Int) + for market, price := range prices { + midPrices[Market(market)] = price + } + return midPrices +} + func (pipeline *MatchingPipeline) GetCollaterals() []hu.Collateral { return pipeline.configService.GetCollaterals() } diff --git a/plugin/evm/orderbook/memory_database.go b/plugin/evm/orderbook/memory_database.go index fab59b370e..c9c209086e 100644 --- a/plugin/evm/orderbook/memory_database.go +++ b/plugin/evm/orderbook/memory_database.go @@ -231,7 +231,6 @@ type LimitOrderDatabase interface { UpdateNextSamplePITime(nextSamplePITime uint64) GetNextSamplePITime() uint64 UpdateLastPrice(market Market, lastPrice *big.Int) - GetLastPrice(market Market) *big.Int GetLastPrices() map[Market]*big.Int GetAllTraders() map[common.Address]Trader GetOrderBookData() InMemoryDatabase @@ -239,7 +238,7 @@ type LimitOrderDatabase interface { Accept(acceptedBlockNumber uint64, blockTimestamp uint64) SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error RevertLastStatus(orderId common.Hash) error - GetNaughtyTraders(oraclePrices map[Market]*big.Int, assets []hu.Collateral, markets []Market) ([]LiquidablePosition, map[common.Address][]Order) + GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order) GetAllOpenOrdersForTrader(trader common.Address) []Order GetOpenOrdersForTraderByType(trader common.Address, orderType OrderType) []Order UpdateLastPremiumFraction(market Market, trader common.Address, lastPremiumFraction *big.Int, cumlastPremiumFraction *big.Int) @@ -817,18 +816,15 @@ func (db *InMemoryDatabase) UpdateLastPremiumFraction(market Market, trader comm db.TraderMap[trader].Positions[market].UnrealisedFunding = hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(cumulativePremiumFraction, lastPremiumFraction), db.TraderMap[trader].Positions[market].Size)) } -func (db *InMemoryDatabase) GetLastPrice(market Market) *big.Int { - db.mu.RLock() - defer db.mu.RUnlock() - - return big.NewInt(0).Set(db.LastPrice[market]) -} - func (db *InMemoryDatabase) GetLastPrices() map[Market]*big.Int { db.mu.RLock() defer db.mu.RUnlock() - return db.LastPrice + copyMap := make(map[Market]*big.Int) + for k, v := range db.LastPrice { + copyMap[k] = new(big.Int).Set(v) + } + return copyMap } func (db *InMemoryDatabase) GetAllTraders() map[common.Address]Trader { @@ -912,7 +908,7 @@ func determinePositionToLiquidate(trader *Trader, addr common.Address, marginFra return liquidable } -func (db *InMemoryDatabase) GetNaughtyTraders(oraclePrices map[Market]*big.Int, assets []hu.Collateral, markets []Market) ([]LiquidablePosition, map[common.Address][]Order) { +func (db *InMemoryDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order) { db.mu.RLock() defer db.mu.RUnlock() @@ -924,26 +920,30 @@ func (db *InMemoryDatabase) GetNaughtyTraders(oraclePrices map[Market]*big.Int, minSizes := []*big.Int{} for addr, trader := range db.TraderMap { - pendingFunding := getTotalFunding(trader, markets) - marginFraction := calcMarginFraction(trader, pendingFunding, assets, oraclePrices, db.LastPrice, markets) - if marginFraction.Cmp(db.configService.getMaintenanceMargin()) == -1 { + userState := &hu.UserState{ + Positions: translatePositions(trader.Positions), + Margins: getMargins(trader, len(hState.Assets)), + PendingFunding: getTotalFunding(trader, hState.ActiveMarkets), + ReservedMargin: new(big.Int).Set(trader.Margin.Reserved), + } + marginFraction := hu.GetMarginFraction(hState, userState) + if marginFraction.Cmp(hState.MaintenanceMargin) == -1 { log.Info("below maintenanceMargin", "trader", addr.String(), "marginFraction", prettifyScaledBigInt(marginFraction, 6)) if len(minSizes) == 0 { - for _, market := range markets { + for _, market := range hState.ActiveMarkets { minSizes = append(minSizes, db.configService.getMinSizeRequirement(market)) } } - liquidablePositions = append(liquidablePositions, determinePositionToLiquidate(trader, addr, marginFraction, markets, minSizes)) + liquidablePositions = append(liquidablePositions, determinePositionToLiquidate(trader, addr, marginFraction, hState.ActiveMarkets, minSizes)) continue // we do not check for their open orders yet. Maybe liquidating them first will make available margin positive } if trader.Margin.Reserved.Sign() == 0 { continue } // has orders that might be cancellable - availableMargin := getAvailableMargin(trader, pendingFunding, assets, oraclePrices, db.LastPrice, db.configService.getMinAllowableMargin(), markets) - // availableMargin := getAvailableMarginWithDebugInfo(addr, trader, pendingFunding, oraclePrices, db.LastPrice, db.configService.getMinAllowableMargin(), markets) + availableMargin := hu.GetAvailableMargin(hState, userState) if availableMargin.Sign() == -1 { - foundCancellableOrders := db.determineOrdersToCancel(addr, trader, availableMargin, oraclePrices, ordersToCancel) + foundCancellableOrders := db.determineOrdersToCancel(addr, trader, availableMargin, hState.OraclePrices, ordersToCancel) if foundCancellableOrders { log.Info("negative available margin", "trader", addr.String(), "availableMargin", prettifyScaledBigInt(availableMargin, 6)) } else { @@ -1085,19 +1085,13 @@ func getBlankTrader() *Trader { } } -func getAvailableMargin(trader *Trader, pendingFunding *big.Int, assets []hu.Collateral, oraclePrices map[Market]*big.Int, lastPrices map[Market]*big.Int, minAllowableMargin *big.Int, markets []Market) *big.Int { +func getAvailableMargin(trader *Trader, hState *hu.HubbleState) *big.Int { return hu.GetAvailableMargin( - &hu.HubbleState{ - Assets: assets, - OraclePrices: oraclePrices, - LastPrices: lastPrices, - ActiveMarkets: markets, - MinAllowableMargin: minAllowableMargin, - }, + hState, &hu.UserState{ Positions: translatePositions(trader.Positions), - Margins: getMargins(trader, len(assets)), - PendingFunding: pendingFunding, + Margins: getMargins(trader, len(hState.Assets)), + PendingFunding: getTotalFunding(trader, hState.ActiveMarkets), ReservedMargin: trader.Margin.Reserved, }, ) diff --git a/plugin/evm/orderbook/memory_database_test.go b/plugin/evm/orderbook/memory_database_test.go index ba0377520f..587d289ba9 100644 --- a/plugin/evm/orderbook/memory_database_test.go +++ b/plugin/evm/orderbook/memory_database_test.go @@ -419,14 +419,21 @@ func TestGetCancellableOrders(t *testing.T) { notionalPosition, unrealizePnL = getTotalNotionalPositionAndUnrealizedPnl(_trader, depositMargin, hu.Maintenance_Margin, priceMap, inMemoryDatabase.GetLastPrices(), []Market{market}) assert.Equal(t, hu.Mul1e6(big.NewInt(90)), notionalPosition) assert.Equal(t, big.NewInt(0), unrealizePnL) - - marginFraction := calcMarginFraction(_trader, big.NewInt(0), assets, priceMap, inMemoryDatabase.GetLastPrices(), []Market{market}) + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: priceMap, + MidPrices: inMemoryDatabase.GetLastPrices(), + ActiveMarkets: []Market{market}, + MinAllowableMargin: inMemoryDatabase.configService.getMinAllowableMargin(), + MaintenanceMargin: inMemoryDatabase.configService.getMaintenanceMargin(), + } + marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(depositMargin /* uPnL = 0 */), notionalPosition), marginFraction) - availableMargin := getAvailableMargin(_trader, big.NewInt(0), assets, priceMap, inMemoryDatabase.GetLastPrices(), inMemoryDatabase.configService.getMinAllowableMargin(), []Market{market}) + availableMargin := getAvailableMargin(_trader, hState) // availableMargin = 40 - 9 - (99 + (10+9+8) * 3)/5 = -5 assert.Equal(t, hu.Mul1e6(big.NewInt(-5)), availableMargin) - _, ordersToCancel := inMemoryDatabase.GetNaughtyTraders(priceMap, assets, []Market{market}) + _, ordersToCancel := inMemoryDatabase.GetNaughtyTraders(hState) // t.Log("####", "ordersToCancel", ordersToCancel) assert.Equal(t, 1, len(ordersToCancel)) // only one trader @@ -791,7 +798,7 @@ func TestGetLastPrice(t *testing.T) { var market Market = 1 lastPrice := big.NewInt(20) inMemoryDatabase.UpdateLastPrice(market, lastPrice) - assert.Equal(t, lastPrice, inMemoryDatabase.GetLastPrice(market)) + assert.Equal(t, lastPrice, inMemoryDatabase.GetLastPrices()[market]) } func TestUpdateReservedMargin(t *testing.T) { diff --git a/plugin/evm/orderbook/mocks.go b/plugin/evm/orderbook/mocks.go index 8a6c754ad0..60f2eda248 100644 --- a/plugin/evm/orderbook/mocks.go +++ b/plugin/evm/orderbook/mocks.go @@ -116,7 +116,7 @@ func (db *MockLimitOrderDatabase) GetLastPrices() map[Market]*big.Int { return map[Market]*big.Int{} } -func (db *MockLimitOrderDatabase) GetNaughtyTraders(oraclePrices map[Market]*big.Int, assets []hu.Collateral, markets []Market) ([]LiquidablePosition, map[common.Address][]Order) { +func (db *MockLimitOrderDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order) { return []LiquidablePosition{}, map[common.Address][]Order{} } @@ -257,6 +257,10 @@ func (cs *MockConfigService) GetUnderlyingPrices() []*big.Int { return []*big.Int{} } +func (cs *MockConfigService) GetMidPrices() []*big.Int { + return []*big.Int{} +} + func (cs *MockConfigService) GetLastPremiumFraction(market Market, trader *common.Address) *big.Int { return big.NewInt(0) } diff --git a/plugin/evm/orderbook/service.go b/plugin/evm/orderbook/service.go index fcecda3637..9becba9d4b 100644 --- a/plugin/evm/orderbook/service.go +++ b/plugin/evm/orderbook/service.go @@ -73,6 +73,7 @@ type GetDebugDataResponse struct { UnrealizePnL map[common.Address]*big.Int LastPrice map[Market]*big.Int OraclePrice map[Market]*big.Int + MidPrice map[Market]*big.Int } func (api *OrderBookAPI) GetDebugData(ctx context.Context, trader string) GetDebugDataResponse { @@ -97,24 +98,34 @@ func (api *OrderBookAPI) GetDebugData(ctx context.Context, trader string) GetDeb } } - minAllowableMargin := api.configService.getMinAllowableMargin() prices := api.configService.GetUnderlyingPrices() - lastPrices := api.db.GetLastPrices() + mPrices := api.configService.GetMidPrices() + oraclePrices := map[Market]*big.Int{} + midPrices := map[Market]*big.Int{} count := api.configService.GetActiveMarketsCount() markets := make([]Market, count) for i := int64(0); i < count; i++ { markets[i] = Market(i) oraclePrices[Market(i)] = prices[Market(i)] + midPrices[Market(i)] = mPrices[Market(i)] } assets := api.configService.GetCollaterals() for addr, trader := range traderMap { pendingFunding := getTotalFunding(&trader, markets) margin := new(big.Int).Sub(getNormalisedMargin(&trader, assets), pendingFunding) - notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(&trader, margin, hu.Min_Allowable_Margin, oraclePrices, lastPrices, markets) - marginFraction := calcMarginFraction(&trader, pendingFunding, assets, oraclePrices, lastPrices, markets) - availableMargin := getAvailableMargin(&trader, pendingFunding, assets, oraclePrices, lastPrices, api.configService.getMinAllowableMargin(), markets) - utilisedMargin := hu.Div1e6(new(big.Int).Mul(notionalPosition, minAllowableMargin)) + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(&trader, margin, hu.Min_Allowable_Margin, oraclePrices, midPrices, markets) + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: oraclePrices, + MidPrices: midPrices, + ActiveMarkets: markets, + MinAllowableMargin: api.configService.getMinAllowableMargin(), + MaintenanceMargin: api.configService.getMaintenanceMargin(), + } + marginFraction := calcMarginFraction(&trader, hState) + availableMargin := getAvailableMargin(&trader, hState) + utilisedMargin := hu.Div1e6(new(big.Int).Mul(notionalPosition, hState.MinAllowableMargin)) response.MarginFraction[addr] = marginFraction response.AvailableMargin[addr] = availableMargin @@ -126,8 +137,9 @@ func (api *OrderBookAPI) GetDebugData(ctx context.Context, trader string) GetDeb response.ReservedMargin[addr] = trader.Margin.Reserved } - response.LastPrice = lastPrices + response.LastPrice = api.db.GetLastPrices() response.OraclePrice = oraclePrices + response.MidPrice = midPrices return response } diff --git a/plugin/evm/orderbook/trading_apis.go b/plugin/evm/orderbook/trading_apis.go index af6edd89b2..68ac0449f0 100644 --- a/plugin/evm/orderbook/trading_apis.go +++ b/plugin/evm/orderbook/trading_apis.go @@ -184,8 +184,8 @@ func (api *TradingAPI) GetMarginAndPositions(ctx context.Context, trader string) response.ReservedMargin = utils.BigIntToDecimal(traderInfo.Margin.Reserved, 6, 8) for market, position := range traderInfo.Positions { - lastPrice := api.db.GetLastPrice(market) - notionalPosition, uPnL, mf := getPositionMetadata(lastPrice, position.OpenNotional, position.Size, margin) + midPrice := api.configService.GetMidPrices()[market] + notionalPosition, uPnL, mf := getPositionMetadata(midPrice, position.OpenNotional, position.Size, margin) response.Positions = append(response.Positions, TraderPosition{ Market: market, @@ -197,7 +197,7 @@ func (api *TradingAPI) GetMarginAndPositions(ctx context.Context, trader string) MarginFraction: utils.BigIntToDecimal(mf, 6, 8), NotionalPosition: utils.BigIntToDecimal(notionalPosition, 6, 8), LiquidationPrice: "0", // todo: calculate - MarkPrice: utils.BigIntToDecimal(lastPrice, 6, 8), + MarkPrice: utils.BigIntToDecimal(midPrice, 6, 8), }) } diff --git a/precompile/contracts/bibliophile/clearing_house.go b/precompile/contracts/bibliophile/clearing_house.go index a393c30278..8484d92a87 100644 --- a/precompile/contracts/bibliophile/clearing_house.go +++ b/precompile/contracts/bibliophile/clearing_house.go @@ -46,6 +46,8 @@ func GetMarkets(stateDB contract.StateDB) []common.Address { numMarkets := GetActiveMarketsCount(stateDB) markets := make([]common.Address, numMarkets) baseStorageSlot := marketsStorageSlot() + // @todo when we ever settle a market, here it needs to be taken care of + // because currently the following assumes that all markets are active for i := int64(0); i < numMarkets; i++ { amm := stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(new(big.Int).Add(baseStorageSlot, big.NewInt(i)))) markets[i] = common.BytesToAddress(amm.Bytes()) @@ -69,13 +71,13 @@ func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPo numMarkets := len(markets) positions := make(map[int]*hu.Position, numMarkets) underlyingPrices := make(map[int]*big.Int, numMarkets) - lastPrices := make(map[int]*big.Int, numMarkets) - var marketIds []int + midPrices := make(map[int]*big.Int, numMarkets) + var activeMarketIds []int for i, market := range markets { positions[i] = getPosition(stateDB, getMarketAddressFromMarketID(int64(i), stateDB), &input.Trader) underlyingPrices[i] = getUnderlyingPrice(stateDB, market) - lastPrices[i] = getLastPrice(stateDB, market) - marketIds = append(marketIds, i) + midPrices[i] = getMidPrice(stateDB, market) + activeMarketIds = append(activeMarketIds, i) } pendingFunding := big.NewInt(0) if input.IncludeFundingPayments { @@ -85,8 +87,8 @@ func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPo &hu.HubbleState{ Assets: GetCollaterals(stateDB), OraclePrices: underlyingPrices, - LastPrices: lastPrices, - ActiveMarkets: marketIds, + MidPrices: midPrices, + ActiveMarkets: activeMarketIds, }, &hu.UserState{ Positions: positions, @@ -132,6 +134,14 @@ func GetUnderlyingPrices(stateDB contract.StateDB) []*big.Int { return underlyingPrices } +func GetMidPrices(stateDB contract.StateDB) []*big.Int { + underlyingPrices := make([]*big.Int, 0) + for _, market := range GetMarkets(stateDB) { + underlyingPrices = append(underlyingPrices, getMidPrice(stateDB, market)) + } + return underlyingPrices +} + func getPosSizes(stateDB contract.StateDB, trader *common.Address) []*big.Int { positionSizes := make([]*big.Int, 0) for _, market := range GetMarkets(stateDB) { diff --git a/precompile/contracts/bibliophile/oracle.go b/precompile/contracts/bibliophile/oracle.go index 394abd17de..633e9f0944 100644 --- a/precompile/contracts/bibliophile/oracle.go +++ b/precompile/contracts/bibliophile/oracle.go @@ -3,6 +3,7 @@ package bibliophile import ( "math/big" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ethereum/go-ethereum/common" @@ -41,6 +42,15 @@ func getUnderlyingPrice_(stateDB contract.StateDB, underlying common.Address) *b return fromTwosComplement(stateDB.GetState(oracle, common.BytesToHash(slot)).Bytes()) } +func getMidPrice(stateDB contract.StateDB, market common.Address) *big.Int { + asksHead := getAsksHead(stateDB, market) + bidsHead := getBidsHead(stateDB, market) + if asksHead.Sign() == 0 || bidsHead.Sign() == 0 { + return getUnderlyingPrice(stateDB, market) + } + return hu.Div(hu.Add(asksHead, bidsHead), big.NewInt(2)) +} + func getRedStoneAdapterAddress(stateDB contract.StateDB, oracle common.Address) common.Address { return common.BytesToAddress(stateDB.GetState(oracle, common.BigToHash(big.NewInt(RED_STONE_ADAPTER_SLOT))).Bytes()) } diff --git a/tests/orderbook/juror/getNotionalPositionAndMarginTests.js b/tests/orderbook/juror/getNotionalPositionAndMarginTests.js index 0d400d310e..95b2d2f17e 100644 --- a/tests/orderbook/juror/getNotionalPositionAndMarginTests.js +++ b/tests/orderbook/juror/getNotionalPositionAndMarginTests.js @@ -79,8 +79,8 @@ describe('Testing getNotionalPositionAndMargin',async function () { await placeOrderFromLimitOrderV2(oppositeShortOrder, alice) await waitForOrdersToMatch() - makerFee = await getMakerFee() - takerFee = await getTakerFee() + makerFee = await getMakerFee() + takerFee = await getTakerFee() resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) charlieOrder1Fee = makerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) @@ -103,8 +103,6 @@ describe('Testing getNotionalPositionAndMargin',async function () { //create position let aliceOrder1 = getOrderV2(market, alice.address, aliceOrderSize, aliceOrderPrice, getRandomSalt()) let charlieOrder1 = getOrderV2(market, charlie.address, charlieOrderSize, charlieOrderPrice, getRandomSalt()) - let oppositeAliceOrder1 = getOrderV2(market, alice.address, charlieOrderSize, charlieOrderPrice, getRandomSalt()) - let oppositeCharlieOrder1 = getOrderV2(market, charlie.address, aliceOrderSize, aliceOrderPrice, getRandomSalt()) // increase position let aliceOrder2Size = multiplySize(0.2) let charlieOrder2Size = multiplySize(-0.2) @@ -119,8 +117,8 @@ describe('Testing getNotionalPositionAndMargin',async function () { let makerFee, takerFee this.beforeAll(async function () { - makerFee = await getMakerFee() - takerFee = await getTakerFee() + makerFee = await getMakerFee() + takerFee = await getTakerFee() await addMargin(alice, aliceInitialMargin) await addMargin(charlie, charlieInitialMargin) // charlie places a short order and alice places a long order @@ -130,8 +128,6 @@ describe('Testing getNotionalPositionAndMargin',async function () { }) this.afterAll(async function () { - let resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) - let resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) // charlie places a long order and alice places a short order charlieTotalSize = charlieOrder1.baseAssetQuantity.add(charlieOrder2Size).add(charlieOrder3Size) aliceTotalSize = aliceOrder1.baseAssetQuantity.add(aliceOrder2Size).add(aliceOrder3Size) @@ -152,15 +148,19 @@ describe('Testing getNotionalPositionAndMargin',async function () { it('should return correct notional position and margin', async function () { let resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) let charlieOrderFee = takerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) - let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrderFee) - let expectedCharlieNotionalPosition = charlieOrderSize.abs().mul(charlieOrderPrice).div(_1e18) + // since there is no liquidity in the market, the optimal pnl will fall back to using underlying price + const amm = await utils.getAMMContract(market) + const underlyingPrice = await amm.getUnderlyingPrice() + let expectedCharlieNotionalPosition = charlieOrderSize.abs().mul(underlyingPrice).div(_1e18) + let uPnl = charlieOrderSize.abs().mul(charlieOrderPrice).div(_1e18).sub(expectedCharlieNotionalPosition) // short pos + let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrderFee).add(uPnl) expect(resultCharlie.notionalPosition.toString()).to.equal(expectedCharlieNotionalPosition.toString()) expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) let resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) let aliceOrderFee = takerFee.mul(aliceOrderSize).mul(aliceOrderPrice).div(_1e18).div(_1e6) - let expectedAliceMargin = aliceInitialMargin.sub(aliceOrderFee) - let expectedAliceNotionalPosition = aliceOrderSize.mul(aliceOrderPrice).div(_1e18) + let expectedAliceNotionalPosition = aliceOrderSize.abs().mul(underlyingPrice).div(_1e18) + let expectedAliceMargin = aliceInitialMargin.sub(aliceOrderFee).sub(uPnl) // - charlie's uPnL expect(resultAlice.notionalPosition.toString()).to.equal(expectedAliceNotionalPosition.toString()) expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) }) @@ -176,20 +176,23 @@ describe('Testing getNotionalPositionAndMargin',async function () { let resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) let charlieOrder1Fee = makerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) let charlieOrder2Fee = takerFee.mul(charlieOrder2Size.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) - let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee) - let charlieOrder1Notional = charlieOrderSize.mul(charlieOrderPrice).div(_1e18).abs() - let charlieOrder2Notional = charlieOrder2Size.mul(charlieOrderPrice).div(_1e18).abs() - let expectedCharlieNotionalPosition = charlieOrder1Notional.add(charlieOrder2Notional) + + const amm = await utils.getAMMContract(market) + const underlyingPrice = await amm.getUnderlyingPrice() + let { openNotional, size } = await amm.positions(charlie.address) + let expectedCharlieNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let uPnl = expectedCharlieNotionalPosition.sub(openNotional).mul(size.isNegative() ? -1 : 1) + + let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee).add(uPnl) expect(resultCharlie.notionalPosition.toString()).to.equal(expectedCharlieNotionalPosition.toString()) expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) let resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) let aliceOrder1Fee = takerFee.mul(aliceOrderSize).mul(aliceOrderPrice).div(_1e18).div(_1e6) let aliceOrder2Fee = makerFee.mul(aliceOrder2Size).mul(aliceOrderPrice).div(_1e18).div(_1e6) - let expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee) - let aliceOrder1Notional = aliceOrderSize.mul(aliceOrderPrice).div(_1e18) - let aliceOrder2Notional = aliceOrder2Size.mul(aliceOrderPrice).div(_1e18) - let expectedAliceNotionalPosition = aliceOrder1Notional.add(aliceOrder2Notional) + ;({ openNotional, size } = await amm.positions(charlie.address)) + let expectedAliceNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee).sub(uPnl) expect(resultAlice.notionalPosition.toString()).to.equal(expectedAliceNotionalPosition.toString()) expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) }) @@ -205,11 +208,14 @@ describe('Testing getNotionalPositionAndMargin',async function () { let charlieOrder1Fee = makerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) let charlieOrder2Fee = takerFee.mul(charlieOrder2Size.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) let charlieOrder3Fee = makerFee.mul(charlieOrder3Size.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) - let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee).sub(charlieOrder3Fee) - let charlieOrder1Notional = charlieOrderSize.mul(charlieOrderPrice).div(_1e18) - let charlieOrder2Notional = charlieOrder2Size.mul(charlieOrderPrice).div(_1e18) - let charlieOrder3Notional = charlieOrder3Size.mul(charlieOrderPrice).div(_1e18) - let expectedCharlieNotionalPosition = charlieOrder1Notional.add(charlieOrder2Notional).add(charlieOrder3Notional).abs() + + const amm = await utils.getAMMContract(market) + const underlyingPrice = await amm.getUnderlyingPrice() + let { openNotional, size } = await amm.positions(charlie.address) + let expectedCharlieNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let uPnl = expectedCharlieNotionalPosition.sub(openNotional).mul(size.isNegative() ? -1 : 1) + let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee).sub(charlieOrder3Fee).add(uPnl) + expect(resultCharlie.notionalPosition.toString()).to.equal(expectedCharlieNotionalPosition.toString()) expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) @@ -217,11 +223,9 @@ describe('Testing getNotionalPositionAndMargin',async function () { let aliceOrder1Fee = takerFee.mul(aliceOrderSize.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) let aliceOrder2Fee = makerFee.mul(aliceOrder2Size.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) let aliceOrder3Fee = takerFee.mul(aliceOrder3Size.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) - let expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee).sub(aliceOrder3Fee) - let aliceOrder1Notional = aliceOrderSize.mul(aliceOrderPrice).div(_1e18) - let aliceOrder2Notional = aliceOrder2Size.mul(aliceOrderPrice).div(_1e18) - let aliceOrder3Notional = aliceOrder3Size.mul(aliceOrderPrice).div(_1e18) - let expectedAliceNotionalPosition = aliceOrder1Notional.add(aliceOrder2Notional).add(aliceOrder3Notional).abs() + ;({ openNotional, size } = await amm.positions(charlie.address)) + let expectedAliceNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee).sub(aliceOrder3Fee).sub(uPnl) expect(resultAlice.notionalPosition.toString()).to.equal(expectedAliceNotionalPosition.toString()) expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) })