Skip to content

Commit

Permalink
test: fix margin calculation for fully collateralised markets
Browse files Browse the repository at this point in the history
Signed-off-by: Elias Van Ootegem <[email protected]>
  • Loading branch information
EVODelavega committed May 15, 2024
1 parent 7a7a824 commit 386a99a
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 43 deletions.
48 changes: 23 additions & 25 deletions core/integration/features/capped-futures/0016-PFUT-021.feature
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
| 0.2 | 0.1 | 100 | -100 | 0.1 |

And the markets:
| id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | max price cap | fully collateralised | binary |
| ETH/DEC21 | ETH | USD | simple-risk-model-1 | default-margin-calculator | 1 | fees-config-1 | price-monitoring-1 | ethDec21Oracle | 0.25 | 0 | default-futures | 1500 | true | false |
| id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | max price cap | fully collateralised | binary |
| ETH/DEC21 | ETH | USD | simple-risk-model-1 | default-capped-margin-calculator | 1 | fees-config-1 | price-monitoring-1 | ethDec21Oracle | 0.25 | 0 | default-futures | 1500 | true | false |

@SLABug @NoPerp @Capped
Scenario: 0016-PFUT-021: Settlement happened when market is being closed - happens when the oracle price is < max price cap, higher prices are ignored.
Given the initial insurance pool balance is "10000" for all the markets
And the parties deposit on asset's general account the following amount:
| party | asset | amount |
| party1 | USD | 10000 |
| party2 | USD | 1000 |
| party2 | USD | 10000 |
| party3 | USD | 5000 |
| aux1 | USD | 100000 |
| aux2 | USD | 100000 |
Expand All @@ -61,23 +61,23 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater

Then the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/DEC21"
And the market state should be "STATE_ACTIVE" for the market "ETH/DEC21"
Then the mark price should be "1000" for the market "ETH/DEC21"

Then the parties should have the following account balances:
And the mark price should be "1000" for the market "ETH/DEC21"
And the parties should have the following account balances:
| party | asset | market id | margin | general |
| party1 | USD | ETH/DEC21 | 1800 | 8200 |
| party2 | USD | ETH/DEC21 | 0 | 1000 |
| party1 | USD | ETH/DEC21 | 5000 | 5000 |
| party2 | USD | ETH/DEC21 | 2500 | 7500 |

#order margin for aux1: limit price * size = 999*2=1998
#order margin for aux2: (max price - limit price) * size = (1500-1000)*2=1000
#order margin for aux2: (max price - limit price) * size = (1500-1301)*2=398
# party1 maintenance margin level: position size * average entry price = 5*1000=5000
# party2 maintenance margin level: position size * (max price - average entry price)=5*(1500-1000)=2500
Then the parties should have the following margin levels:
# Aux1: potential position * average price on book = 2 * 999 = 1998, but due to the MTM settlement the margin level
And the parties should have the following margin levels:
| party | market id | maintenance | search | initial | release | margin mode |
| aux1 | ETH/DEC21 | 600 | 660 | 720 | 840 | cross margin |
| aux2 | ETH/DEC21 | 0 | 0 | 0 | 0 | cross margin |
| party1 | ETH/DEC21 | 1500 | 1650 | 1800 | 2100 | cross margin |
| party2 | ETH/DEC21 | 0 | 0 | 0 | 0 | cross margin |
| party1 | ETH/DEC21 | 5000 | 5000 | 5000 | 5000 | cross margin |
| party2 | ETH/DEC21 | 2500 | 2500 | 2500 | 2500 | cross margin |
| aux2 | ETH/DEC21 | 398 | 398 | 398 | 398 | cross margin |
| aux1 | ETH/DEC21 | 1998 | 1998 | 1998 | 1998 | cross margin |

#update mark price
When the parties place the following orders:
Expand All @@ -88,16 +88,14 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
And the network moves ahead "2" blocks
Then the mark price should be "1100" for the market "ETH/DEC21"

