diff --git a/core/integration/features/capped-futures/0016-PFUT-021.feature b/core/integration/features/capped-futures/0016-PFUT-021.feature index 36d526cec06..c04ce58fd0c 100644 --- a/core/integration/features/capped-futures/0016-PFUT-021.feature +++ b/core/integration/features/capped-futures/0016-PFUT-021.feature @@ -32,8 +32,8 @@ 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. @@ -41,7 +41,7 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater 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 | @@ -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: @@ -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 | diff --git a/core/integration/steps/market/defaults/margin-calculator/default-capped-margin-calculator.json b/core/integration/steps/market/defaults/margin-calculator/default-capped-margin-calculator.json new file mode 100644 index 00000000000..456b359e776 --- /dev/null +++ b/core/integration/steps/market/defaults/margin-calculator/default-capped-margin-calculator.json @@ -0,0 +1,7 @@ +{ + "scalingFactors": { + "searchLevel": 1, + "initialMargin": 1, + "collateralRelease": 1 + } +} diff --git a/core/integration/steps/market/defaults/margin-calculator/default-margin-calculator.json b/core/integration/steps/market/defaults/margin-calculator/default-margin-calculator.json index 126f9eadf35..456b359e776 100644 --- a/core/integration/steps/market/defaults/margin-calculator/default-margin-calculator.json +++ b/core/integration/steps/market/defaults/margin-calculator/default-margin-calculator.json @@ -1,7 +1,7 @@ { "scalingFactors": { - "searchLevel": 1.1, - "initialMargin": 1.2, - "collateralRelease": 1.4 + "searchLevel": 1, + "initialMargin": 1, + "collateralRelease": 1 } } diff --git a/core/integration/steps/market/margin_calculators.go b/core/integration/steps/market/margin_calculators.go index e29e49dd118..faabcb6798b 100644 --- a/core/integration/steps/market/margin_calculators.go +++ b/core/integration/steps/market/margin_calculators.go @@ -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", } ) diff --git a/core/integration/steps/the_markets.go b/core/integration/steps/the_markets.go index 2596c445007..1373353be09 100644 --- a/core/integration/steps/the_markets.go +++ b/core/integration/steps/the_markets.go @@ -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) diff --git a/core/risk/margins_calculation.go b/core/risk/margins_calculation.go index 4fa7e6150af..4c68c6b2191 100644 --- a/core/risk/margins_calculation.go +++ b/core/risk/margins_calculation.go @@ -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{ @@ -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(), @@ -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(),