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

Support multi-collateral in getNormalizedMargin functions + refactor #118

Merged
merged 11 commits into from
Sep 29, 2023
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
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