Skip to content

Commit

Permalink
Support multi-collateral in getNormalizedMargin functions + refactor (#…
Browse files Browse the repository at this point in the history
…118)

* getNormalisedMargin

* refactor GetAvailableMargin

* fix tests

* update slot

* rename vars

* cleanup comments

* baseSlot for collateral idx*3

* userState/hubbleState in margin_math

* hu.Position

* ticks.getQuote

* misc changes
  • Loading branch information
atvanguard committed Sep 29, 2023
1 parent 850d417 commit bc928d5
Show file tree
Hide file tree
Showing 29 changed files with 3,086 additions and 2,756 deletions.
6 changes: 6 additions & 0 deletions plugin/evm/orderbook/config_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/ava-labs/subnet-evm/core"
"github.com/ava-labs/subnet-evm/core/state"
hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils"
"github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile"
"github.com/ethereum/go-ethereum/common"
)
Expand All @@ -17,6 +18,7 @@ type IConfigService interface {
getMinSizeRequirement(market Market) *big.Int
GetActiveMarketsCount() int64
GetUnderlyingPrices() []*big.Int
GetCollaterals() []hu.Collateral
GetLastPremiumFraction(market Market, trader *common.Address) *big.Int
GetCumulativePremiumFraction(market Market) *big.Int
GetAcceptableBounds(market Market) (*big.Int, *big.Int)
Expand Down Expand Up @@ -82,6 +84,10 @@ func (cs *ConfigService) GetUnderlyingPrices() []*big.Int {
return bibliophile.GetUnderlyingPrices(cs.getStateAtCurrentBlock())
}

func (cs *ConfigService) GetCollaterals() []hu.Collateral {
return bibliophile.GetCollaterals(cs.getStateAtCurrentBlock())
}

func (cs *ConfigService) GetLastPremiumFraction(market Market, trader *common.Address) *big.Int {
markets := bibliophile.GetMarkets(cs.getStateAtCurrentBlock())
return bibliophile.GetLastPremiumFraction(cs.getStateAtCurrentBlock(), markets[market], trader)
Expand Down
15 changes: 5 additions & 10 deletions plugin/evm/orderbook/contract_events_processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,7 @@ func TestOrderBookMarginAccountClearingHouseEventInLog(t *testing.T) {
unrealisedFunding := hu.Mul1e6(big.NewInt(1))
market := Market(0)
position := &Position{
OpenNotional: openNotional,
Size: size,
Position: hu.Position{OpenNotional: openNotional, Size: size},
UnrealisedFunding: unrealisedFunding,
LastPremiumFraction: lastPremiumFraction,
LiquidationThreshold: liquidationThreshold,
Expand Down Expand Up @@ -455,8 +454,7 @@ func TestHandleClearingHouseEvent(t *testing.T) {
db := getDatabase()
cep := newcep(t, db)
position := &Position{
OpenNotional: openNotional,
Size: size,
Position: hu.Position{OpenNotional: openNotional, Size: size},
UnrealisedFunding: unrealisedFunding,
LastPremiumFraction: lastPremiumFraction,
LiquidationThreshold: liquidationThreshold,
Expand Down Expand Up @@ -493,8 +491,7 @@ func TestHandleClearingHouseEvent(t *testing.T) {
db := getDatabase()
cep := newcep(t, db)
position := &Position{
OpenNotional: openNotional,
Size: size,
Position: hu.Position{OpenNotional: openNotional, Size: size},
UnrealisedFunding: unrealisedFunding,
LastPremiumFraction: lastPremiumFraction,
LiquidationThreshold: liquidationThreshold,
Expand Down Expand Up @@ -529,8 +526,7 @@ func TestHandleClearingHouseEvent(t *testing.T) {
db := getDatabase()
cep := newcep(t, db)
position := &Position{
OpenNotional: openNotional,
Size: size,
Position: hu.Position{OpenNotional: openNotional, Size: size},
UnrealisedFunding: unrealisedFunding,
LastPremiumFraction: lastPremiumFraction,
LiquidationThreshold: liquidationThreshold,
Expand Down Expand Up @@ -577,8 +573,7 @@ func TestHandleClearingHouseEvent(t *testing.T) {
db := getDatabase()
cep := newcep(t, db)
position := &Position{
OpenNotional: openNotional,
Size: size,
Position: hu.Position{OpenNotional: openNotional, Size: size},
UnrealisedFunding: unrealisedFunding,
LastPremiumFraction: lastPremiumFraction,
LiquidationThreshold: liquidationThreshold,
Expand Down
39 changes: 39 additions & 0 deletions plugin/evm/orderbook/hubbleutils/data_structures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package hubbleutils

import (
// "encoding/json"
"math/big"
)

type MarginMode = uint8

const (
Maintenance_Margin MarginMode = iota
Min_Allowable_Margin
)

type Collateral struct {
Price *big.Int // scaled by 1e6
Weight *big.Int // scaled by 1e6
Decimals uint8
}

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 {
Positions map[Market]*Position `json:"positions"` // position for every market
Margin Margin `json:"margin"` // available margin/balance for every market
}

type Margin struct {
Reserved *big.Int `json:"reserved"`
Deposited map[Collateral]*big.Int `json:"deposited"`
}
8 changes: 8 additions & 0 deletions plugin/evm/orderbook/hubbleutils/hubble_math.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ func RoundOff(a, b *big.Int) *big.Int {
func Mod(a, b *big.Int) *big.Int {
return new(big.Int).Mod(a, b)
}

func Scale(a *big.Int, decimals uint8) *big.Int {
return Mul(a, new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil))
}

func Unscale(a *big.Int, decimals uint8) *big.Int {
return Div(a, new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil))
}
113 changes: 113 additions & 0 deletions plugin/evm/orderbook/hubbleutils/margin_math.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package hubbleutils

import (
"math/big"
)

type HubbleState struct {
Assets []Collateral
OraclePrices map[Market]*big.Int
LastPrices map[Market]*big.Int
ActiveMarkets []Market
MinAllowableMargin *big.Int
}

type UserState struct {
Positions map[Market]*Position
Margins []*big.Int
PendingFunding *big.Int
ReservedMargin *big.Int
}

func GetAvailableMargin(hState *HubbleState, userState *UserState) *big.Int {
notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Min_Allowable_Margin)
return GetAvailableMargin_(notionalPosition, margin, userState.ReservedMargin, hState.MinAllowableMargin)
}

func GetAvailableMargin_(notionalPosition, margin, reservedMargin, minAllowableMargin *big.Int) *big.Int {
utilisedMargin := Div1e6(Mul(notionalPosition, minAllowableMargin))
return Sub(Sub(margin, utilisedMargin), reservedMargin)
}

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)
return notionalPosition, Add(margin, unrealizedPnl)
}

func GetTotalNotionalPositionAndUnrealizedPnl(hState *HubbleState, userState *UserState, margin *big.Int, marginMode MarginMode) (*big.Int, *big.Int) {
notionalPosition := big.NewInt(0)
unrealizedPnl := big.NewInt(0)
for _, market := range hState.ActiveMarkets {
_notionalPosition, _unrealizedPnl := GetOptimalPnl(hState, userState.Positions[market], margin, market, marginMode)
notionalPosition.Add(notionalPosition, _notionalPosition)
unrealizedPnl.Add(unrealizedPnl, _unrealizedPnl)
}
return notionalPosition, unrealizedPnl
}

func GetOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, market Market, marginMode MarginMode) (notionalPosition *big.Int, uPnL *big.Int) {
if position == nil || position.Size.Sign() == 0 {
return big.NewInt(0), big.NewInt(0)
}

// based on last price
notionalPosition, unrealizedPnl, lastPriceBasedMF := GetPositionMetadata(
hState.LastPrices[market],
position.OpenNotional,
position.Size,
margin,
)

// based on oracle price
oracleBasedNotional, oracleBasedUnrealizedPnl, oracleBasedMF := GetPositionMetadata(
hState.OraclePrices[market],
position.OpenNotional,
position.Size,
margin,
)

if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(lastPriceBasedMF) == 1) || // for liquidations
(marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(lastPriceBasedMF) == -1) { // for increasing leverage
return oracleBasedNotional, oracleBasedUnrealizedPnl
}
return notionalPosition, unrealizedPnl
}

func GetPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, margin *big.Int) (notionalPosition *big.Int, unrealisedPnl *big.Int, marginFraction *big.Int) {
notionalPosition = GetNotionalPosition(price, size)
uPnL := new(big.Int)
if notionalPosition.Sign() == 0 {
return big.NewInt(0), big.NewInt(0), big.NewInt(0)
}
if size.Cmp(big.NewInt(0)) > 0 {
uPnL = Sub(notionalPosition, openNotional)
} else {
uPnL = Sub(openNotional, notionalPosition)
}
mf := Div(Mul1e6(Add(margin, uPnL)), notionalPosition)
return notionalPosition, uPnL, mf
}

func GetNotionalPosition(price *big.Int, size *big.Int) *big.Int {
return big.NewInt(0).Abs(Div1e18(Mul(size, price)))
}

func GetNormalizedMargin(assets []Collateral, margins []*big.Int) *big.Int {
weighted, _ := WeightedAndSpotCollateral(assets, margins)
return weighted
}

func WeightedAndSpotCollateral(assets []Collateral, margins []*big.Int) (weighted, spot *big.Int) {
weighted = big.NewInt(0)
spot = big.NewInt(0)
for i, asset := range assets {
if margins[i] == nil || margins[i].Sign() == 0 {
continue
}
numerator := Mul(margins[i], asset.Price) // margin[i] is scaled by asset.Decimal
spot.Add(spot, Unscale(numerator, asset.Decimals))
weighted.Add(weighted, Unscale(Mul(numerator, asset.Weight), asset.Decimals+6))
}
return weighted, spot
}
99 changes: 33 additions & 66 deletions plugin/evm/orderbook/liquidations.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ func (liq LiquidablePosition) GetUnfilledSize() *big.Int {
return big.NewInt(0).Sub(liq.Size, liq.FilledSize)
}

func calcMarginFraction(trader *Trader, pendingFunding *big.Int, oraclePrices map[Market]*big.Int, lastPrices map[Market]*big.Int, markets []Market) *big.Int {
margin := new(big.Int).Sub(getNormalisedMargin(trader), pendingFunding)
notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(trader, margin, Maintenance_Margin, oraclePrices, lastPrices, markets)
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)
}
Expand All @@ -39,9 +39,16 @@ func sortLiquidableSliceByMarginFraction(positions []LiquidablePosition) []Liqui
return positions
}

func getNormalisedMargin(trader *Trader) *big.Int {
return trader.Margin.Deposited[HUSD]
// @todo: Write for multi-collateral
func getNormalisedMargin(trader *Trader, assets []hu.Collateral) *big.Int {
return hu.GetNormalizedMargin(assets, getMargins(trader, len(assets)))
}

func getMargins(trader *Trader, numAssets int) []*big.Int {
margin := make([]*big.Int, numAssets)
for i := 0; i < numAssets; i++ {
margin[i] = trader.Margin.Deposited[Collateral(i)]
}
return margin
}

func getTotalFunding(trader *Trader, markets []Market) *big.Int {
Expand All @@ -54,75 +61,35 @@ func getTotalFunding(trader *Trader, markets []Market) *big.Int {
return totalPendingFunding
}

func getNotionalPosition(price *big.Int, size *big.Int) *big.Int {
return big.NewInt(0).Abs(hu.Div1e18(big.NewInt(0).Mul(size, price)))
}

type MarginMode uint8

const (
Maintenance_Margin MarginMode = iota
Min_Allowable_Margin
)
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) {
notionalPosition := big.NewInt(0)
unrealizedPnl := big.NewInt(0)
for _, market := range markets {
_notionalPosition, _unrealizedPnl := getOptimalPnl(market, oraclePrices[market], lastPrices[market], trader, margin, marginMode)
notionalPosition.Add(notionalPosition, _notionalPosition)
unrealizedPnl.Add(unrealizedPnl, _unrealizedPnl)
}
return notionalPosition, unrealizedPnl
}

func getOptimalPnl(market Market, oraclePrice *big.Int, lastPrice *big.Int, trader *Trader, margin *big.Int, marginMode MarginMode) (notionalPosition *big.Int, uPnL *big.Int) {
position := trader.Positions[market]
if position == nil || position.Size.Sign() == 0 {
return big.NewInt(0), big.NewInt(0)
}

// based on last price
notionalPosition, unrealizedPnl, lastPriceBasedMF := getPositionMetadata(
lastPrice,
position.OpenNotional,
position.Size,
margin,
)
// log.Info("in getOptimalPnl", "notionalPosition", notionalPosition, "unrealizedPnl", unrealizedPnl, "lastPriceBasedMF", lastPriceBasedMF)

// based on oracle price
oracleBasedNotional, oracleBasedUnrealizedPnl, oracleBasedMF := getPositionMetadata(
oraclePrice,
position.OpenNotional,
position.Size,
return hu.GetTotalNotionalPositionAndUnrealizedPnl(
&hu.HubbleState{
OraclePrices: oraclePrices,
LastPrices: lastPrices,
ActiveMarkets: markets,
},
&hu.UserState{
Positions: translatePositions(trader.Positions),
},
margin,
marginMode,
)
// log.Info("in getOptimalPnl", "oracleBasedNotional", oracleBasedNotional, "oracleBasedUnrealizedPnl", oracleBasedUnrealizedPnl, "oracleBasedMF", oracleBasedMF)

if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(lastPriceBasedMF) == 1) || // for liquidations
(marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(lastPriceBasedMF) == -1) { // for increasing leverage
return oracleBasedNotional, oracleBasedUnrealizedPnl
}
return notionalPosition, unrealizedPnl
}

func getPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, margin *big.Int) (notionalPosition *big.Int, unrealisedPnl *big.Int, marginFraction *big.Int) {
// log.Info("in getPositionMetadata", "price", price, "openNotional", openNotional, "size", size, "margin", margin)
notionalPosition = getNotionalPosition(price, size)
uPnL := new(big.Int)
if notionalPosition.Cmp(big.NewInt(0)) == 0 {
return big.NewInt(0), big.NewInt(0), big.NewInt(0)
}
if size.Cmp(big.NewInt(0)) > 0 {
uPnL = new(big.Int).Sub(notionalPosition, openNotional)
} else {
uPnL = new(big.Int).Sub(openNotional, notionalPosition)
}
mf := new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(margin, uPnL)), notionalPosition)
return notionalPosition, uPnL, mf
return hu.GetPositionMetadata(price, openNotional, size, margin)
}

func prettifyScaledBigInt(number *big.Int, precision int8) string {
return new(big.Float).Quo(new(big.Float).SetInt(number), big.NewFloat(math.Pow10(int(precision)))).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
}
return huPositions
}
Loading

0 comments on commit bc928d5

Please sign in to comment.