# MTM settlement 5 long makes a profit of 500, 5 short loses 500
# Now for aux1 and 2, the calculations from above still hold but more margin is required duw to the open positions:
# aux1: position * 1100 + 999*2 = 1100 + 1998 = 3098
# aux2: then placing the order (max price - average order price) * 3 = (1500 - (1301 + 1301 + 1100)/3) * 3 = (1500 - 1234) * 3 = 266 * 3 = 798
# aux2's short position and potential margins are calculated separately as 2 * (1500-1301) + 1 * (1500 - 1100) = 398 + 400 = 798
Then the parties should have the following account balances:
| party | asset | market id | margin | general |
| party1 | USD | ETH/DEC21 | 1800 | 8700 |
| party2 | USD | ETH/DEC21 | 0 | 500 |

# party1 maintenance margin level: position size * average entry price
# party2 maintenance margin level: (max price - average entry price)
Then the parties should have the following margin levels:
| party | market id | maintenance | search | initial | release | margin mode |
| party1 | ETH/DEC21 | 1500 | 1650 | 1800 | 2100 | cross margin |
| party2 | ETH/DEC21 | 0 | 0 | 0 | 0 | cross margin |


| party1 | USD | ETH/DEC21 | 5000 | 5500 |
| party2 | USD | ETH/DEC21 | 2500 | 7000 |
| aux1 | USD | ETH/DEC21 | 3098 | 96908 |
| aux2 | USD | ETH/DEC21 | 798 | 99174 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"scalingFactors": {
"searchLevel": 1,
"initialMargin": 1,
"collateralRelease": 1
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"scalingFactors": {
"searchLevel": 1.1,
"initialMargin": 1.2,
"collateralRelease": 1.4
"searchLevel": 1,
"initialMargin": 1,
"collateralRelease": 1
}
}
1 change: 1 addition & 0 deletions core/integration/steps/market/margin_calculators.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
defaultMarginCalculators embed.FS
defaultMarginCalculatorFileNames = []string{
"defaults/margin-calculator/default-margin-calculator.json",
"defaults/margin-calculator/default-capped-margin-calculator.json",
"defaults/margin-calculator/default-overkill-margin-calculator.json",
}
)
Expand Down
6 changes: 6 additions & 0 deletions core/integration/steps/the_markets.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,12 @@ func newMarket(config *market.Config, row marketRow) types.Market {
tip := m.TradableInstrument.IntoProto()
if row.IsCapped() {
tip.MarginCalculator.FullyCollateralised = ptr.From(pCap.FullyCollateralised)
// scaling factors should be irrelevant
tip.MarginCalculator.ScalingFactors = &proto.ScalingFactors{
SearchLevel: 1.0,
InitialMargin: 1.0,
CollateralRelease: 1.0,
}
}
err = config.RiskModels.LoadModel(row.riskModel(), tip)
m.TradableInstrument = types.TradableInstrumentFromProto(tip)
Expand Down
64 changes: 49 additions & 15 deletions core/risk/margins_calculation.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ func scalingFactorsUintFromDecimals(sf *types.ScalingFactors) *scalingFactorsUin
}
}

func newMarginLevelsFull(maintenance num.Decimal) *types.MarginLevels {
maint, _ := num.UintFromDecimal(maintenance.Ceil())
return &types.MarginLevels{
MaintenanceMargin: maint,
SearchLevel: maint.Clone(),
InitialMargin: maint.Clone(),
CollateralReleaseLevel: maint.Clone(),
OrderMargin: num.UintZero(),
MarginMode: types.MarginModeCrossMargin,
MarginFactor: num.DecimalZero(),
}
}

