code.vegaprotocol.io/vega@v0.79.0/core/execution/stoporders/stop_orders.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package stoporders
    17  
    18  import (
    19  	"log"
    20  	"sort"
    21  
    22  	"code.vegaprotocol.io/vega/core/positions"
    23  	"code.vegaprotocol.io/vega/core/types"
    24  	"code.vegaprotocol.io/vega/libs/num"
    25  	"code.vegaprotocol.io/vega/logging"
    26  	v1 "code.vegaprotocol.io/vega/protos/vega/snapshot/v1"
    27  
    28  	"golang.org/x/exp/maps"
    29  )
    30  
    31  type Pool struct {
    32  	log *logging.Logger
    33  	// map partyId * map orderId * StopOrder
    34  	orders map[string]map[string]*types.StopOrder
    35  	// useful to find back a party from an order
    36  	orderToParty map[string]string
    37  	priced       *PricedStopOrders
    38  	trailing     *TrailingStopOrders
    39  }
    40  
    41  func New(log *logging.Logger) *Pool {
    42  	return &Pool{
    43  		log:          log,
    44  		orders:       map[string]map[string]*types.StopOrder{},
    45  		orderToParty: map[string]string{},
    46  		priced:       NewPricedStopOrders(),
    47  		trailing:     NewTrailingStopOrders(),
    48  	}
    49  }
    50  
    51  func NewFromProto(log *logging.Logger, p *v1.StopOrders) *Pool {
    52  	pool := New(log)
    53  
    54  	for _, porder := range p.StopOrders {
    55  		order := types.NewStopOrderFromProto(porder)
    56  
    57  		if party, ok := pool.orders[order.Party]; ok {
    58  			if _, ok := party[order.ID]; ok {
    59  				pool.log.Panic("stop order already exists", logging.String("id", order.ID))
    60  			}
    61  		} else {
    62  			pool.orders[order.Party] = map[string]*types.StopOrder{}
    63  		}
    64  
    65  		pool.orders[order.Party][order.ID] = order
    66  		pool.orderToParty[order.ID] = order.Party
    67  	}
    68  
    69  	pool.priced = NewPricedStopOrdersFromProto(p.PricedStopOrders)
    70  	pool.trailing = NewTrailingStopOrdersFromProto(p.TrailingStopOrders)
    71  
    72  	return pool
    73  }
    74  
    75  func (p *Pool) ToProto() *v1.StopOrders {
    76  	out := &v1.StopOrders{}
    77  
    78  	for _, v := range p.orders {
    79  		for _, order := range v {
    80  			out.StopOrders = append(out.StopOrders, order.ToProtoEvent())
    81  		}
    82  	}
    83  
    84  	sort.Slice(out.StopOrders, func(i, j int) bool {
    85  		return out.StopOrders[i].StopOrder.Id < out.StopOrders[j].StopOrder.Id
    86  	})
    87  
    88  	out.PricedStopOrders = p.priced.ToProto()
    89  	out.TrailingStopOrders = p.trailing.ToProto()
    90  
    91  	return out
    92  }
    93  
    94  func (p *Pool) GetStopOrderCount() uint64 {
    95  	return uint64(len(p.orderToParty))
    96  }
    97  
    98  func (p *Pool) Settled() []*types.StopOrder {
    99  	out := []*types.StopOrder{}
   100  	for _, v := range p.orders {
   101  		for _, so := range v {
   102  			so.Status = types.StopOrderStatusStopped
   103  			out = append(out, so)
   104  		}
   105  	}
   106  
   107  	sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
   108  	return out
   109  }
   110  
   111  func (p *Pool) CheckDirection(positions *positions.SnapshotEngine) []*types.StopOrder {
   112  	toCancel := []*types.StopOrder{}
   113  	for partyID, orders := range p.orders {
   114  		pos, ok := positions.GetPositionByPartyID(partyID)
   115  		if !ok {
   116  			continue
   117  		}
   118  		for _, order := range orders {
   119  			if order.SizeOverrideSetting == types.StopOrderSizeOverrideSettingPosition {
   120  				if pos.Size() == 0 {
   121  					continue
   122  				} else if pos.Size() > 0 {
   123  					// Order needs to be a sell
   124  					if order.OrderSubmission.Side != types.SideSell {
   125  						order.Status = types.StopOrderStatusCancelled
   126  						toCancel = append(toCancel, order)
   127  					}
   128  				} else {
   129  					// Order needs to be a buy
   130  					if order.OrderSubmission.Side != types.SideBuy {
   131  						order.Status = types.StopOrderStatusCancelled
   132  						toCancel = append(toCancel, order)
   133  					}
   134  				}
   135  			}
   136  		}
   137  	}
   138  
   139  	// Remove the deleted items from the map
   140  	for _, stopOrder := range toCancel {
   141  		delete(p.orders[stopOrder.Party], stopOrder.OrderID)
   142  		delete(p.orderToParty, stopOrder.OrderID)
   143  	}
   144  	return toCancel
   145  }
   146  
   147  func (p *Pool) PriceUpdated(newPrice *num.Uint) (triggered, cancelled []*types.StopOrder) {
   148  	// first update prices and get triggered orders
   149  	ids := append(
   150  		p.priced.PriceUpdated(newPrice.Clone()),
   151  		p.trailing.PriceUpdated(newPrice.Clone())...,
   152  	)
   153  
   154  	// first get all the orders which got triggered
   155  	for _, v := range ids {
   156  		pid, ok := p.orderToParty[v]
   157  		if !ok {
   158  			log.Panic("order in tree but not in pool", logging.String("order-id", v))
   159  		}
   160  
   161  		// not needed anymore
   162  		delete(p.orderToParty, v)
   163  
   164  		orders, ok := p.orders[pid]
   165  		if !ok {
   166  			p.log.Panic("party was expected to have orders but have none",
   167  				logging.String("party-id", pid), logging.String("order-id", v))
   168  		}
   169  
   170  		// now we are down to the actual order
   171  		sorder, ok := orders[v]
   172  		if !ok {
   173  			p.log.Panic("party was expected to have an order",
   174  				logging.String("party-id", pid), logging.String("order-id", v))
   175  		}
   176  
   177  		sorder.Status = types.StopOrderStatusTriggered
   178  		triggered = append(triggered, sorder)
   179  
   180  		// now we can cleanup
   181  		delete(orders, v)
   182  		if len(orders) <= 0 {
   183  			// we can remove the trader altogether
   184  			delete(p.orders, pid)
   185  		}
   186  	}
   187  
   188  	// now we get all the OCO opposite to them as they shall
   189  	// be cancelled as well
   190  	for _, v := range triggered[:] {
   191  		if len(v.OCOLinkID) <= 0 {
   192  			continue
   193  		}
   194  
   195  		res, err := p.removeWithOCO(v.Party, v.OCOLinkID, false)
   196  		if err != nil || len(res) <= 0 {
   197  			// that should never happen, this mean for some
   198  			// reason that the other side of the OCO has been
   199  			// remove and left the pool in a bad state
   200  			p.log.Panic("other side of the oco missing from the pool",
   201  				logging.Error(err),
   202  				logging.PartyID(v.Party),
   203  				logging.OrderID(v.OCOLinkID))
   204  		}
   205  
   206  		// only one order returned here
   207  		res[0].Status = types.StopOrderStatusStopped
   208  		cancelled = append(cancelled, res[0])
   209  	}
   210  
   211  	return triggered, cancelled
   212  }
   213  
   214  func (p *Pool) Insert(order *types.StopOrder) {
   215  	if party, ok := p.orders[order.Party]; ok {
   216  		if _, ok := party[order.ID]; ok {
   217  			p.log.Panic("stop order already exists", logging.String("id", order.ID))
   218  		}
   219  	} else {
   220  		p.orders[order.Party] = map[string]*types.StopOrder{}
   221  	}
   222  
   223  	p.orders[order.Party][order.ID] = order
   224  	p.orderToParty[order.ID] = order.Party
   225  	switch {
   226  	case order.Trigger.IsPrice():
   227  		p.priced.Insert(order.ID, order.Trigger.Price().Clone(), order.Trigger.Direction)
   228  	case order.Trigger.IsTrailingPercentOffset():
   229  		p.trailing.Insert(order.ID, order.Trigger.TrailingPercentOffset(), order.Trigger.Direction)
   230  	}
   231  }
   232  
   233  func (p *Pool) Cancel(
   234  	partyID string,
   235  	orderID string, // if empty remove all
   236  ) ([]*types.StopOrder, error) {
   237  	orders, err := p.removeWithOCO(partyID, orderID, true)
   238  	if err == nil {
   239  		for _, v := range orders {
   240  			v.Status = types.StopOrderStatusCancelled
   241  		}
   242  	}
   243  
   244  	return orders, err
   245  }
   246  
   247  func (p *Pool) removeWithOCO(
   248  	partyID string,
   249  	orderID string,
   250  	withOCO bool, // not always necessary in case we are
   251  ) ([]*types.StopOrder, error) {
   252  	partyOrders, ok := p.orders[partyID]
   253  	if !ok {
   254  		// return an error only when trying to find a specific stop order
   255  		if len(orderID) > 0 {
   256  			return nil, ErrStopOrderNotFound
   257  		}
   258  
   259  		// this party have no stop orders, move on
   260  		return nil, nil
   261  	}
   262  
   263  	// remove a single one and maybe OCO
   264  	if len(orderID) > 0 {
   265  		order, ok := partyOrders[orderID]
   266  		if !ok {
   267  			return nil, ErrStopOrderNotFound
   268  		}
   269  
   270  		orders := []*types.StopOrder{order}
   271  		if withOCO && len(order.OCOLinkID) > 0 {
   272  			orders = append(orders, partyOrders[order.OCOLinkID])
   273  		}
   274  
   275  		p.remove(orders)
   276  
   277  		return orders, nil
   278  	}
   279  
   280  	orders := maps.Values(partyOrders)
   281  	sort.Slice(orders, func(i, j int) bool { return orders[i].ID < orders[j].ID })
   282  	p.remove(orders)
   283  
   284  	return orders, nil
   285  }
   286  
   287  func (p *Pool) remove(orders []*types.StopOrder) {
   288  	for _, order := range orders {
   289  		delete(p.orderToParty, order.ID)
   290  		delete(p.orders[order.Party], order.ID)
   291  
   292  		if len(p.orders[order.Party]) <= 0 {
   293  			// no need of this entry anymore
   294  			delete(p.orders, order.Party)
   295  		}
   296  
   297  		switch {
   298  		case order.Trigger.IsPrice():
   299  			p.priced.Remove(order.ID)
   300  		case order.Trigger.IsTrailingPercentOffset():
   301  			p.trailing.Remove(order.ID)
   302  		}
   303  	}
   304  }
   305  
   306  func (p *Pool) RemoveExpired(orderIDs []string) []*types.StopOrder {
   307  	ordersM := map[string]*types.StopOrder{}
   308  
   309  	// first find all orders and add them to the map
   310  	for _, id := range orderIDs {
   311  		order := p.orders[p.orderToParty[id]][id]
   312  		order.Status = types.StopOrderStatusExpired
   313  		ordersM[id] = order
   314  
   315  		// once an order is removed, we also remove it's OCO link
   316  		if len(order.OCOLinkID) > 0 {
   317  			// first check if it's not been removed already
   318  			if _, ok := p.orderToParty[order.OCOLinkID]; ok {
   319  				// is the OCO link already mapped
   320  				if _, ok := ordersM[order.OCOLinkID]; !ok {
   321  					ordersM[order.OCOLinkID] = p.orders[p.orderToParty[id]][order.OCOLinkID]
   322  					ordersM[order.OCOLinkID].Status = types.StopOrderStatusStopped
   323  				}
   324  			}
   325  		}
   326  	}
   327  
   328  	orders := maps.Values(ordersM)
   329  	sort.Slice(orders, func(i, j int) bool { return orders[i].ID < orders[j].ID })
   330  	p.remove(orders)
   331  
   332  	return orders
   333  }
   334  
   335  func (p *Pool) CountForParty(party string) uint64 {
   336  	orders, ok := p.orders[party]
   337  	if !ok {
   338  		return 0
   339  	}
   340  
   341  	return uint64(len(orders))
   342  }