code.vegaprotocol.io/vega@v0.79.0/core/execution/stoporders/trailing_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 ordersAtOffset struct {
    30  	offset num.Decimal
    31  	orders []string
    32  }
    33  
    34  func (o *ordersAtOffset) String() string {
    35  	return fmt.Sprintf("%s:%v", o.offset.String(), o.orders)
    36  }
    37  
    38  type orderAtOffsetStat struct {
    39  	offset    num.Decimal
    40  	direction types.StopOrderTriggerDirection
    41  }
    42  
    43  func lessFuncOrdersAtOffset(a, b *ordersAtOffset) bool {
    44  	return a.offset.LessThan(b.offset)
    45  }
    46  
    47  type offsetsAtPrice struct {
    48  	price   *num.Uint
    49  	offsets *btree.BTreeG[*ordersAtOffset]
    50  }
    51  
    52  func (o *offsetsAtPrice) String() string {
    53  	return fmt.Sprintf("%s:%s", o.price.String(), dumpTree(o.offsets))
    54  }
    55  
    56  func lessFuncOffsetsAtPrice(a, b *offsetsAtPrice) bool {
    57  	return a.price.LT(b.price)
    58  }
    59  
    60  type TrailingStopOrders struct {
    61  	lastSeenPrice *num.Uint
    62  	orders        map[string]orderAtOffsetStat
    63  	risesAbove    *btree.BTreeG[*offsetsAtPrice]
    64  	fallsBelow    *btree.BTreeG[*offsetsAtPrice]
    65  }
    66  
    67  func NewTrailingStopOrders() *TrailingStopOrders {
    68  	return &TrailingStopOrders{
    69  		orders:     map[string]orderAtOffsetStat{},
    70  		risesAbove: btree.NewG(2, lessFuncOffsetsAtPrice),
    71  		fallsBelow: btree.NewG(2, lessFuncOffsetsAtPrice),
    72  	}
    73  }
    74  
    75  func NewTrailingStopOrdersFromProto(p *v1.TrailingStopOrders) *TrailingStopOrders {
    76  	tso := NewTrailingStopOrders()
    77  
    78  	if len(p.LastSeenPrice) > 0 {
    79  		var overflow bool
    80  		tso.lastSeenPrice, overflow = num.UintFromString(p.LastSeenPrice, 10)
    81  		if overflow {
    82  			panic("lastSeenPrice should always be valid")
    83  		}
    84  	}
    85  
    86  	for _, v := range p.FallsBellow {
    87  		price, overflow := num.UintFromString(v.Price, 10)
    88  		if overflow {
    89  			panic(fmt.Sprintf("invalid uint from snapshot, would overflow: %s", v.Price))
    90  		}
    91  		for _, offset := range v.Offsets {
    92  			off, err := num.DecimalFromString(offset.Offset)
    93  			if err != nil {
    94  				panic(fmt.Sprintf("invalid decimal from snapshot: %s", offset.Offset))
    95  			}
    96  			for _, oid := range offset.Orders {
    97  				tso.insertAtPrice(
    98  					oid, off, price, types.StopOrderTriggerDirectionFallsBelow)
    99  			}
   100  		}
   101  	}
   102  
   103  	for _, v := range p.RisesAbove {
   104  		price, overflow := num.UintFromString(v.Price, 10)
   105  		if overflow {
   106  			panic(fmt.Sprintf("invalid uint from snapshot, would overflow: %s", v.Price))
   107  		}
   108  		for _, offset := range v.Offsets {
   109  			off, err := num.DecimalFromString(offset.Offset)
   110  			if err != nil {
   111  				panic(fmt.Sprintf("invalid decimal from snapshot: %s", offset.Offset))
   112  			}
   113  			for _, oid := range offset.Orders {
   114  				tso.insertAtPrice(
   115  					oid, off, price, types.StopOrderTriggerDirectionRisesAbove)
   116  			}
   117  		}
   118  	}
   119  
   120  	return tso
   121  }
   122  
   123  func (p *TrailingStopOrders) ToProto() *v1.TrailingStopOrders {
   124  	var lastSeenPrice string
   125  	if p.lastSeenPrice != nil {
   126  		lastSeenPrice = p.lastSeenPrice.String()
   127  	}
   128  
   129  	return &v1.TrailingStopOrders{
   130  		LastSeenPrice: lastSeenPrice,
   131  		FallsBellow:   p.serialize(p.fallsBelow),
   132  		RisesAbove:    p.serialize(p.risesAbove),
   133  	}
   134  }
   135  
   136  func (p *TrailingStopOrders) serialize(
   137  	tree *btree.BTreeG[*offsetsAtPrice],
   138  ) []*v1.OffsetsAtPrice {
   139  	out := []*v1.OffsetsAtPrice{}
   140  
   141  	tree.Ascend(func(item *offsetsAtPrice) bool {
   142  		offsets := []*v1.OrdersAtOffset{}
   143  
   144  		item.offsets.Ascend(func(item *ordersAtOffset) bool {
   145  			offsets = append(offsets, &v1.OrdersAtOffset{
   146  				Offset: item.offset.String(),
   147  				Orders: slices.Clone(item.orders),
   148  			})
   149  			return true
   150  		})
   151  
   152  		out = append(out, &v1.OffsetsAtPrice{
   153  			Price:   item.price.String(),
   154  			Offsets: offsets,
   155  		})
   156  		return true
   157  	})
   158  
   159  	return out
   160  }
   161  
   162  func (p *TrailingStopOrders) PriceUpdated(newPrice *num.Uint) []string {
   163  	// short circuit for very first update,
   164  	// not much to do here, just set the newPrice
   165  	// and move on
   166  	if p.lastSeenPrice == nil {
   167  		p.lastSeenPrice = newPrice.Clone()
   168  		return nil
   169  	}
   170  
   171  	var out []string
   172  	// price increased, move trailing prices buckets
   173  	// for fallsBelow and executed triggers for risesAbove
   174  	if p.lastSeenPrice.LT(newPrice) {
   175  		p.adjustBuckets(p.fallsBelow, p.fallsBelow.AscendLessThan, newPrice)
   176  		out = p.trigger(
   177  			p.risesAbove,
   178  			p.risesAbove.Ascend,
   179  			func(a *num.Uint, b *num.Uint) bool { return a.GT(b) },
   180  			func(a num.Decimal, b num.Decimal) bool { return a.GreaterThan(b) },
   181  			func(a num.Decimal, offset num.Decimal) num.Decimal { return a.Add(a.Mul(offset)) },
   182  			newPrice,
   183  		)
   184  	} else if p.lastSeenPrice.GT(newPrice) {
   185  		p.adjustBuckets(p.risesAbove, p.risesAbove.DescendGreaterThan, newPrice)
   186  		out = p.trigger(
   187  			p.fallsBelow,
   188  			p.fallsBelow.Descend,
   189  			func(a *num.Uint, b *num.Uint) bool { return a.LT(b) },
   190  			func(a num.Decimal, b num.Decimal) bool { return a.LessThan(b) },
   191  			func(a num.Decimal, offset num.Decimal) num.Decimal { return a.Sub(a.Mul(offset)) },
   192  			newPrice,
   193  		)
   194  	} else {
   195  		// nothing happened
   196  		return nil
   197  	}
   198  
   199  	// remove orders from the mapping
   200  	for _, v := range out {
   201  		delete(p.orders, v)
   202  	}
   203  
   204  	p.lastSeenPrice = newPrice.Clone()
   205  
   206  	return out
   207  }
   208  
   209  func (p *TrailingStopOrders) adjustBuckets(
   210  	tree *btree.BTreeG[*offsetsAtPrice],
   211  	findFn func(*offsetsAtPrice, btree.ItemIteratorG[*offsetsAtPrice]),
   212  	newPrice *num.Uint,
   213  ) {
   214  	// first we get all prices to adjust
   215  	item := &offsetsAtPrice{price: newPrice}
   216  	pricesToAdjust := []*num.Uint{}
   217  	findFn(item, func(oap *offsetsAtPrice) bool {
   218  		pricesToAdjust = append(pricesToAdjust, oap.price.Clone())
   219  		return true
   220  	})
   221  
   222  	// now for each of them, we pull the orders, and insert them in the new price.
   223  	for _, price := range pricesToAdjust {
   224  		current := &offsetsAtPrice{price: price}
   225  		// no error to check, we just iterated over,
   226  		// impossible we would not find it.
   227  		oap, _ := tree.Get(current)
   228  
   229  		// now for each orders of every leaf we can add at the new price
   230  		oap.offsets.Ascend(func(oao *ordersAtOffset) bool {
   231  			for _, order := range oao.orders {
   232  				// update the mapping
   233  				prevOrderAtOffsetStat := p.orders[order]
   234  				p.orders[order] = orderAtOffsetStat{oao.offset, prevOrderAtOffsetStat.direction}
   235  				// insert
   236  				p.insertOrUpdateOffsetAtPrice(
   237  					tree, order, newPrice, oao.offset,
   238  				)
   239  			}
   240  			return true
   241  		})
   242  
   243  		// now we delete this one, it's not needed anymore
   244  		_, _ = tree.Delete(current)
   245  	}
   246  }
   247  
   248  func (p *TrailingStopOrders) trigger(
   249  	tree *btree.BTreeG[*offsetsAtPrice],
   250  	iterateFn func(btree.ItemIteratorG[*offsetsAtPrice]),
   251  	cmpPriceFn func(*num.Uint, *num.Uint) bool,
   252  	cmpOffsetFn func(num.Decimal, num.Decimal) bool,
   253  	applyOffsetFn func(num.Decimal, num.Decimal) num.Decimal,
   254  	price *num.Uint,
   255  ) (orders []string) {
   256  	priceDec := price.ToDecimal()
   257  	toRemovePrices := []*num.Uint{}
   258  	iterateFn(func(item *offsetsAtPrice) bool {
   259  		if cmpPriceFn(item.price, price) {
   260  			// continue but nothing to do here
   261  			return true
   262  		}
   263  
   264  		leafPriceDec := item.price.ToDecimal()
   265  
   266  		toRemoveOffsets := []num.Decimal{}
   267  		// now in here, we iterate all the
   268  		item.offsets.Ascend(func(item *ordersAtOffset) bool {
   269  			offsetedPrice := applyOffsetFn(leafPriceDec, item.offset)
   270  			if cmpOffsetFn(offsetedPrice, priceDec) {
   271  				// we have still margin, no need to process the others
   272  				return false
   273  			}
   274  
   275  			toRemoveOffsets = append(toRemoveOffsets, item.offset)
   276  
   277  			return true
   278  		})
   279  
   280  		// now remove all
   281  		for _, o := range toRemoveOffsets {
   282  			oao, _ := item.offsets.Delete(&ordersAtOffset{offset: o})
   283  			// add to the list of orders
   284  			orders = append(orders, oao.orders...)
   285  		}
   286  
   287  		// now we check if the offsets at the price have been depleted,
   288  		// and add them to the list to eventually remove
   289  		if item.offsets.Len() <= 0 {
   290  			toRemovePrices = append(toRemovePrices, item.price.Clone())
   291  		}
   292  
   293  		return true
   294  	})
   295  
   296  	// now we remove all depleted prices
   297  	for _, p := range toRemovePrices {
   298  		_, _ = tree.Delete(&offsetsAtPrice{price: p})
   299  	}
   300  
   301  	return orders
   302  }
   303  
   304  func (p *TrailingStopOrders) Insert(
   305  	id string, offset num.Decimal, direction types.StopOrderTriggerDirection,
   306  ) {
   307  	p.orders[id] = orderAtOffsetStat{offset, direction}
   308  
   309  	switch direction {
   310  	case types.StopOrderTriggerDirectionFallsBelow:
   311  		p.insertOrUpdateOffsetAtPrice(p.fallsBelow, id, p.lastSeenPrice.Clone(), offset)
   312  	case types.StopOrderTriggerDirectionRisesAbove:
   313  		p.insertOrUpdateOffsetAtPrice(p.risesAbove, id, p.lastSeenPrice.Clone(), offset)
   314  	}
   315  }
   316  
   317  func (p *TrailingStopOrders) insertAtPrice(
   318  	id string, offset num.Decimal, price *num.Uint, direction types.StopOrderTriggerDirection,
   319  ) {
   320  	p.orders[id] = orderAtOffsetStat{offset, direction}
   321  
   322  	switch direction {
   323  	case types.StopOrderTriggerDirectionFallsBelow:
   324  		p.insertOrUpdateOffsetAtPrice(p.fallsBelow, id, price.Clone(), offset)
   325  	case types.StopOrderTriggerDirectionRisesAbove:
   326  		p.insertOrUpdateOffsetAtPrice(p.risesAbove, id, price.Clone(), offset)
   327  	}
   328  }
   329  
   330  func (p *TrailingStopOrders) insertOrUpdateOffsetAtPrice(
   331  	tree *btree.BTreeG[*offsetsAtPrice],
   332  	id string,
   333  	price *num.Uint,
   334  	offset num.Decimal,
   335  ) {
   336  	oap, ok := tree.Get(&offsetsAtPrice{price: price})
   337  	if !ok {
   338  		oap = &offsetsAtPrice{
   339  			price:   price,
   340  			offsets: btree.NewG(2, lessFuncOrdersAtOffset),
   341  		}
   342  	}
   343  
   344  	// add to the list
   345  	p.insertOrUpdateOrderAtOffset(oap.offsets, id, offset)
   346  
   347  	// finally insert or whatever
   348  	tree.ReplaceOrInsert(oap)
   349  }
   350  
   351  func (p *TrailingStopOrders) insertOrUpdateOrderAtOffset(
   352  	tree *btree.BTreeG[*ordersAtOffset],
   353  	id string,
   354  	offset num.Decimal,
   355  ) {
   356  	oap, ok := tree.Get(&ordersAtOffset{offset: offset})
   357  	if !ok {
   358  		oap = &ordersAtOffset{offset: offset}
   359  	}
   360  
   361  	// add to the list
   362  	oap.orders = append(oap.orders, id)
   363  
   364  	// finally insert or whatever
   365  	tree.ReplaceOrInsert(oap)
   366  }
   367  
   368  func (p *TrailingStopOrders) Remove(id string) error {
   369  	o, ok := p.orders[id]
   370  	if !ok {
   371  		return ErrOrderNotFound
   372  	}
   373  
   374  	// we can remove from the map now
   375  	delete(p.orders, id)
   376  
   377  	switch o.direction {
   378  	case types.StopOrderTriggerDirectionFallsBelow:
   379  		p.remove(p.fallsBelow, id, o.offset)
   380  	case types.StopOrderTriggerDirectionRisesAbove:
   381  		p.remove(p.risesAbove, id, o.offset)
   382  	}
   383  
   384  	return nil
   385  }
   386  
   387  func (p *TrailingStopOrders) remove(
   388  	tree *btree.BTreeG[*offsetsAtPrice],
   389  	id string,
   390  	offset num.Decimal,
   391  ) {
   392  	var deletePrice *num.Uint
   393  	tree.Ascend(func(item *offsetsAtPrice) bool {
   394  		innerItem := &ordersAtOffset{offset: offset}
   395  		// does that offset exists at that price?
   396  		oao, ok := item.offsets.Get(innerItem)
   397  		if !ok {
   398  			return true // nope, keep moving
   399  		}
   400  
   401  		continu := true
   402  		for n, v := range oao.orders {
   403  			// we found our order!
   404  			if v == id {
   405  				oao.orders = slices.Delete(oao.orders, n, n+1)
   406  				continu = false
   407  				break
   408  			}
   409  		}
   410  
   411  		if len(oao.orders) <= 0 {
   412  			item.offsets.Delete(innerItem)
   413  		}
   414  
   415  		if item.offsets.Len() <= 0 {
   416  			deletePrice = item.price.Clone()
   417  		}
   418  
   419  		return continu
   420  	})
   421  
   422  	if deletePrice != nil {
   423  		tree.Delete(&offsetsAtPrice{price: deletePrice})
   424  	}
   425  }
   426  
   427  func (p *TrailingStopOrders) DumpRisesAbove() string {
   428  	return dumpTree(p.risesAbove)
   429  }
   430  
   431  func (p *TrailingStopOrders) DumpFallsBelow() string {
   432  	return dumpTree(p.fallsBelow)
   433  }