Skip to content

Commit

Permalink
New algo for get optimal pnl (#134)
Browse files Browse the repository at this point in the history
* fork get optimal logic

* import asquares test

* make getOptimalPnL local fn

* update fork time
  • Loading branch information
atvanguard authored Oct 12, 2023
1 parent 76d59c9 commit d3c0e82
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 63 deletions.
25 changes: 18 additions & 7 deletions plugin/evm/orderbook/hubbleutils/margin_math.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type UserState struct {
}

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

Expand All @@ -32,31 +32,32 @@ func GetAvailableMargin_(notionalPosition, margin, reservedMargin, minAllowableM
}

func GetMarginFraction(hState *HubbleState, userState *UserState) *big.Int {
notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Maintenance_Margin)
notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Maintenance_Margin, 0)
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) {
func GetNotionalPositionAndMargin(hState *HubbleState, userState *UserState, marginMode MarginMode, blockTimestamp uint64) (*big.Int, *big.Int) {
margin := Sub(GetNormalizedMargin(hState.Assets, userState.Margins), userState.PendingFunding)
notionalPosition, unrealizedPnl := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode)
notionalPosition, unrealizedPnl := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode, blockTimestamp)
return notionalPosition, Add(margin, unrealizedPnl)
}

func GetTotalNotionalPositionAndUnrealizedPnl(hState *HubbleState, userState *UserState, margin *big.Int, marginMode MarginMode) (*big.Int, *big.Int) {
func GetTotalNotionalPositionAndUnrealizedPnl(hState *HubbleState, userState *UserState, margin *big.Int, marginMode MarginMode, blockTimestamp uint64) (*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, _unrealizedPnl := getOptimalPnl(hState, userState.Positions[market], margin, market, marginMode, blockTimestamp)
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) {
func getOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, market Market, marginMode MarginMode, blockTimestamp uint64) (notionalPosition *big.Int, uPnL *big.Int) {
if position == nil || position.Size.Sign() == 0 {
return big.NewInt(0), big.NewInt(0)
}
Expand All @@ -77,6 +78,16 @@ func GetOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, mar
margin,
)

// Thursday, 12 October 2023 16:45:00 GMT
if blockTimestamp == 0 || blockTimestamp >= 1697129100 { // use new algorithm
if (marginMode == Maintenance_Margin && oracleBasedUnrealizedPnl.Cmp(unrealizedPnl) == 1) || // for liquidations
(marginMode == Min_Allowable_Margin && oracleBasedUnrealizedPnl.Cmp(unrealizedPnl) == -1) { // for increasing leverage
return oracleBasedNotional, oracleBasedUnrealizedPnl
}
return notionalPosition, unrealizedPnl
}

// use old algorithm
if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == 1) || // for liquidations
(marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == -1) { // for increasing leverage
return oracleBasedNotional, oracleBasedUnrealizedPnl
Expand Down
185 changes: 136 additions & 49 deletions plugin/evm/orderbook/hubbleutils/margin_math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,62 @@ import (
"github.com/stretchr/testify/assert"
)

func TestWeightedAndSpotCollateral(t *testing.T) {
assets := []Collateral{
var hState = &HubbleState{
Assets: []Collateral{
{
Price: big.NewInt(80500000), // 80.5
Weight: big.NewInt(800000), // 0.8
Price: big.NewInt(1.01 * 1e6), // 1.01
Weight: big.NewInt(1e6), // 1
Decimals: 6,
},
{
Price: big.NewInt(410000), // 0.41
Weight: big.NewInt(900000), // 0.9
Price: big.NewInt(54.36 * 1e6), // 54.36
Weight: big.NewInt(0.7 * 1e6), // 0.7
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
},
MidPrices: map[Market]*big.Int{
0: big.NewInt(1544.21 * 1e6), // 1544.21
1: big.NewInt(19.5 * 1e6), // 19.5
},
OraclePrices: map[Market]*big.Int{
0: big.NewInt(1503.21 * 1e6),
1: big.NewInt(17.5 * 1e6),
},
ActiveMarkets: []Market{
0, 1,
},
MinAllowableMargin: big.NewInt(100000), // 0.1
MaintenanceMargin: big.NewInt(200000), // 0.2
}

var userState = &UserState{
Positions: map[Market]*Position{
0: {
Size: big.NewInt(0.582 * 1e18), // 0.0582
OpenNotional: big.NewInt(875 * 1e6), // 87.5, openPrice = 1503.43
},
1: {
Size: Scale(big.NewInt(-101), 18), // -101
OpenNotional: big.NewInt(1767.5 * 1e6), // 1767.5, openPrice = 17.5
},
},
Margins: []*big.Int{
big.NewInt(30.5 * 1e6), // 30.5
big.NewInt(14 * 1e6), // 14
},
PendingFunding: big.NewInt(0),
ReservedMargin: big.NewInt(0),
}

func TestWeightedAndSpotCollateral(t *testing.T) {
assets := hState.Assets
margins := userState.Margins
expectedWeighted := Unscale(Mul(Mul(margins[0], assets[0].Price), assets[0].Weight), assets[0].Decimals+6)
expectedWeighted.Add(expectedWeighted, Unscale(Mul(Mul(margins[1], assets[1].Price), assets[1].Weight), assets[1].Decimals+6))

expectedSpot := Unscale(Mul(margins[0], assets[0].Price), assets[0].Decimals)
expectedSpot.Add(expectedSpot, Unscale(Mul(margins[1], assets[1].Price), assets[1].Decimals))

resultWeighted, resultSpot := WeightedAndSpotCollateral(assets, margins)
fmt.Println(resultWeighted, resultSpot)
assert.Equal(t, expectedWeighted, resultWeighted)
Expand Down Expand Up @@ -79,55 +116,105 @@ func TestGetPositionMetadata(t *testing.T) {
}

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
margin := big.NewInt(20 * 1e6) // 20
market := 0
position := userState.Positions[market]
marginMode := Maintenance_Margin

notionalPosition, uPnL := GetOptimalPnl(hState, position, margin, market, marginMode)
notionalPosition, uPnL := getOptimalPnl(hState, position, margin, market, marginMode, 0)

expectedNotionalPosition := big.NewInt(89938788)
expectedUPnL := big.NewInt(2438788)
// mid price pnl is more than oracle price pnl
expectedNotionalPosition := Unscale(Mul(position.Size, hState.MidPrices[market]), 18)
expectedUPnL := Sub(expectedNotionalPosition, position.OpenNotional)
fmt.Println("Maintenace_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL)

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, 0)

expectedNotionalPosition = Unscale(Mul(position.Size, hState.OraclePrices[market]), 18)
expectedUPnL = Sub(expectedNotionalPosition, position.OpenNotional)
fmt.Println("Min_Allowable_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL)

assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedUPnL, uPnL)
}

func TestGetOptimalPnlDeprecated(t *testing.T) {
margin := big.NewInt(20 * 1e6) // 20
market := 0
position := userState.Positions[market]
marginMode := Maintenance_Margin

notionalPosition, uPnL = GetOptimalPnl(hState, position, margin, market, marginMode)
notionalPosition, uPnL := getOptimalPnl(hState, position, margin, market, marginMode, 1)

expectedNotionalPosition = big.NewInt(89931222)
expectedUPnL = big.NewInt(2431222)
// mid price pnl is more than oracle price pnl
expectedNotionalPosition := Unscale(Mul(position.Size, hState.MidPrices[market]), 18)
expectedUPnL := Sub(expectedNotionalPosition, position.OpenNotional)
fmt.Println("Maintenace_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL)

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, 1)

expectedNotionalPosition = Unscale(Mul(position.Size, hState.OraclePrices[market]), 18)
expectedUPnL = Sub(expectedNotionalPosition, position.OpenNotional)
fmt.Println("Min_Allowable_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL)

assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedUPnL, uPnL)
}

func TestGetTotalNotionalPositionAndUnrealizedPnl(t *testing.T) {
margin := GetNormalizedMargin(hState.Assets, userState.Margins)
// margin := big.NewInt(2000 * 1e6) // 50
fmt.Println("margin = ", margin) // 563.533
marginMode := Maintenance_Margin
fmt.Println("availableMargin = ", GetAvailableMargin(hState, userState))
fmt.Println("marginFraction = ", GetMarginFraction(hState, userState))

notionalPosition, uPnL := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode, 0)
fmt.Println("Maintenace_Margin_Mode ", "notionalPosition = ", notionalPosition, "uPnL = ", uPnL)
_, pnl := getOptimalPnl(hState, userState.Positions[0], margin, 0, marginMode, 0)
fmt.Println("best pnl market 0 =", pnl)
_, pnl = getOptimalPnl(hState, userState.Positions[1], margin, 1, marginMode, 0)
fmt.Println("best pnl market 1 =", pnl)

// mid price pnl is more than oracle price pnl for long position
expectedNotionalPosition := Unscale(Mul(userState.Positions[0].Size, hState.MidPrices[0]), 18)
expectedUPnL := Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional)
// oracle price pnl is more than mid price pnl for short position
expectedNotional2 := Abs(Unscale(Mul(userState.Positions[1].Size, hState.OraclePrices[1]), 18))
expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2)
expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2))

assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedUPnL, uPnL)

// ------ when marginMode is Min_Allowable_Margin ------

marginMode = Min_Allowable_Margin
notionalPosition, uPnL = GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode, 0)
fmt.Println("Min_Allowable_Margin_Mode ", "notionalPosition = ", notionalPosition, "uPnL = ", uPnL)

_, pnl = getOptimalPnl(hState, userState.Positions[0], margin, 0, marginMode, 0)
fmt.Println("worst pnl market 0 =", pnl)
_, pnl = getOptimalPnl(hState, userState.Positions[1], margin, 1, marginMode, 0)
fmt.Println("worst pnl market 1 =", pnl)

expectedNotionalPosition = Unscale(Mul(userState.Positions[0].Size, hState.OraclePrices[0]), 18)
expectedUPnL = Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional)
expectedNotional2 = Abs(Unscale(Mul(userState.Positions[1].Size, hState.MidPrices[1]), 18))
expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2)
expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2))

assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedUPnL, uPnL)
Expand Down
1 change: 1 addition & 0 deletions plugin/evm/orderbook/liquidations.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func getTotalNotionalPositionAndUnrealizedPnl(trader *Trader, margin *big.Int, m
},
margin,
marginMode,
0,
)
}

Expand Down
4 changes: 2 additions & 2 deletions precompile/contracts/bibliophile/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func GetClearingHouseVariables(stateDB contract.StateDB, trader common.Address)
Trader: trader,
IncludeFundingPayments: false,
Mode: 0,
})
}, 0 /* use new algorithm */)
totalFunding := GetTotalFunding(stateDB, &trader)
positionSizes := getPosSizes(stateDB, &trader)
underlyingPrices := GetUnderlyingPrices(stateDB)
Expand Down Expand Up @@ -131,7 +131,7 @@ func GetAMMVariables(stateDB contract.StateDB, ammAddress common.Address, ammInd
minAllowableMargin := GetMinAllowableMargin(stateDB)
takerFee := GetTakerFee(stateDB)
totalMargin := GetNormalizedMargin(stateDB, trader)
availableMargin := GetAvailableMargin(stateDB, trader)
availableMargin := GetAvailableMargin(stateDB, trader, 0)
reduceOnlyAmount := getReduceOnlyAmount(stateDB, trader, big.NewInt(ammIndex))
longOpenOrdersAmount := getLongOpenOrdersAmount(stateDB, trader, big.NewInt(ammIndex))
shortOpenOrdersAmount := getShortOpenOrdersAmount(stateDB, trader, big.NewInt(ammIndex))
Expand Down
3 changes: 2 additions & 1 deletion precompile/contracts/bibliophile/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type GetNotionalPositionAndMarginOutput struct {
Margin *big.Int
}

func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput) GetNotionalPositionAndMarginOutput {
func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput, blockTimestamp uint64) GetNotionalPositionAndMarginOutput {
markets := GetMarkets(stateDB)
numMarkets := len(markets)
positions := make(map[int]*hu.Position, numMarkets)
Expand Down Expand Up @@ -96,6 +96,7 @@ func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPo
PendingFunding: pendingFunding,
},
input.Mode,
blockTimestamp,
)
return GetNotionalPositionAndMarginOutput{
NotionalPosition: notionalPosition,
Expand Down
4 changes: 2 additions & 2 deletions precompile/contracts/bibliophile/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ func (b *bibliophileClient) GetReduceOnlyAmount(trader common.Address, ammIndex
}

func (b *bibliophileClient) GetAvailableMargin(trader common.Address) *big.Int {
return GetAvailableMargin(b.accessibleState.GetStateDB(), trader)
return GetAvailableMargin(b.accessibleState.GetStateDB(), trader, b.GetTimeStamp())
}

func (b *bibliophileClient) GetNotionalPositionAndMargin(trader common.Address, includeFundingPayments bool, mode uint8) (*big.Int, *big.Int) {
output := getNotionalPositionAndMargin(b.accessibleState.GetStateDB(), &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: includeFundingPayments, Mode: mode})
output := getNotionalPositionAndMargin(b.accessibleState.GetStateDB(), &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: includeFundingPayments, Mode: mode}, b.GetTimeStamp())
return output.NotionalPosition, output.Margin
}

Expand Down
4 changes: 2 additions & 2 deletions precompile/contracts/bibliophile/margin_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ func getReservedMargin(stateDB contract.StateDB, trader common.Address) *big.Int
return stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BytesToHash(baseMappingHash)).Big()
}

func GetAvailableMargin(stateDB contract.StateDB, trader common.Address) *big.Int {
output := getNotionalPositionAndMargin(stateDB, &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: true, Mode: uint8(1)}) // Min_Allowable_Margin
func GetAvailableMargin(stateDB contract.StateDB, trader common.Address, blockTimeStamp uint64) *big.Int {
output := getNotionalPositionAndMargin(stateDB, &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: true, Mode: uint8(1)}, blockTimeStamp) // Min_Allowable_Margin
return hu.GetAvailableMargin_(output.NotionalPosition, output.Margin, getReservedMargin(stateDB, trader), GetMinAllowableMargin(stateDB))
}

Expand Down

0 comments on commit d3c0e82

Please sign in to comment.