Skip to content

Commit

Permalink
Merge pull request #11289 from vegaprotocol/capped-ACs
Browse files Browse the repository at this point in the history
test: add AC coverage
  • Loading branch information
Jiajia-Cui authored May 23, 2024
2 parents 75d7ff9 + 2be1930 commit e489055
Show file tree
Hide file tree
Showing 10 changed files with 765 additions and 50 deletions.
2 changes: 2 additions & 0 deletions core/execution/common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,6 @@ var (
ErrInvalidOrderPrice = errors.New("invalid order price")
// ErrIsolatedMarginFullyCollateralised is returned when a party tries to switch margin modes on a fully collateralised market.
ErrIsolatedMarginFullyCollateralised = errors.New("isolated margin not permitted on fully collateralised markets")
// ErrSettlementDataOutOfRange is returned when a capped future receives settlement data that is outside of the acceptable range (either > max price, or neither 0 nor max for binary settlements).
ErrSettlementDataOutOfRange = errors.New("settlement data is outside of the price cap")
)
73 changes: 62 additions & 11 deletions core/execution/future/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -1158,8 +1158,9 @@ func (m *Market) BlockEnd(ctx context.Context) {

m.nextMTM = t.Add(m.mtmDelta)

// mark price mustn't be zero, except for capped futures, where a zero price may well be possible
if !m.as.InAuction() && (prevMarkPrice == nil || !m.markPriceCalculator.GetPrice().EQ(prevMarkPrice) || m.settlement.HasTraded()) &&
!m.getCurrentMarkPrice().IsZero() {
(!m.getCurrentMarkPrice().IsZero() || m.capMax != nil) {
if m.confirmMTM(ctx, false) {
closedPositions := m.position.GetClosedPositions()
if len(closedPositions) > 0 {
Expand Down Expand Up @@ -1428,7 +1429,11 @@ func (m *Market) getNewPeggedPrice(order *types.Order) (*num.Uint, error) {
price, _ := num.UintFromDecimal(priceInMarket.ToDecimal().Mul(m.priceFactor))

if m.capMax != nil {
price = num.Min(price, m.capMax.Clone())
upper := num.UintZero().Sub(m.capMax, num.UintOne())
price = num.Min(price, upper)
}
if price.IsZero() {
price = num.UintOne()
}
return price, nil
}
Expand All @@ -1444,7 +1449,11 @@ func (m *Market) getNewPeggedPrice(order *types.Order) (*num.Uint, error) {
price, _ = num.UintFromDecimal(priceInMarket.ToDecimal().Mul(m.priceFactor))

if m.capMax != nil {
price = num.Min(price, m.capMax.Clone())
upper := num.UintZero().Sub(m.capMax, num.UintOne())
price = num.Min(price, upper)
}
if price.IsZero() {
price = num.UintOne()
}
return price, nil
}
Expand Down Expand Up @@ -1492,9 +1501,14 @@ func (m *Market) UpdateMarketState(ctx context.Context, changes *types.MarketSta
if m.mkt.State == types.MarketStatePending || m.mkt.State == types.MarketStateProposed {
final = types.MarketStateCancelled
}
m.uncrossOrderAtAuctionEnd(ctx)
// terminate and settle data (either last traded price for perp, or settlement data provided via governance
settlement, _ := num.UintFromDecimal(changes.SettlementPrice.ToDecimal().Mul(m.priceFactor))
if !m.validateSettlementData(settlement) {
// final settlement is not valid/impossible
return common.ErrSettlementDataOutOfRange
}
// in case we're in auction, uncross
m.uncrossOrderAtAuctionEnd(ctx)
m.tradingTerminatedWithFinalState(ctx, final, settlement)
} else if changes.UpdateType == types.MarketStateUpdateTypeSuspend {
m.mkt.State = types.MarketStateSuspendedViaGovernance
Expand Down Expand Up @@ -2290,14 +2304,21 @@ func (m *Market) SubmitOrderWithIDGeneratorAndOrderID(
m.triggerStopOrders(ctx, idgen)
}()
order := orderSubmission.IntoOrder(party)
order.CreatedAt = m.timeService.GetTimeNow().UnixNano()
order.ID = orderID
if order.Price != nil {
order.OriginalPrice = order.Price.Clone()
order.Price, _ = num.UintFromDecimal(order.Price.ToDecimal().Mul(m.priceFactor))
if order.Type == types.OrderTypeLimit && order.PeggedOrder == nil && order.Price.IsZero() {
// limit orders need to be priced > 0
order.Status = types.OrderStatusRejected
order.Reason = types.OrderErrorPriceNotInTickSize // @TODO add new error
m.broker.Send(events.NewOrderEvent(ctx, order))
return nil, common.ErrInvalidOrderPrice
}
}
order.CreatedAt = m.timeService.GetTimeNow().UnixNano()
order.ID = orderID
// check max price in case of capped market
if m.capMax != nil && order.Price != nil && order.Price.GT(m.capMax) {
if m.capMax != nil && order.Price != nil && order.Price.GTE(m.capMax) {
order.Status = types.OrderStatusRejected
order.Reason = types.OrderErrorPriceLTEMaxPrice
m.broker.Send(events.NewOrderEvent(ctx, order))
Expand Down Expand Up @@ -4651,16 +4672,21 @@ func (m *Market) terminateMarket(ctx context.Context, finalState types.MarketSta

m.broker.Send(events.NewMarketUpdatedEvent(ctx, *m.mkt))
var err error
if settlementDataInAsset != nil {
if settlementDataInAsset != nil && m.validateSettlementData(settlementDataInAsset) {
m.settlementDataWithLock(ctx, finalState, settlementDataInAsset)
} else if m.settlementDataInMarket != nil {
// because we need to be able to perform the MTM settlement, only update market state now
settlementDataInAsset, err = m.tradableInstrument.Instrument.Product.ScaleSettlementDataToDecimalPlaces(m.settlementDataInMarket, m.assetDP)
if err != nil {
m.log.Error(err.Error())
} else {
m.settlementDataWithLock(ctx, finalState, settlementDataInAsset)
return
}
if !m.validateSettlementData(settlementDataInAsset) {
m.log.Warn("invalid settlement data", logging.MarketID(m.GetID()))
m.settlementDataInMarket = nil
return
}
m.settlementDataWithLock(ctx, finalState, settlementDataInAsset)
} else {
m.log.Debug("no settlement data", logging.MarketID(m.GetID()))
}
Expand Down Expand Up @@ -4713,6 +4739,13 @@ func (m *Market) settlementData(ctx context.Context, settlementData *num.Numeric
return
}

// validate the settlement data
if !m.validateSettlementData(settlementDataInAsset) {
m.log.Warn("settlement data for capped market is invalid", logging.MarketID(m.GetID()))
// reset settlement data, it's not valid
m.settlementDataInMarket = nil
return
}
m.settlementDataWithLock(ctx, types.MarketStateSettled, settlementDataInAsset)
}

Expand Down Expand Up @@ -4798,13 +4831,31 @@ func (m *Market) settlementDataPerp(ctx context.Context, settlementData *num.Num
m.checkForReferenceMoves(ctx, orderUpdates, false)
}

func (m *Market) validateSettlementData(data *num.Uint) bool {
if m.closed {
return false
}
// not capped, accept the data
if m.fCap == nil {
return true
}
// data > max
if m.capMax.LT(data) {
return false
}
// binary capped market: reject if data is not zero and not == max price.
if m.fCap.Binary && !data.IsZero() && !data.EQ(m.capMax) {
return false
}
return true
}

// NB this must be called with the lock already acquired.
func (m *Market) settlementDataWithLock(ctx context.Context, finalState types.MarketState, settlementDataInAsset *num.Uint) {
if m.closed {
return
}
if m.capMax != nil && m.capMax.LT(settlementDataInAsset) {
// we cannot perform the final settlement because the settlement price is out of the [0, max_price] range
return
}
if m.fCap != nil && m.fCap.Binary {
Expand Down
18 changes: 9 additions & 9 deletions core/integration/features/capped-futures/0016-PFUT-015.feature
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ Feature: Pegged orders are capped to max price.
| party2 | DAI/DEC22 | buy | 1 | 800000000 | 0 | TYPE_LIMIT | TIF_GTC | party2-1 |
| party2 | DAI/DEC22 | buy | 1 | 3500000000 | 0 | TYPE_LIMIT | TIF_GTC | party2-2 |
| party3 | DAI/DEC22 | sell | 1 | 3500000000 | 0 | TYPE_LIMIT | TIF_GTC | party3-1 |
| party3 | DAI/DEC22 | sell | 1 | 4500000000 | 0 | TYPE_LIMIT | TIF_GTC | party3-2 |
| party3 | DAI/DEC22 | sell | 1 | 4499999999 | 0 | TYPE_LIMIT | TIF_GTC | party3-2 |

And the opening auction period ends for market "DAI/DEC22"
Then the following trades should be executed:
| buyer | price | size | seller |
| party2 | 3500000000 | 1 | party3 |
And the market data for the market "DAI/DEC22" should be:
| mark price | best static bid price | static mid price | best static offer price |
| 3500000000 | 800000000 | 2650000000 | 4500000000 |
| 3500000000 | 800000000 | 2649999999 | 4499999999 |
And the order book should have the following volumes for market "DAI/DEC22":
| side | price | volume |
| sell | 4500000000 | 1 |
| sell | 2650000010 | 5 |
| sell | 4499999999 | 1 |
| sell | 2650000009 | 5 |
| buy | 2649999990 | 5 |
| buy | 800000000 | 1 |
# Ensure the price cap is enforced on all orders
Expand All @@ -67,13 +67,13 @@ Feature: Pegged orders are capped to max price.
| party2 | party2-1 |
And the parties place the following orders:
| party | market id | side | volume | price | resulting trades | type | tif | reference |
| party2 | DAI/DEC22 | buy | 1 | 4499999999 | 0 | TYPE_LIMIT | TIF_GTC | party2-2 |
| party2 | DAI/DEC22 | buy | 1 | 4499999998 | 0 | TYPE_LIMIT | TIF_GTC | party2-2 |
Then the market data for the market "DAI/DEC22" should be:
| mark price | best static bid price | static mid price | best static offer price |
| 3500000000 | 4499999999 | 4499999999 | 4500000000 |
| 3500000000 | 4499999998 | 4499999998 | 4499999999 |
# Now the sell order should be capped to max price, buy order is offset by 10
And the order book should have the following volumes for market "DAI/DEC22":
| side | price | volume |
| sell | 4500000000 | 6 |
| buy | 4499999999 | 1 |
| buy | 4499999990 | 5 |
| sell | 4499999999 | 6 |
| buy | 4499999998 | 1 |
| buy | 4499999989 | 5 |
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Feature: Oracle price data within range is used to determine the mid price
| party2 | DAI/DEC22 | buy | 1 | 2500000 | 0 | TYPE_LIMIT | TIF_GTC | party2-1 | |
| party2 | DAI/DEC22 | buy | 1 | 3500000 | 0 | TYPE_LIMIT | TIF_GTC | party2-2 | |
| party3 | DAI/DEC22 | sell | 1 | 3500000 | 0 | TYPE_LIMIT | TIF_GTC | party3-1 | |
| party3 | DAI/DEC22 | sell | 1 | 4500000 | 0 | TYPE_LIMIT | TIF_GTC | party3-2 | |
| party3 | DAI/DEC22 | sell | 1 | 4499999 | 0 | TYPE_LIMIT | TIF_GTC | party3-2 | |
| party3 | DAI/DEC22 | sell | 1 | 8000000 | 0 | TYPE_LIMIT | TIF_GTC | party3-2 | invalid order price |

And the opening auction period ends for market "DAI/DEC22"
Expand All @@ -59,11 +59,12 @@ Feature: Oracle price data within range is used to determine the mid price

And the market data for the market "DAI/DEC22" should be:
| mark price | best static bid price | static mid price | best static offer price |
| 3500000 | 2500000 | 3500000 | 4500000 |
| 3500000 | 2500000 | 3499999 | 4499999 |
And debug detailed orderbook volumes for market "DAI/DEC22"
And the order book should have the following volumes for market "DAI/DEC22":
| side | price | volume |
| sell | 3600000 | 5 |
| sell | 4500000 | 1 |
| sell | 3599999 | 5 |
| sell | 4499999 | 1 |
| buy | 3400000 | 5 |
| buy | 2500000 | 1 |

Expand Down
39 changes: 19 additions & 20 deletions core/integration/features/capped-futures/0016-PFUT-021.feature
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
When the parties place the following orders:
| party | market id | side | volume | price | resulting trades | type | tif | reference | error |
| aux1 | ETH/DEC21 | buy | 2 | 999 | 0 | TYPE_LIMIT | TIF_GTC | ref-1 | |
| aux2 | ETH/DEC21 | sell | 2 | 1500 | 0 | TYPE_LIMIT | TIF_GTC | ref-2 | |
| aux2 | ETH/DEC21 | sell | 2 | 1499 | 0 | TYPE_LIMIT | TIF_GTC | ref-2 | |
| party1 | ETH/DEC21 | buy | 5 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | ref-3 | |
| party2 | ETH/DEC21 | sell | 5 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | ref-4 | |
And the network moves ahead "2" blocks
Expand All @@ -78,7 +78,7 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
| party | market id | maintenance | search | initial | release | margin mode |
| party1 | ETH/DEC21 | 5000 | 5000 | 5000 | 5000 | cross margin |
| party2 | ETH/DEC21 | 2500 | 2500 | 2500 | 2500 | cross margin |
| aux2 | ETH/DEC21 | 0 | 0 | 0 | 0 | cross margin |
| aux2 | ETH/DEC21 | 2 | 2 | 2 | 2 | cross margin |
| aux1 | ETH/DEC21 | 1998 | 1998 | 1998 | 1998 | cross margin |

#update mark price
Expand All @@ -100,7 +100,7 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
| party1 | USD | ETH/DEC21 | 5000 | 5500 |
| party2 | USD | ETH/DEC21 | 2500 | 7000 |
| aux1 | USD | ETH/DEC21 | 3098 | 96908 |
| aux2 | USD | ETH/DEC21 | 400 | 99572 |
| aux2 | USD | ETH/DEC21 | 402 | 99570 |
# The market is fully collateralised, switching to isolated margin is not supported
When the parties submit update margin mode:
| party | market | margin_mode | margin_factor | error |
Expand All @@ -110,47 +110,50 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
#update mark price to max_price
When the parties place the following orders:
| party | market id | side | volume | price | resulting trades | type | tif | reference | error |
| aux3 | ETH/DEC21 | buy | 2 | 1500 | 1 | TYPE_LIMIT | TIF_GTC | aux3-1 | |
| aux3 | ETH/DEC21 | buy | 2 | 1499 | 1 | TYPE_LIMIT | TIF_GTC | aux3-1 | |

And the following trades should be executed:
| buyer | price | size | seller |
| aux3 | 1500 | 2 | aux2 |
| aux3 | 1499 | 2 | aux2 |

And the network moves ahead "2" blocks
Then the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/DEC21"
Then the mark price should be "1500" for the market "ETH/DEC21"
Then the mark price should be "1499" for the market "ETH/DEC21"

# MTM settlement 5 long makes a profit of 2000, 5 short loses 2000
# Now for aux1 and 2, the calculations from above still hold but more margin is required due to the open positions:
# aux1: position * 1100 + 999*2 = 1100 + 1998 = 3098
# aux2: short position of size 2, traded price at 1500, then margin: postion size * (max price - average entry price) = 3*(1100+1500*2)/3
And the parties should have the following account balances:
| party | asset | market id | margin | general |
| party1 | USD | ETH/DEC21 | 5000 | 7500 |
| party2 | USD | ETH/DEC21 | 2500 | 5000 |
| aux1 | USD | ETH/DEC21 | 3098 | 97308 |
| aux2 | USD | ETH/DEC21 | 402 | 99185 |
| aux3 | USD | ETH/DEC21 | 3000 | 96925 |
| party1 | USD | ETH/DEC21 | 5000 | 7495 |
| party2 | USD | ETH/DEC21 | 2500 | 5005 |
| aux1 | USD | ETH/DEC21 | 3098 | 97307 |
| aux2 | USD | ETH/DEC21 | 402 | 99186 |
| aux3 | USD | ETH/DEC21 | 2998 | 96927 |

And the parties should have the following margin levels:
| party | market id | maintenance | search | initial | release | margin mode |
| party1 | ETH/DEC21 | 5000 | 5000 | 5000 | 5000 | cross margin |
| party2 | ETH/DEC21 | 2500 | 2500 | 2500 | 2500 | cross margin |
| aux2 | ETH/DEC21 | 402 | 402 | 402 | 402 | cross margin |
| aux1 | ETH/DEC21 | 3098 | 3098 | 3098 | 3098 | cross margin |
#trade at max_price

#0016-PFUT-024: trade at max_price, no closeout for parties with short position
When the parties place the following orders:
| party | market id | side | volume | price | resulting trades | type | tif | reference |
| aux4 | ETH/DEC21 | buy | 2 | 1500 | 0 | TYPE_LIMIT | TIF_GTC | aux4-1 |
| aux5 | ETH/DEC21 | sell | 2 | 1500 | 1 | TYPE_LIMIT | TIF_GTC | aux5-1 |
| aux4 | ETH/DEC21 | buy | 2 | 1499 | 0 | TYPE_LIMIT | TIF_GTC | aux4-1 |
| aux5 | ETH/DEC21 | sell | 2 | 1499 | 1 | TYPE_LIMIT | TIF_GTC | aux5-1 |

And the network moves ahead "2" blocks

# aux5: short position of size 2, traded price at 1500, then margin: postion size * (max price - average entry price) = 0
And the parties should have the following account balances:
| party | asset | market id | margin | general |
| aux4 | USD | ETH/DEC21 | 3000 | 97015 |
| aux5 | USD | ETH/DEC21 | 0 | 99925 |
| aux1 | USD | ETH/DEC21 | 3098 | 97307 |
| aux2 | USD | ETH/DEC21 | 402 | 99186 |
| aux4 | USD | ETH/DEC21 | 2998 | 97017 |
| aux5 | USD | ETH/DEC21 | 2 | 99923 |

And the following transfers should happen:
| from | to | from account | to account | market id | amount | asset |
Expand All @@ -159,7 +162,3 @@ Feature: When `max_price` is specified and the market is ran in a fully-collater
| aux5 | | ACCOUNT_TYPE_GENERAL | ACCOUNT_TYPE_FEES_LIQUIDITY | ETH/DEC21 | 0 | USD |
| market | aux4 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | ETH/DEC21 | 15 | USD |





Loading

0 comments on commit e489055

Please sign in to comment.