diff --git a/CHANGELOG.md b/CHANGELOG.md index ba226e6f830..8938b6a5494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,11 @@ - [10725](https://github.com/vegaprotocol/vega/issues/10725) - Batch proposal votes to contain `ELS` per market. - [10744](https://github.com/vegaprotocol/vega/issues/10744) - Prevent governance suspension of a market already governance suspended. - [10374](https://github.com/vegaprotocol/vega/issues/10374) - Ledger entries did not return data when filtering by transfer id. +- [10750](https://github.com/vegaprotocol/vega/issues/10750) - Handle cancellation of order on entering auction for party in isolated margin mode. +- [10748](https://github.com/vegaprotocol/vega/issues/10748) - Ensure apply fees cannot fail. +- [10752](https://github.com/vegaprotocol/vega/issues/10752) - Handle amend in place correctly for failure in isolated margin check. +- [10753](https://github.com/vegaprotocol/vega/issues/10753) - Handle the case that a submitted order is `FoK` in isolated margin to not double discount it from position. - [10136](https://github.com/vegaprotocol/vega/issues/10136) - Assure opening auction uncrossing price gets registered in the perps engine. -- [](https://github.com/vegaprotocol/vega/issues/xxx) ## 0.74.3 diff --git a/commands/batch_market_instructions.go b/commands/batch_market_instructions.go index bdf45940225..422a1b1b041 100644 --- a/commands/batch_market_instructions.go +++ b/commands/batch_market_instructions.go @@ -30,10 +30,6 @@ func checkBatchMarketInstructions(cmd *commandspb.BatchMarketInstructions) Error return errs.FinalAddForProperty("batch_market_instructions", ErrIsRequired) } - if len(cmd.UpdateMarginMode) > 0 { - return errs.FinalAddForProperty("batch_market_instructions.update_margin_mode", ErrIsDisabled) - } - // there's very little to verify here, only if the batch is not empty // all transaction verification is done when processing then. if len(cmd.Cancellations)+ diff --git a/commands/transaction.go b/commands/transaction.go index 5207ce75a11..bd401e7b167 100644 --- a/commands/transaction.go +++ b/commands/transaction.go @@ -253,9 +253,7 @@ func CheckInputData(rawInputData []byte) (*commandspb.InputData, Errors) { case *commandspb.InputData_ApplyReferralCode: errs.Merge(checkApplyReferralCode(cmd.ApplyReferralCode)) case *commandspb.InputData_UpdateMarginMode: - // FIXME: Disable Update margin mode for now - errs.AddForProperty("update_margin_mode", ErrIsDuplicated) - // errs.Merge(checkUpdateMarginMode(cmd.UpdateMarginMode)) + errs.Merge(checkUpdateMarginMode(cmd.UpdateMarginMode)) case *commandspb.InputData_JoinTeam: errs.Merge(checkJoinTeam(cmd.JoinTeam)) case *commandspb.InputData_UpdatePartyProfile: diff --git a/core/collateral/engine.go b/core/collateral/engine.go index 6ac578ac348..35c30b7b47a 100644 --- a/core/collateral/engine.go +++ b/core/collateral/engine.go @@ -929,6 +929,25 @@ func (e *Engine) TransferFeesContinuousTrading(ctx context.Context, marketID str return e.transferFees(ctx, marketID, assetID, ft) } +func (e *Engine) PartyCanCoverFees(asset, mktID, partyID string, amount *num.Uint) error { + generalAccount, err := e.GetPartyGeneralAccount(partyID, asset) + if err != nil { + return err + } + generalAccountBalance := generalAccount.Balance + marginAccount, _ := e.GetPartyMarginAccount(mktID, partyID, asset) + + marginAccountBalance := num.UintZero() + if marginAccount != nil { + marginAccountBalance = marginAccount.Balance + } + + if num.Sum(generalAccountBalance, marginAccountBalance).LT(amount) { + return fmt.Errorf("party has insufficient funds to cover fees") + } + return nil +} + func (e *Engine) transferFees(ctx context.Context, marketID string, assetID string, ft events.FeesTransfer) ([]*types.LedgerMovement, error) { makerFee, infraFee, liquiFee, err := e.getFeesAccounts(marketID, assetID) if err != nil { diff --git a/core/collateral/engine_test.go b/core/collateral/engine_test.go index 2acc04ac655..75ddbb26266 100644 --- a/core/collateral/engine_test.go +++ b/core/collateral/engine_test.go @@ -18,6 +18,7 @@ package collateral_test import ( "context" "encoding/hex" + "fmt" "strconv" "testing" "time" @@ -428,6 +429,41 @@ func testFeesTransferContinuousNoTransfer(t *testing.T) { assert.Nil(t, err) } +func TestPartyHasSufficientBalanceForFees(t *testing.T) { + eng := getTestEngine(t) + defer eng.Finish() + + party := "myparty" + // create party + eng.broker.EXPECT().Send(gomock.Any()).Times(3) + gen, err := eng.CreatePartyGeneralAccount(context.Background(), party, testMarketAsset) + require.NoError(t, err) + + mar, err := eng.CreatePartyMarginAccount(context.Background(), party, testMarketID, testMarketAsset) + require.NoError(t, err) + + // add funds + eng.broker.EXPECT().Send(gomock.Any()).Times(1) + err = eng.UpdateBalance(context.Background(), gen, num.NewUint(100)) + assert.Nil(t, err) + + // there's no margin account balance but the party has enough funds in the margin account + require.Nil(t, eng.PartyCanCoverFees(testMarketAsset, testMarketID, party, num.NewUint(50))) + + // there's no margin account balance and the party has insufficient funds to cover fees in the general account + require.Error(t, fmt.Errorf("party has insufficient funds to cover fees"), eng.PartyCanCoverFees(testMarketAsset, testMarketID, party, num.NewUint(101))) + + eng.broker.EXPECT().Send(gomock.Any()).Times(1) + err = eng.UpdateBalance(context.Background(), mar, num.NewUint(500)) + assert.Nil(t, err) + + // there's enough in the margin + general to cover the fees + require.Nil(t, eng.PartyCanCoverFees(testMarketAsset, testMarketID, party, num.NewUint(101))) + + // there's not enough in the margin + general to cover the fees + require.Error(t, fmt.Errorf("party has insufficient funds to cover fees"), eng.PartyCanCoverFees(testMarketAsset, testMarketID, party, num.NewUint(601))) +} + func testReleasePartyMarginAccount(t *testing.T) { eng := getTestEngine(t) defer eng.Finish() diff --git a/core/execution/common/interfaces.go b/core/execution/common/interfaces.go index 913fa30b6a6..4ed8138c0de 100644 --- a/core/execution/common/interfaces.go +++ b/core/execution/common/interfaces.go @@ -179,6 +179,7 @@ type Collateral interface { ReleaseFromHoldingAccount(ctx context.Context, transfer *types.Transfer) (*types.LedgerMovement, error) ClearSpotMarket(ctx context.Context, mktID, quoteAsset string) ([]*types.LedgerMovement, error) PartyHasSufficientBalance(asset, partyID string, amount *num.Uint) error + PartyCanCoverFees(asset, mktID, partyID string, amount *num.Uint) error TransferSpot(ctx context.Context, partyID, toPartyID, asset string, quantity *num.Uint) (*types.LedgerMovement, error) GetOrCreatePartyLiquidityFeeAccount(ctx context.Context, partyID, marketID, asset string) (*types.Account, error) GetPartyLiquidityFeeAccount(market, partyID, asset string) (*types.Account, error) diff --git a/core/execution/common/mocks/mocks.go b/core/execution/common/mocks/mocks.go index 46ddd547e78..0eff9107b28 100644 --- a/core/execution/common/mocks/mocks.go +++ b/core/execution/common/mocks/mocks.go @@ -716,6 +716,20 @@ func (mr *MockCollateralMockRecorder) MarkToMarket(arg0, arg1, arg2, arg3, arg4 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkToMarket", reflect.TypeOf((*MockCollateral)(nil).MarkToMarket), arg0, arg1, arg2, arg3, arg4) } +// PartyCanCoverFees mocks base method. +func (m *MockCollateral) PartyCanCoverFees(arg0, arg1, arg2 string, arg3 *num.Uint) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PartyCanCoverFees", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// PartyCanCoverFees indicates an expected call of PartyCanCoverFees. +func (mr *MockCollateralMockRecorder) PartyCanCoverFees(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PartyCanCoverFees", reflect.TypeOf((*MockCollateral)(nil).PartyCanCoverFees), arg0, arg1, arg2, arg3) +} + // PartyHasSufficientBalance mocks base method. func (m *MockCollateral) PartyHasSufficientBalance(arg0, arg1 string, arg2 *num.Uint) error { m.ctrl.T.Helper() diff --git a/core/execution/future/market.go b/core/execution/future/market.go index 7da4b85b362..ff067041dda 100644 --- a/core/execution/future/market.go +++ b/core/execution/future/market.go @@ -1099,6 +1099,10 @@ func (m *Market) BlockEnd(ctx context.Context) { markPriceCopy = m.markPriceCalculator.GetPrice().Clone() } m.liquidity.EndBlock(markPriceCopy, m.midPrice(), m.positionFactor) + + if !m.matching.CheckBook() { + m.log.Panic("ontick book has orders pegged to nothing") + } } func (m *Market) removeAllStopOrders( @@ -2299,6 +2303,11 @@ func (m *Market) submitValidatedOrder(ctx context.Context, order *types.Order) ( order.Status = types.OrderStatusActive + var aggressorFee *num.Uint + if fees != nil { + aggressorFee = fees.TotalFeesAmountPerParty()[order.Party] + } + // NB: this is the position with the trades included and the order sizes updated to remaining!!! // NB: this is not touching the actual position from the position engine but is all done on a clone, so that // in handle confirmation this will be done as per normal. @@ -2313,14 +2322,23 @@ func (m *Market) submitValidatedOrder(ctx context.Context, order *types.Order) ( // If the trade would decrease the party's position, that portion will trade and margin will be released as in the Decreasing Position. // If the order is not persistent this is the end, if it is persistent any portion of the order which // has not traded in step 1 will move to being placed on the order book. - if len(trades) > 0 && marginMode == types.MarginModeIsolatedMargin { - if err := m.updateIsolatedMarginOnAggressor(ctx, posWithTrades, order, trades, false); err != nil { - if m.log.GetLevel() <= logging.DebugLevel { - m.log.Debug("Unable to check/add immediate trade margin for party", - logging.Order(*order), logging.Error(err)) + if len(trades) > 0 { + if marginMode == types.MarginModeIsolatedMargin { + // check that the party can cover the trade AND the fees + if err := m.updateIsolatedMarginOnAggressor(ctx, posWithTrades, order, trades, false, aggressorFee); err != nil { + if m.log.GetLevel() <= logging.DebugLevel { + m.log.Debug("Unable to check/add immediate trade margin for party", + logging.Order(*order), logging.Error(err)) + } + _ = m.position.UnregisterOrder(ctx, order) + return nil, nil, common.ErrMarginCheckFailed + } + } else if aggressorFee != nil { + if err := m.collateral.PartyCanCoverFees(m.settlementAsset, m.mkt.ID, order.Party, aggressorFee); err != nil { + m.log.Error("insufficient funds to cover fees", logging.Order(order), logging.Error(err)) + m.unregisterAndReject(ctx, order, types.OrderErrorInsufficientFundsToPayFees) + return nil, nil, err } - _ = m.position.UnregisterOrder(ctx, order) - return nil, nil, common.ErrMarginCheckFailed } } @@ -2351,7 +2369,7 @@ func (m *Market) submitValidatedOrder(ctx context.Context, order *types.Order) ( // the contains the fees information confirmation.Trades = trades - if marginMode == types.MarginModeIsolatedMargin { + if marginMode == types.MarginModeIsolatedMargin && order.Status == types.OrderStatusActive && order.TrueRemaining() > 0 { // now we need to check if the party has sufficient funds to cover the order margin for the remaining size // if not the remaining order is cancelled. // if successful the required order margin are transferred to the order margin account. @@ -2363,17 +2381,22 @@ func (m *Market) submitValidatedOrder(ctx context.Context, order *types.Order) ( _ = m.unregisterAndReject( ctx, order, types.OrderErrorMarginCheckFailed) m.matching.RemoveOrder(order.ID) + if len(trades) > 0 { + if err = m.applyFees(ctx, order, fees); err != nil { + m.log.Panic("failed to apply fees on order", logging.Order(order), logging.String("aggressor total fees", fees.TotalFeesAmountPerParty()[order.ID].String()), logging.Error(err)) + } + // if there were trades we need to return the confirmation so the trades can be handled + // otherwise they were just removed from the book for the passive side and gone + orderUpdates := m.handleConfirmation(ctx, confirmation, nil) + return confirmation, orderUpdates, common.ErrMarginCheckFailed + } return nil, nil, common.ErrMarginCheckFailed } } if fees != nil { - err = m.applyFees(ctx, order, fees) - if err != nil { - _ = m.unregisterAndReject( - ctx, order, types.OrderErrorMarginCheckFailed) - m.matching.RemoveOrder(order.ID) - return nil, nil, common.ErrMarginCheckFailed + if err = m.applyFees(ctx, order, fees); err != nil { + m.log.Panic("failed to apply fees on order", logging.Order(order), logging.String("aggressor total fees", fees.TotalFeesAmountPerParty()[order.ID].String()), logging.Error(err)) } } @@ -2974,7 +2997,7 @@ func (m *Market) checkMarginForOrder(ctx context.Context, pos *positions.MarketP // updateIsolatedMarginOnAggressor is called when a new or amended order is matched immediately upon submission. // it checks that new margin requirements can be satisfied and if so transfers the margin from the general account to the margin account. -func (m *Market) updateIsolatedMarginOnAggressor(ctx context.Context, pos *positions.MarketPosition, order *types.Order, trades []*types.Trade, isAmend bool) error { +func (m *Market) updateIsolatedMarginOnAggressor(ctx context.Context, pos *positions.MarketPosition, order *types.Order, trades []*types.Trade, isAmend bool, fees *num.Uint) error { marketObservable, mpos, increment, _, marginFactor, orders, err := m.getIsolatedMarginContext(pos, order) if err != nil { return err @@ -3007,7 +3030,11 @@ func (m *Market) updateIsolatedMarginOnAggressor(ctx context.Context, pos *posit } } - risk, err := m.risk.UpdateIsolatedMarginOnAggressor(ctx, mpos, marketObservable, increment, clonedOrders, trades, marginFactor, order.Side, isAmend) + aggressorFee := num.UintZero() + if fees != nil { + aggressorFee = fees.Clone() + } + risk, err := m.risk.UpdateIsolatedMarginOnAggressor(ctx, mpos, marketObservable, increment, clonedOrders, trades, marginFactor, order.Side, isAmend, aggressorFee) if err != nil { return err } @@ -3017,6 +3044,21 @@ func (m *Market) updateIsolatedMarginOnAggressor(ctx context.Context, pos *posit return m.transferMargins(ctx, risk, nil) } +func (m *Market) updateIsolatedMarginOnOrderCancel(ctx context.Context, mpos *positions.MarketPosition, order *types.Order) error { + marketObservable, pos, increment, auctionPrice, marginFactor, orders, err := m.getIsolatedMarginContext(mpos, order) + if err != nil { + return err + } + risk, err := m.risk.UpdateIsolatedMarginOnOrderCancel(ctx, pos, orders, marketObservable, auctionPrice, increment, marginFactor) + if err != nil { + return err + } + if risk == nil { + return nil + } + return m.transferMargins(ctx, []events.Risk{risk}, nil) +} + func (m *Market) updateIsolatedMarginOnOrder(ctx context.Context, mpos *positions.MarketPosition, order *types.Order) error { marketObservable, pos, increment, auctionPrice, marginFactor, orders, err := m.getIsolatedMarginContext(mpos, order) if err != nil { @@ -3307,9 +3349,11 @@ func (m *Market) cancelOrder(ctx context.Context, partyID, orderID string) (*typ // order margin if foundOnBook && m.getMarginMode(partyID) == types.MarginModeIsolatedMargin { pos, _ := m.position.GetPositionByPartyID(partyID) - if err := m.updateIsolatedMarginOnOrder(ctx, pos, order); err != nil { - m.log.Panic("failed to update order margin after order cancellation", logging.Order(order), logging.String("party", pos.Party())) - } + // it might be that we place orders before an auction, then during an auction we're trying to cancel the order - if we have still other order + // they will definitely have insufficient order margin but that's ok, either they will be fine when uncrossing the auction + // or will get cancelled then, no need to punish the party and cancel them at this point. Therefore this can either release funds + // from the order account or error which we ignore. + m.updateIsolatedMarginOnOrderCancel(ctx, pos, order) } return &types.OrderCancellationConfirmation{Order: order}, nil @@ -3384,6 +3428,7 @@ func (m *Market) AmendOrderWithIDGenerator( conf, updatedOrders, err := m.amendOrder(ctx, orderAmendment, party) if err != nil { + m.log.Error("failed to amend order", logging.String("marketID", orderAmendment.MarketID), logging.String("orderID", orderAmendment.OrderID), logging.Error(err)) if m.getMarginMode(party) == types.MarginModeIsolatedMargin && err == common.ErrMarginCheckFailed { m.handleIsolatedMarginInsufficientOrderMargin(ctx, party) } @@ -3688,6 +3733,8 @@ func (m *Market) amendOrder( if err := m.updateIsolatedMarginOnOrder(ctx, pos, amendedOrder); err == risk.ErrInsufficientFundsForMarginInGeneralAccount { m.log.Error("party has insufficient margin to cover the order change, going to cancel all orders for the party") _ = m.position.AmendOrder(ctx, amendedOrder, existingOrder) + // we're amending the order back in the order book so that when we unregister it, it would match what the position expects + m.orderAmendInPlace(amendedOrder, existingOrder) return nil, nil, common.ErrMarginCheckFailed } } @@ -3769,8 +3816,7 @@ func (m *Market) orderCancelReplace( } if fees != nil { if feeErr := m.applyFees(ctx, newOrder, fees); feeErr != nil { - _ = m.position.AmendOrder(ctx, newOrder, existingOrder) - return + m.log.Panic("orderCancelReplace failed to apply fees on order", logging.Order(newOrder), logging.String("aggressor total fees", fees.TotalFeesAmountPerParty()[newOrder.ID].String()), logging.Error(feeErr)) } } orders = m.handleConfirmation(ctx, conf, nil) @@ -3830,18 +3876,29 @@ func (m *Market) orderCancelReplace( return nil, nil, errors.New("could not calculate fees for order") } + var aggressorFee *num.Uint + if fees != nil { + aggressorFee = fees.TotalFeesAmountPerParty()[newOrder.Party] + } + marginMode := m.getMarginMode(newOrder.Party) pos, _ := m.position.GetPositionByPartyID(newOrder.Party) posWithTrades := pos - if len(trades) > 0 && marginMode == types.MarginModeIsolatedMargin { - posWithTrades = pos.UpdateInPlaceOnTrades(m.log, newOrder.Side, trades, newOrder) - // NB: this is the position with the trades included and the order sizes updated to remaining!!! - if err = m.updateIsolatedMarginOnAggressor(ctx, posWithTrades, newOrder, trades, true); err != nil { - if m.log.GetLevel() <= logging.DebugLevel { - m.log.Debug("Unable to check/add immediate trade margin for party", - logging.Order(*newOrder), logging.Error(err)) + if len(trades) > 0 { + if marginMode == types.MarginModeIsolatedMargin { + posWithTrades = pos.UpdateInPlaceOnTrades(m.log, newOrder.Side, trades, newOrder) + if err = m.updateIsolatedMarginOnAggressor(ctx, posWithTrades, newOrder, trades, true, aggressorFee); err != nil { + if m.log.GetLevel() <= logging.DebugLevel { + m.log.Debug("Unable to check/add immediate trade margin for party", + logging.Order(*newOrder), logging.Error(err)) + } + return nil, nil, common.ErrMarginCheckFailed + } + } else if aggressorFee != nil { + if err := m.collateral.PartyCanCoverFees(m.settlementAsset, m.mkt.ID, newOrder.Party, aggressorFee); err != nil { + m.log.Error("insufficient funds to cover fees", logging.Order(newOrder), logging.Error(err)) + return nil, nil, err } - return nil, nil, common.ErrMarginCheckFailed } } diff --git a/core/matching/orderbook.go b/core/matching/orderbook.go index 12b78773c13..06df6787357 100644 --- a/core/matching/orderbook.go +++ b/core/matching/orderbook.go @@ -631,6 +631,34 @@ func (b *OrderBook) CancelAllOrders(party string) ([]*types.OrderCancellationCon return confs, err } +func (b *OrderBook) CheckBook() bool { + if len(b.buy.levels) > 0 { + allPegged := true + for _, o := range b.buy.levels[len(b.buy.levels)-1].orders { + if o.PeggedOrder == nil || o.PeggedOrder.Reference != types.PeggedReferenceBestBid { + allPegged = false + break + } + } + if allPegged { + return false + } + } + if len(b.sell.levels) > 0 { + allPegged := true + for _, o := range b.sell.levels[len(b.sell.levels)-1].orders { + if o.PeggedOrder == nil || o.PeggedOrder.Reference != types.PeggedReferenceBestAsk { + allPegged = false + break + } + } + if allPegged { + return false + } + } + return true +} + // CancelOrder cancel an order that is active on an order book. Market and Order ID are validated, however the order must match // the order on the book with respect to side etc. The caller will typically validate this by using a store, we should // not trust that the external world can provide these values reliably. diff --git a/core/risk/engine.go b/core/risk/engine.go index 6473f3e6140..2f55a94555a 100644 --- a/core/risk/engine.go +++ b/core/risk/engine.go @@ -39,6 +39,7 @@ var ( ErrInsufficientFundsForOrderMargin = errors.New("insufficient funds for order margin") ErrInsufficientFundsForMarginInGeneralAccount = errors.New("insufficient funds to cover margin in general margin") ErrRiskFactorsNotAvailableForAsset = errors.New("risk factors not available for the specified asset") + ErrInsufficientFundsToCoverTradeFees = errors.New("insufficient funds to cover fees") ) const RiskFactorStateVarName = "risk-factors" diff --git a/core/risk/isolated_margin.go b/core/risk/isolated_margin.go index 063161f6592..bf517c555cd 100644 --- a/core/risk/isolated_margin.go +++ b/core/risk/isolated_margin.go @@ -74,7 +74,7 @@ func (e *Engine) ReleaseExcessMarginAfterAuctionUncrossing(ctx context.Context, // NB: evt has the position after the trades + orders need to include the new order with the updated remaining. // returns an error if the new margin is invalid or if the margin account cannot be topped up from general account. // if successful it updates the margin level and returns the transfer that is needed for the topup of the margin account or release from the margin account excess. -func (e *Engine) UpdateIsolatedMarginOnAggressor(ctx context.Context, evt events.Margin, marketObservable *num.Uint, increment num.Decimal, orders []*types.Order, trades []*types.Trade, marginFactor num.Decimal, traderSide types.Side, isAmend bool) ([]events.Risk, error) { +func (e *Engine) UpdateIsolatedMarginOnAggressor(ctx context.Context, evt events.Margin, marketObservable *num.Uint, increment num.Decimal, orders []*types.Order, trades []*types.Trade, marginFactor num.Decimal, traderSide types.Side, isAmend bool, fees *num.Uint) ([]events.Risk, error) { if evt == nil { return nil, nil } @@ -102,6 +102,14 @@ func (e *Engine) UpdateIsolatedMarginOnAggressor(ctx context.Context, evt events if isAmend && requiredMargin.GT(num.Sum(evt.GeneralAccountBalance(), evt.OrderMarginBalance())) { return nil, ErrInsufficientFundsForMarginInGeneralAccount } + // new order, given that they can cover for the trade, do they have enough left to cover the fees? + if !isAmend && num.Sum(requiredMargin, fees).GT(num.Sum(evt.GeneralAccountBalance(), evt.MarginBalance())) { + return nil, ErrInsufficientFundsToCoverTradeFees + } + // amended order, given that they can cover for the trade, do they have enough left to cover the fees for the amended order's trade? + if isAmend && num.Sum(requiredMargin, fees).GT(num.Sum(evt.GeneralAccountBalance(), evt.MarginBalance(), evt.OrderMarginBalance())) { + return nil, ErrInsufficientFundsToCoverTradeFees + } } } else { // position did switch sides @@ -130,6 +138,9 @@ func (e *Engine) UpdateIsolatedMarginOnAggressor(ctx context.Context, evt events if requiredMargin.GT(num.Sum(evt.GeneralAccountBalance(), evt.MarginBalance())) { return nil, ErrInsufficientFundsForMarginInGeneralAccount } + if num.Sum(requiredMargin, fees).GT(num.Sum(evt.GeneralAccountBalance(), evt.MarginBalance())) { + return nil, ErrInsufficientFundsToCoverTradeFees + } } e.updateMarginLevels(events.NewMarginLevelsEvent(ctx, *margins)) @@ -196,6 +207,45 @@ func (e *Engine) UpdateIsolatedMarginOnOrder(ctx context.Context, evt events.Mar return change, nil } +func (e *Engine) UpdateIsolatedMarginOnOrderCancel(ctx context.Context, evt events.Margin, orders []*types.Order, marketObservable *num.Uint, auctionPrice *num.Uint, increment num.Decimal, marginFactor num.Decimal) (events.Risk, error) { + auction := e.as.InAuction() && !e.as.CanLeave() + var ap *num.Uint + if auction { + ap = auctionPrice + } + margins := e.calculateIsolatedMargins(evt, marketObservable, increment, marginFactor, ap, orders) + if margins.OrderMargin.GT(evt.OrderMarginBalance()) { + return nil, ErrInsufficientFundsForOrderMargin + } + + e.updateMarginLevels(events.NewMarginLevelsEvent(ctx, *margins)) + var amt *num.Uint + tp := types.TransferTypeOrderMarginHigh + amt = num.UintZero().Sub(evt.OrderMarginBalance(), margins.OrderMargin) + + var trnsfr *types.Transfer + if amt.IsZero() { + return nil, nil + } + + trnsfr = &types.Transfer{ + Owner: evt.Party(), + Type: tp, + Amount: &types.FinancialAmount{ + Asset: evt.Asset(), + Amount: amt, + }, + MinAmount: amt.Clone(), + } + + change := &marginChange{ + Margin: evt, + transfer: trnsfr, + margins: margins, + } + return change, nil +} + // UpdateIsolatedMarginOnPositionChanged is called upon changes to the position of a party in isolated margin mode. // Depending on the nature of the change it checks if it needs to move funds into our out of the margin account from the // order margin account or to the general account. @@ -205,16 +255,16 @@ func (e *Engine) UpdateIsolatedMarginsOnPositionChange(ctx context.Context, evt return nil, nil } margins := e.calculateIsolatedMargins(evt, marketObservable, increment, marginFactor, nil, orders) + ret := []events.Risk{} transfer := getIsolatedMarginTransfersOnPositionChange(evt.Party(), evt.Asset(), trades, traderSide, evt.Size(), e.positionFactor, marginFactor, evt.MarginBalance(), evt.OrderMarginBalance(), marketObservable, false, false) e.updateMarginLevels(events.NewMarginLevelsEvent(ctx, *margins)) - ret := []events.Risk{ - &marginChange{ + if transfer != nil { + ret = append(ret, &marginChange{ Margin: evt, transfer: transfer[0], margins: margins, - }, + }) } - var amtForRelease *num.Uint if !evt.OrderMarginBalance().IsZero() && margins.OrderMargin.IsZero() && transfer != nil && evt.OrderMarginBalance().GT(transfer[0].Amount.Amount) { amtForRelease = num.UintZero().Sub(evt.OrderMarginBalance(), transfer[0].Amount.Amount)