code.vegaprotocol.io/vega@v0.79.0/core/execution/stoporders/priced_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  	"fmt"
    20  
    21  	"code.vegaprotocol.io/vega/core/types"
    22  	"code.vegaprotocol.io/vega/libs/num"
    23  	v1 "code.vegaprotocol.io/vega/protos/vega/snapshot/v1"
    24  
    25  	"github.com/google/btree"
    26  	"golang.org/x/exp/slices"
    27  )
    28  
    29  type ordersAtPrice struct {
    30  	price  *num.Uint
    31  	orders []string
    32  }
    33  
    34  func (o *ordersAtPrice) String() string {
    35  	return fmt.Sprintf("%s:%v", o.price.String(), o.orders)
    36  }
    37  
    38  func lessFuncOrdersAtPrice(a, b *ordersAtPrice) bool {
    39  	return a.price.LT(b.price)
    40  }
    41  
    42  type orderAtPriceStat struct {
    43  	price     *num.Uint
    44  	direction types.StopOrderTriggerDirection
    45  }
    46  
    47  type PricedStopOrders struct {
    48  	// mapping table for stop order ID
    49  	// help finding them back in the trees.
    50  	orders map[string]orderAtPriceStat
    51  
    52  	fallsBelow *btree.BTreeG[*ordersAtPrice]
    53  	risesAbove *btree.BTreeG[*ordersAtPrice]
    54  }
    55  
    56  func NewPricedStopOrders() *PricedStopOrders {
    57  	return &PricedStopOrders{
    58  		orders:     map[string]orderAtPriceStat{},
    59  		fallsBelow: btree.NewG(2, lessFuncOrdersAtPrice),
    60  		risesAbove: btree.NewG(2, lessFuncOrdersAtPrice),
    61  	}
    62  }
    63  
    64  func NewPricedStopOrdersFromProto(p *v1.PricedStopOrders) *PricedStopOrders {
    65  	pso := NewPricedStopOrders()
    66  
    67  	for _, v := range p.FallsBellow {
    68  		for _, oid := range v.Orders {
    69  			price, overflow := num.UintFromString(v.Price, 10)
    70  			if overflow {
    71  				panic(fmt.Sprintf("invalid uint from snapshot, would overflow: %s", v.Price))
    72  			}
    73  			pso.Insert(oid, price, types.StopOrderTriggerDirectionFallsBelow)
    74  		}
    75  	}
    76  
    77  	for _, v := range p.RisesAbove {
    78  		for _, oid := range v.Orders {
    79  			price, overflow := num.UintFromString(v.Price, 10)
    80  			if overflow {
    81  				panic(fmt.Sprintf("invalid uint from snapshot, would overflow: %s", v.Price))
    82  			}
    83  			pso.Insert(oid, price, types.StopOrderTriggerDirectionRisesAbove)
    84  		}
    85  	}
    86  
    87  	return pso
    88  }
    89  
    90  func (p *PricedStopOrders) ToProto() *v1.PricedStopOrders {
    91  	return &v1.PricedStopOrders{
    92  		FallsBellow: p.serialize(p.fallsBelow),
    93  		RisesAbove:  p.serialize(p.risesAbove),
    94  	}
    95  }
    96  
    97  func (p *PricedStopOrders) serialize(tree *btree.BTreeG[*ordersAtPrice]) []*v1.OrdersAtPrice {
    98  	out := []*v1.OrdersAtPrice{}
    99  
   100  	tree.Ascend(func(item *ordersAtPrice) bool {
   101  		out = append(out, &v1.OrdersAtPrice{
   102  			Price:  item.price.String(),
   103  			Orders: slices.Clone(item.orders),
   104  		})
   105  		return true
   106  	})
   107  
   108  	return out
   109  }
   110  
   111  func (p *PricedStopOrders) PriceUpdated(newPrice *num.Uint) []string {
   112  	// first remove if price fallsBelow
   113  	orderIDs := p.trigger(
   114  		p.fallsBelow,
   115  		p.fallsBelow.DescendGreaterThan,
   116  		newPrice.Clone().Sub(newPrice.Clone(), num.NewUint(1)),
   117  	)
   118  
   119  	// then if it rises above?
   120  	orderIDs = append(orderIDs,
   121  		p.trigger(
   122  			p.risesAbove,
   123  			p.risesAbove.AscendLessThan,
   124  			newPrice.Clone().Add(newPrice.Clone(), num.NewUint(1)),
   125  		)...,
   126  	)
   127  
   128  	// here we can cleanup the mapping table as well
   129  	for _, v := range orderIDs {
   130  		delete(p.orders, v)
   131  	}
   132  
   133  	return orderIDs
   134  }
   135  
   136  func (p *PricedStopOrders) trigger(
   137  	tree *btree.BTreeG[*ordersAtPrice],
   138  	findFn func(pivot *ordersAtPrice, iterator btree.ItemIteratorG[*ordersAtPrice]),
   139  	newPrice *num.Uint,
   140  ) []string {
   141  	orderIDs := []string{}
   142  	toDelete := []*num.Uint{}
   143  	findFn(&ordersAtPrice{price: newPrice}, func(item *ordersAtPrice) bool {
   144  		orderIDs = append(orderIDs, item.orders...)
   145  		toDelete = append(toDelete, item.price)
   146  		return true
   147  	})
   148  
   149  	// now we delete all the unused tree item
   150  	for _, p := range toDelete {
   151  		tree.Delete(&ordersAtPrice{price: p})
   152  	}
   153  
   154  	return orderIDs
   155  }
   156  
   157  func (p *PricedStopOrders) Insert(
   158  	id string, price *num.Uint, direction types.StopOrderTriggerDirection,
   159  ) {
   160  	p.orders[id] = orderAtPriceStat{price.Clone(), direction}
   161  
   162  	switch direction {
   163  	case types.StopOrderTriggerDirectionFallsBelow:
   164  		p.insertOrUpdate(p.fallsBelow, id, price.Clone())
   165  	case types.StopOrderTriggerDirectionRisesAbove:
   166  		p.insertOrUpdate(p.risesAbove, id, price.Clone())
   167  	}
   168  }
   169  
   170  func (p *PricedStopOrders) insertOrUpdate(
   171  	tree *btree.BTreeG[*ordersAtPrice], id string, price *num.Uint,
   172  ) {
   173  	oap, ok := tree.Get(&ordersAtPrice{price: price})
   174  	if !ok {
   175  		oap = &ordersAtPrice{price: price}
   176  	}
   177  
   178  	// add to the list
   179  	oap.orders = append(oap.orders, id)
   180  
   181  	// finally insert or whatever
   182  	tree.ReplaceOrInsert(oap)
   183  }
   184  
   185  func (p *PricedStopOrders) Remove(id string) error {
   186  	oaps, ok := p.orders[id]
   187  	if !ok {
   188  		return ErrOrderNotFound
   189  	}
   190  
   191  	delete(p.orders, id)
   192  
   193  	var err error
   194  	switch oaps.direction {
   195  	case types.StopOrderTriggerDirectionFallsBelow:
   196  		err = p.removeAndMaybeDelete(p.fallsBelow, id, oaps.price)
   197  	case types.StopOrderTriggerDirectionRisesAbove:
   198  		err = p.removeAndMaybeDelete(p.risesAbove, id, oaps.price)
   199  	}
   200  
   201  	return err
   202  }
   203  
   204  func (p *PricedStopOrders) removeAndMaybeDelete(
   205  	tree *btree.BTreeG[*ordersAtPrice], id string, price *num.Uint,
   206  ) error {
   207  	// just declare it first, we may reuse it by the end
   208  	item := &ordersAtPrice{price: price}
   209  
   210  	oap, ok := tree.Get(item)
   211  	if !ok {
   212  		return ErrPriceNotFound
   213  	}
   214  
   215  	before := len(oap.orders)
   216  
   217  	for n, v := range oap.orders {
   218  		// this is our ID
   219  		if v == id {
   220  			oap.orders = slices.Delete(oap.orders, n, n+1)
   221  			break
   222  		}
   223  	}
   224  
   225  	// didn't found the order, we can just panic it should never happen
   226  	if before == len(oap.orders) {
   227  		panic("order not in tree but in mapping table")
   228  	}
   229  
   230  	// now if the len is 0, we probably don't need that
   231  	// price level anymore
   232  	if len(oap.orders) <= 0 {
   233  		tree.Delete(item)
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func (p *PricedStopOrders) DumpRisesAbove() string {
   240  	return dumpTree(p.risesAbove)
   241  }
   242  
   243  func (p *PricedStopOrders) DumpFallsBelow() string {
   244  	return dumpTree(p.fallsBelow)
   245  }