func newMarginLevels(maintenance num.Decimal, scalingFactors *scalingFactorsUint) *types.MarginLevels {
umaintenance, _ := num.UintFromDecimal(maintenance.Ceil())
return &types.MarginLevels{
Expand All @@ -59,24 +72,26 @@ func newMarginLevels(maintenance num.Decimal, scalingFactors *scalingFactorsUint
}
}

func (e *Engine) calculateFullCollatMargins(m events.Margin, price *num.Uint, rf types.RiskFactor, withPotential bool) *types.MarginLevels {
func (e *Engine) calculateFullCollatMargins(m events.Margin, price *num.Uint, _ types.RiskFactor, withPotential bool) *types.MarginLevels {
var (
marginMaintenanceLng num.Decimal
marginMaintenanceSht num.Decimal
)
// convert volumn to a decimal number from a * 10^pdp
openVolume := num.DecimalFromInt64(m.Size()).Div(e.positionFactor)
aep := m.AverageEntryPrice().ToDecimal()
base := price.ToDecimal()
var (
riskiestLng = openVolume
riskiestSht = openVolume
)
if withPotential {
// calculate both long and short riskiest positions
riskiestLng = riskiestLng.Add(num.DecimalFromInt64(m.Buy()).Div(e.positionFactor))
riskiestSht = riskiestSht.Sub(num.DecimalFromInt64(m.Sell()).Div(e.positionFactor))
}
// if withPotential {
// calculate both long and short riskiest positions
// riskiestLng = riskiestLng.Add(num.DecimalFromInt64(m.Buy()).Div(e.positionFactor))
// riskiestSht = riskiestSht.Sub(num.DecimalFromInt64(m.Sell()).Div(e.positionFactor))
// }
// the party has no open positions that we need to calculate margin for
if riskiestLng.IsZero() && riskiestSht.IsZero() {
if riskiestLng.IsZero() && riskiestSht.IsZero() && !withPotential {
return &types.MarginLevels{
MaintenanceMargin: num.UintZero(),
SearchLevel: num.UintZero(),
Expand All @@ -87,21 +102,40 @@ func (e *Engine) calculateFullCollatMargins(m events.Margin, price *num.Uint, rf
MarginFactor: num.DecimalZero(),
}
}
base := price.ToDecimal()
if riskiestLng.IsPositive() {
marginMaintenanceLng = riskiestLng.Mul(base).Mul(rf.Long)
marginMaintenanceLng = riskiestLng.Mul(aep)
} else {
// even our riskies position is short, get the sell AEP
marginMaintenanceLng = riskiestLng.Mul(base.Sub(aep)).Abs()
}
if riskiestSht.IsNegative() {
marginMaintenanceSht = riskiestSht.Mul(base).Mul(rf.Short)
marginMaintenanceSht = riskiestSht.Mul(base.Sub(aep)).Abs()
} else {
// even the shortest position is long, get buy AEP
marginMaintenanceSht = riskiestSht.Mul(aep)
}

if marginMaintenanceLng.GreaterThan(marginMaintenanceSht) && marginMaintenanceLng.IsPositive() {
return newMarginLevels(marginMaintenanceLng, e.scalingFactorsUint)
if withPotential {
// add margins required to cover the buy and sell orders
longSize := num.DecimalFromInt64(m.Buy()).Div(e.positionFactor)
// size * order price
longMargin := longSize.Mul(m.VWBuy().ToDecimal())
// add limit price * size to the margin required
shortSize := num.DecimalFromInt64(m.Sell()).Div(e.positionFactor).Abs()
// size * (max price - order price)
shortMargin := shortSize.Mul(base.Sub(m.VWSell().ToDecimal()))
marginMaintenanceLng = marginMaintenanceLng.Add(longMargin)
marginMaintenanceSht = marginMaintenanceSht.Add(shortMargin)
}
if marginMaintenanceSht.IsPositive() {
return newMarginLevels(marginMaintenanceSht, e.scalingFactorsUint)
// now get the max margin required
if marginMaintenanceLng.GreaterThan(marginMaintenanceSht) {
return newMarginLevelsFull(marginMaintenanceLng)
}
// if short margin level > 0
if !marginMaintenanceSht.IsZero() {
return newMarginLevelsFull(marginMaintenanceSht)
}

// long margin level <= short, and short is zero, so no margin required.
return &types.MarginLevels{
MaintenanceMargin: num.UintZero(),
SearchLevel: num.UintZero(),
Expand Down

0 comments on commit 386a99a

Please sign in to comment.