code.vegaprotocol.io/vega@v0.79.0/core/products/perpetual.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 products
    17  
    18  import (
    19  	"context"
    20  	"time"
    21  
    22  	"code.vegaprotocol.io/vega/core/datasource"
    23  	dscommon "code.vegaprotocol.io/vega/core/datasource/common"
    24  	"code.vegaprotocol.io/vega/core/datasource/spec"
    25  	"code.vegaprotocol.io/vega/core/events"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	"code.vegaprotocol.io/vega/libs/ptr"
    29  	"code.vegaprotocol.io/vega/logging"
    30  	datapb "code.vegaprotocol.io/vega/protos/vega/data/v1"
    31  	eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1"
    32  
    33  	"github.com/pkg/errors"
    34  	"golang.org/x/exp/slices"
    35  )
    36  
    37  var (
    38  	year = num.DecimalFromInt64((24 * 365 * time.Hour).Nanoseconds())
    39  
    40  	ErrDataPointAlreadyExistsAtTime = errors.New("data-point already exists at timestamp")
    41  	ErrDataPointIsTooOld            = errors.New("data-point is too old")
    42  )
    43  
    44  type dataPointSource = eventspb.FundingPeriodDataPoint_Source
    45  
    46  const (
    47  	dataPointSourceExternal dataPointSource = eventspb.FundingPeriodDataPoint_SOURCE_EXTERNAL
    48  	dataPointSourceInternal dataPointSource = eventspb.FundingPeriodDataPoint_SOURCE_INTERNAL
    49  )
    50  
    51  type fundingData struct {
    52  	fundingPayment *num.Int
    53  	fundingRate    num.Decimal
    54  	internalTWAP   *num.Uint
    55  	externalTWAP   *num.Uint
    56  }
    57  
    58  type auctionIntervals struct {
    59  	auctions     []int64 // complete auction intervals as pairs of enter/leave time values, always of even length
    60  	total        int64   // the sum of all the complete auction intervals giving the total time spent in auction
    61  	auctionStart int64   // if we are currently in an auction, this is the time we entered it
    62  }
    63  
    64  // restart resets the auction interval tracking, remembering the current "in-auction" state by carrying over `auctionStart`.
    65  func (a *auctionIntervals) restart() {
    66  	a.total = 0
    67  	a.auctions = []int64{}
    68  }
    69  
    70  // update adds a new aution enter/leave boundary to the auction intervals being tracked.
    71  func (a *auctionIntervals) update(t int64, enter bool) {
    72  	if (a.auctionStart != 0) == enter {
    73  		panic("flags out of sync - double entry or double leave auction detected")
    74  	}
    75  
    76  	if enter {
    77  		if len(a.auctions) == 0 {
    78  			a.auctionStart = t
    79  			return
    80  		}
    81  
    82  		st, nd := a.auctions[len(a.auctions)-2], a.auctions[len(a.auctions)-1]
    83  		if t != nd {
    84  			a.auctionStart = t
    85  			return
    86  		}
    87  
    88  		a.auctions = slices.Delete(a.auctions, len(a.auctions)-2, len(a.auctions))
    89  		a.auctionStart = st
    90  
    91  		// now we've "re-opened" this auction period, remove its contribution from
    92  		// the cached total of completed auction periods.
    93  		a.total -= (nd - st)
    94  		return
    95  	}
    96  
    97  	if t == a.auctionStart {
    98  		// left an auction as soon as we entered it, no need to log it
    99  		a.auctionStart = 0
   100  		return
   101  	}
   102  
   103  	// we've left an auction period, add the new st/nd to the completed auction periods
   104  	a.auctions = append(a.auctions, a.auctionStart, t)
   105  
   106  	// update our running total
   107  	a.total += t - a.auctionStart
   108  	a.auctionStart = 0
   109  }
   110  
   111  // inAuction returns whether the given t exists in an auction period.
   112  func (a *auctionIntervals) inAuction(t int64) bool {
   113  	if a.auctionStart != 0 && t >= a.auctionStart {
   114  		return true
   115  	}
   116  
   117  	for i := len(a.auctions) - 2; i >= 0; i = i - 2 {
   118  		if a.auctions[i] <= t && a.auctions[i+1] < t {
   119  			return true
   120  		}
   121  	}
   122  	return false
   123  }
   124  
   125  // timeSpent returns how long the time interval [st, nd] was spent in auction in the current funding period.
   126  func (a *auctionIntervals) timeSpent(st, nd int64) int64 {
   127  	if nd < st {
   128  		panic("cannot process backwards interval")
   129  	}
   130  
   131  	var sum int64
   132  	if a.auctionStart != 0 && a.auctionStart < nd {
   133  		if st > a.auctionStart {
   134  			return nd - st
   135  		}
   136  		// we want to include the in-progress auction period, so add on how much into it we are
   137  		sum = nd - a.auctionStart
   138  	}
   139  
   140  	// if [st, nd] contains all the auction intervals we can just return the running total
   141  	if len(a.auctions) != 0 && st <= a.auctions[0] && a.auctions[len(a.auctions)-1] <= nd {
   142  		return a.total + sum
   143  	}
   144  
   145  	// iterare over the completed auction periods in pairs and add regions of the auction intervals
   146  	// that overlap with [st, nd]
   147  	for i := len(a.auctions) - 2; i >= 0; i = i - 2 {
   148  		// [st, nd] is entirely after this auction period, we can stop now
   149  		if a.auctions[i+1] < st {
   150  			break
   151  		}
   152  		// calculate
   153  		sum += num.MaxV(0, num.MinV(nd, a.auctions[i+1])-num.MaxV(st, a.auctions[i]))
   154  	}
   155  
   156  	return sum
   157  }
   158  
   159  type cachedTWAP struct {
   160  	log *logging.Logger
   161  
   162  	periodStart int64        // the start of the funding period
   163  	start       int64        // start of the TWAP period, which will be > periodStart if the first data-point comes after it
   164  	end         int64        // time of the last calculated sub-product that was >= the last added data-point
   165  	sumProduct  *num.Uint    // the sum-product of all the intervals between the data-points from `start` -> `end`
   166  	points      []*dataPoint // the data-points used to calculate the twap
   167  
   168  	auctions *auctionIntervals
   169  }
   170  
   171  func NewCachedTWAP(log *logging.Logger, t int64, auctions *auctionIntervals) *cachedTWAP {
   172  	return &cachedTWAP{
   173  		log:         log,
   174  		start:       t,
   175  		periodStart: t,
   176  		end:         t,
   177  		sumProduct:  num.UintZero(),
   178  		auctions:    auctions,
   179  	}
   180  }
   181  
   182  // setPeriod assigns the start and end of the calculated TWAP periods based on the time of incoming data-points.
   183  // If the first data-point is before the true-period start it is still added but we ignore the contribution between
   184  // data-point.t -> periodStart. So here we snap all values, then sanity check we've not set anything backwards.
   185  func (c *cachedTWAP) setPeriod(start, end int64) {
   186  	c.start = num.MaxV(c.periodStart, start)
   187  	c.end = num.MaxV(c.periodStart, end)
   188  
   189  	if c.end < c.start {
   190  		c.log.Panic("twap interval has become backwards")
   191  	}
   192  }
   193  
   194  // unwind returns the sum-product at the given time `t` where `t` is a time before the last
   195  // data-point. We have to subtract each interval until we get to the first point where p.t < t,
   196  // the index of `p` is also returned.
   197  func (c *cachedTWAP) unwind(t int64) (*num.Uint, int) {
   198  	if t < c.start {
   199  		return num.UintZero(), 0
   200  	}
   201  
   202  	sumProduct := c.sumProduct.Clone()
   203  	for i := len(c.points) - 1; i >= 0; i-- {
   204  		point := c.points[i]
   205  		prev := c.points[i-1]
   206  
   207  		// now we need to remove the contribution from this interval
   208  		delta := point.t - num.MaxV(prev.t, c.start)
   209  
   210  		// minus time in auction
   211  		delta -= c.auctions.timeSpent(num.MaxV(prev.t, c.start), point.t)
   212  		sub := num.UintZero().Mul(prev.price, num.NewUint(uint64(delta)))
   213  
   214  		// before we subtract, lets sanity check some things
   215  		if delta < 0 {
   216  			c.log.Panic("twap data-points are out of order creating retrograde segment")
   217  		}
   218  		if sumProduct.LT(sub) {
   219  			c.log.Panic("twap unwind is subtracting too much")
   220  		}
   221  
   222  		sumProduct.Sub(sumProduct, sub)
   223  
   224  		if prev.t <= t {
   225  			return sumProduct, i - 1
   226  		}
   227  	}
   228  
   229  	c.log.Panic("have unwound to before initial data-point -- we shouldn't be here")
   230  	return nil, 0
   231  }
   232  
   233  // getTWAP will return the TWAP if we have enough data, else nil.
   234  func (c *cachedTWAP) getTWAP(t int64) *string {
   235  	if !c.dataAvailable(t) {
   236  		return nil
   237  	}
   238  	return ptr.From(c.calculate(t).String())
   239  }
   240  
   241  // dataAvailable returns true if there are any datapoints available for a calculation at time t and false otherwise.
   242  func (c *cachedTWAP) dataAvailable(t int64) bool {
   243  	return t >= c.start && len(c.points) > 0
   244  }
   245  
   246  // calculate returns the TWAP at time `t` given the existing set of data-points. `t` can be
   247  // any value and we will extend off the last-data-point if necessary, and also unwind intervals
   248  // if the TWAP at a more historic time is required.
   249  func (c *cachedTWAP) calculate(t int64) *num.Uint {
   250  	if !c.dataAvailable(t) {
   251  		return num.UintZero()
   252  	}
   253  	if t == c.start {
   254  		if c.auctions.inAuction(t) {
   255  			return num.UintZero()
   256  		}
   257  		return c.points[0].price.Clone()
   258  	}
   259  
   260  	if t == c.end {
   261  		// already have the sum product here, just twap-it
   262  		period := c.end - c.start
   263  		period -= c.auctions.timeSpent(c.start, c.end)
   264  		return num.UintZero().Div(c.sumProduct, num.NewUint(uint64(period)))
   265  	}
   266  
   267  	// if the time we want the twap from is before the last data-point we need to unwind the intervals
   268  	point := c.points[len(c.points)-1]
   269  	if t < point.t {
   270  		sumProduct, idx := c.unwind(t)
   271  		p := c.points[idx]
   272  
   273  		// if the point we're winding forward from was a carry over, its time will be before the start-period.
   274  		from := num.MaxV(p.t, c.start)
   275  		delta := t - from
   276  		delta -= c.auctions.timeSpent(from, t)
   277  
   278  		period := t - c.start
   279  		period -= c.auctions.timeSpent(c.start, t)
   280  
   281  		sumProduct.Add(sumProduct, num.UintZero().Mul(p.price, num.NewUint(uint64(delta))))
   282  		return num.UintZero().Div(sumProduct, num.NewUint(uint64(period)))
   283  	}
   284  
   285  	// the twap we want is after the final data-point so we can just extend the calculation (or shortern if we've already extended)
   286  	delta := t - c.end
   287  	period := t - c.start
   288  	period -= c.auctions.timeSpent(c.start, t)
   289  
   290  	sumProduct := c.sumProduct.Clone()
   291  	newPeriod := num.NewUint(uint64(period))
   292  	lastPrice := point.price.Clone()
   293  
   294  	// add or subtract from the sum-product based on if we are extending/shortening the interval
   295  	switch {
   296  	case delta < 0:
   297  		delta += c.auctions.timeSpent(t, c.end)
   298  		sumProduct.Sub(sumProduct, lastPrice.Mul(lastPrice, num.NewUint(uint64(-delta))))
   299  	case delta > 0:
   300  		delta -= c.auctions.timeSpent(c.end, t)
   301  		sumProduct.Add(sumProduct, lastPrice.Mul(lastPrice, num.NewUint(uint64(delta))))
   302  	}
   303  	// store these as the last calculated as its likely to be asked again
   304  	c.setPeriod(c.start, t)
   305  	c.sumProduct = sumProduct
   306  
   307  	// now divide by the period to return the TWAP
   308  	return num.UintZero().Div(sumProduct, newPeriod)
   309  }
   310  
   311  // insertPoint adds the given point (which is known to have arrived out of order) to
   312  // the slice of points. The running sum-product is wound back to where we need to add
   313  // the new point and then recalculated forwards to the point with the lastest timestamp.
   314  func (c *cachedTWAP) insertPoint(point *dataPoint) (*num.Uint, error) {
   315  	// unwind the intervals and set the end and sum-product to the unwound values
   316  	sumProduct, idx := c.unwind(point.t)
   317  	if c.points[idx].t == point.t {
   318  		return nil, ErrDataPointAlreadyExistsAtTime
   319  	}
   320  
   321  	c.setPeriod(c.start, c.points[idx].t)
   322  	c.sumProduct = sumProduct.Clone()
   323  
   324  	// grab the data-points after the one we are inserting so that we can add them back in again
   325  	subsequent := slices.Clone(c.points[idx+1:])
   326  	c.points = c.points[:idx+1]
   327  
   328  	// add the new point and calculate the TWAP
   329  	twap := c.calculate(point.t)
   330  	c.points = append(c.points, point)
   331  
   332  	// now add the points that we unwound so that the running sum-product is amended
   333  	// now that we've inserted the new point
   334  	for _, p := range subsequent {
   335  		c.calculate(p.t)
   336  		c.points = append(c.points, p)
   337  	}
   338  
   339  	return twap, nil
   340  }
   341  
   342  // prependPoint handles the case where the given point is either before the first point, or before the start of the period.
   343  func (c *cachedTWAP) prependPoint(point *dataPoint) (*num.Uint, error) {
   344  	first := c.points[0]
   345  
   346  	if point.t == first.t {
   347  		return nil, ErrDataPointAlreadyExistsAtTime
   348  	}
   349  
   350  	// our first point is on or before the start of the period, and the new point is before both, its too old
   351  	if first.t <= c.periodStart && point.t < first.t {
   352  		return nil, ErrDataPointIsTooOld
   353  	}
   354  
   355  	points := c.points[:]
   356  	if first.t < c.periodStart && first.t < point.t {
   357  		// this is the case where we have first-point < new-point < period start and we only want to keep
   358  		// one data point that is before the start of the period, so we throw away first-point
   359  		points = c.points[1:]
   360  	}
   361  
   362  	c.points = []*dataPoint{point}
   363  	c.sumProduct = num.UintZero()
   364  	c.setPeriod(point.t, point.t)
   365  	for _, p := range points {
   366  		c.calculate(p.t)
   367  		c.points = append(c.points, p)
   368  	}
   369  	return point.price.Clone(), nil
   370  }
   371  
   372  // addPoint takes the given point and works out where it fits against what we already have, updates the
   373  // running sum-product and returns the TWAP at point.t.
   374  func (c *cachedTWAP) addPoint(point *dataPoint) (*num.Uint, error) {
   375  	if len(c.points) == 0 {
   376  		c.points = []*dataPoint{point}
   377  		c.setPeriod(point.t, point.t)
   378  		c.sumProduct = num.UintZero()
   379  		return point.price.Clone(), nil
   380  	}
   381  
   382  	if point.t <= c.points[0].t || point.t <= c.periodStart {
   383  		return c.prependPoint(point)
   384  	}
   385  
   386  	// new point is after the last point, just calculate the TWAP at point.t and append
   387  	// the new point to the slice
   388  	lastPoint := c.points[len(c.points)-1]
   389  	if point.t > lastPoint.t {
   390  		twap := c.calculate(point.t)
   391  		c.points = append(c.points, point)
   392  		return twap, nil
   393  	}
   394  
   395  	if point.t == lastPoint.t {
   396  		// already have a point for this time
   397  		return nil, ErrDataPointAlreadyExistsAtTime
   398  	}
   399  
   400  	// we need to undo any extension past the last point we've done, we can do this by recalculating to the last point
   401  	// which will remove the extension
   402  	c.calculate(num.MaxV(c.start, lastPoint.t))
   403  
   404  	// new point is before the last point, we need to unwind all the intervals and insert it into the correct place
   405  	return c.insertPoint(point)
   406  }
   407  
   408  // A data-point that will be used to calculate periodic settlement in a perps market.
   409  type dataPoint struct {
   410  	// the asset price
   411  	price *num.Uint
   412  	// the timestamp of this data point
   413  	t int64
   414  }
   415  
   416  // Perpetual represents a Perpetual as describe by the market framework.
   417  type Perpetual struct {
   418  	p   *types.Perps
   419  	log *logging.Logger
   420  
   421  	// oracle
   422  	settlementDataListener func(context.Context, *num.Numeric) // funding payment
   423  	dataPointListener      func(context.Context, *num.Uint)    // pass through data-points
   424  
   425  	broker      Broker
   426  	oracle      scheduledOracle
   427  	timeService TimeService
   428  
   429  	// id should be the same as the market id
   430  	id string
   431  	// enumeration of the settlement period so that we can track which points landed in each interval
   432  	seq uint64
   433  	// the time that this period interval started (in nanoseconds)
   434  	startedAt int64
   435  	// asset decimal places
   436  	assetDP    uint32
   437  	terminated bool
   438  
   439  	// twap calculators
   440  	internalTWAP *cachedTWAP
   441  	externalTWAP *cachedTWAP
   442  	auctions     *auctionIntervals
   443  }
   444  
   445  func (p Perpetual) GetCurrentPeriod() uint64 {
   446  	return p.seq
   447  }
   448  
   449  func (p *Perpetual) Update(ctx context.Context, pp interface{}, oe OracleEngine) error {
   450  	iPerp, ok := pp.(*types.InstrumentPerps)
   451  	if !ok {
   452  		p.log.Panic("attempting to update a perpetual into something else")
   453  	}
   454  
   455  	// unsubsribe all old oracles
   456  	p.oracle.unsubAll(ctx)
   457  
   458  	// grab all the new margin-factor and whatnot.
   459  	p.p = iPerp.Perps
   460  
   461  	// make sure we have all we need
   462  	if p.p.DataSourceSpecForSettlementData == nil || p.p.DataSourceSpecForSettlementSchedule == nil || p.p.DataSourceSpecBinding == nil {
   463  		return ErrDataSourceSpecAndBindingAreRequired
   464  	}
   465  	oracle, err := newPerpOracle(p.p)
   466  	if err != nil {
   467  		return err
   468  	}
   469  
   470  	// create specs from source
   471  	osForSettle, err := spec.New(*datasource.SpecFromDefinition(*p.p.DataSourceSpecForSettlementData.Data))
   472  	if err != nil {
   473  		return err
   474  	}
   475  	osForSchedule, err := spec.New(*datasource.SpecFromDefinition(*p.p.DataSourceSpecForSettlementSchedule.Data))
   476  	if err != nil {
   477  		return err
   478  	}
   479  	if err = oracle.bindAll(ctx, oe, osForSettle, osForSchedule, p.receiveDataPoint, p.receiveSettlementCue); err != nil {
   480  		return err
   481  	}
   482  	p.oracle = oracle // ensure oracle on perp is not an old copy
   483  
   484  	return nil
   485  }
   486  
   487  func NewPerpetual(ctx context.Context, log *logging.Logger, p *types.Perps, marketID string, ts TimeService, oe OracleEngine, broker Broker, assetDP uint32) (*Perpetual, error) {
   488  	// make sure we have all we need
   489  	if p.DataSourceSpecForSettlementData == nil || p.DataSourceSpecForSettlementSchedule == nil || p.DataSourceSpecBinding == nil {
   490  		return nil, ErrDataSourceSpecAndBindingAreRequired
   491  	}
   492  	oracle, err := newPerpOracle(p)
   493  	if err != nil {
   494  		return nil, err
   495  	}
   496  	// check decimal places for settlement data
   497  	auctions := &auctionIntervals{}
   498  	perp := &Perpetual{
   499  		p:            p,
   500  		id:           marketID,
   501  		log:          log,
   502  		timeService:  ts,
   503  		broker:       broker,
   504  		assetDP:      assetDP,
   505  		auctions:     auctions,
   506  		externalTWAP: NewCachedTWAP(log, 0, auctions),
   507  		internalTWAP: NewCachedTWAP(log, 0, auctions),
   508  	}
   509  	// create specs from source
   510  	osForSettle, err := spec.New(*datasource.SpecFromDefinition(*p.DataSourceSpecForSettlementData.Data))
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  	osForSchedule, err := spec.New(*datasource.SpecFromDefinition(*p.DataSourceSpecForSettlementSchedule.Data))
   515  	if err != nil {
   516  		return nil, err
   517  	}
   518  	if err = oracle.bindAll(ctx, oe, osForSettle, osForSchedule, perp.receiveDataPoint, perp.receiveSettlementCue); err != nil {
   519  		return nil, err
   520  	}
   521  	perp.oracle = oracle // ensure oracle on perp is not an old copy
   522  
   523  	return perp, nil
   524  }
   525  
   526  func (p *Perpetual) RestoreSettlementData(settleData *num.Numeric) {
   527  	p.log.Panic("not implemented")
   528  }
   529  
   530  // NotifyOnSettlementData for a perpetual this will be the funding payment being sent to the listener.
   531  func (p *Perpetual) NotifyOnSettlementData(listener func(context.Context, *num.Numeric)) {
   532  	p.settlementDataListener = listener
   533  }
   534  
   535  func (p *Perpetual) NotifyOnTradingTerminated(listener func(context.Context, bool)) {
   536  	p.log.Panic("not expecting trading terminated with perpetual")
   537  }
   538  
   539  func (p *Perpetual) NotifyOnDataSourcePropagation(listener func(context.Context, *num.Uint)) {
   540  	p.dataPointListener = listener
   541  }
   542  
   543  func (p *Perpetual) ScaleSettlementDataToDecimalPlaces(price *num.Numeric, dp uint32) (*num.Uint, error) {
   544  	p.log.Panic("not implemented")
   545  	return nil, nil
   546  }
   547  
   548  // Settle a position against the perpetual.
   549  func (p *Perpetual) Settle(entryPriceInAsset, settlementData *num.Uint, netFractionalPosition num.Decimal) (amt *types.FinancialAmount, neg bool, rounding num.Decimal, err error) {
   550  	amount, neg := settlementData.Delta(settlementData, entryPriceInAsset)
   551  	// Make sure net position is positive
   552  	if netFractionalPosition.IsNegative() {
   553  		netFractionalPosition = netFractionalPosition.Neg()
   554  		neg = !neg
   555  	}
   556  
   557  	if p.log.IsDebug() {
   558  		p.log.Debug("settlement",
   559  			logging.String("entry-price-in-asset", entryPriceInAsset.String()),
   560  			logging.String("settlement-data-in-asset", settlementData.String()),
   561  			logging.String("net-fractional-position", netFractionalPosition.String()),
   562  			logging.String("amount-in-decimal", netFractionalPosition.Mul(amount.ToDecimal()).String()),
   563  			logging.String("amount-in-uint", amount.String()),
   564  		)
   565  	}
   566  	a, rem := num.UintFromDecimalWithFraction(netFractionalPosition.Mul(amount.ToDecimal()))
   567  
   568  	return &types.FinancialAmount{
   569  		Asset:  p.p.SettlementAsset,
   570  		Amount: a,
   571  	}, neg, rem, nil
   572  	// p.log.Panic("not implemented")
   573  	// return nil, false, num.DecimalZero(), nil
   574  }
   575  
   576  // Value - returns the nominal value of a unit given a current mark price.
   577  func (p *Perpetual) Value(markPrice *num.Uint) (*num.Uint, error) {
   578  	return markPrice.Clone(), nil
   579  }
   580  
   581  // IsTradingTerminated - returns true when the oracle has signalled terminated market.
   582  func (p *Perpetual) IsTradingTerminated() bool {
   583  	return p.terminated
   584  }
   585  
   586  // GetAsset return the asset used by the future.
   587  func (p *Perpetual) GetAsset() string {
   588  	return p.p.SettlementAsset
   589  }
   590  
   591  func (p *Perpetual) UnsubscribeTradingTerminated(ctx context.Context) {
   592  	// we could just use this call to indicate the underlying perp was terminted
   593  	p.log.Info("unsubscribed trading data and cue oracle on perpetual termination", logging.String("quote-name", p.p.QuoteName))
   594  	p.terminated = true
   595  	p.oracle.unsubAll(ctx)
   596  	p.handleSettlementCue(ctx, p.timeService.GetTimeNow().Truncate(time.Second).UnixNano())
   597  }
   598  
   599  func (p *Perpetual) UnsubscribeSettlementData(ctx context.Context) {
   600  	p.log.Info("unsubscribed trading settlement data for", logging.String("quote-name", p.p.QuoteName))
   601  	p.oracle.unsubAll(ctx)
   602  }
   603  
   604  func (p *Perpetual) UpdateAuctionState(ctx context.Context, enter bool) {
   605  	t := p.timeService.GetTimeNow().Truncate(time.Second).UnixNano()
   606  	if p.log.GetLevel() == logging.DebugLevel {
   607  		p.log.Debug(
   608  			"perpetual auction period start/end",
   609  			logging.String("id", p.id),
   610  			logging.Bool("enter", enter),
   611  			logging.Int64("t", t),
   612  		)
   613  	}
   614  
   615  	if p.readyForData() {
   616  		p.auctions.update(t, enter)
   617  		return
   618  	}
   619  
   620  	if enter {
   621  		return
   622  	}
   623  
   624  	// left first auction, we can start the first funding-period
   625  	p.startedAt = t
   626  
   627  	// we might have been sent useful internal points before officially leaving opening auction, such as
   628  	// the uncrossing price, so we make sure we keep those.
   629  	temp := p.internalTWAP
   630  	p.internalTWAP = NewCachedTWAP(p.log, t, p.auctions)
   631  	for _, pt := range temp.points {
   632  		p.internalTWAP.addPoint(pt)
   633  	}
   634  	p.externalTWAP = NewCachedTWAP(p.log, t, p.auctions)
   635  	p.broker.Send(events.NewFundingPeriodEvent(ctx, p.id, p.seq, p.startedAt, nil, nil, nil, p.internalTWAP.getTWAP(p.startedAt), p.externalTWAP.getTWAP(p.startedAt)))
   636  }
   637  
   638  // SubmitDataPoint this will add a data point produced internally by the core node.
   639  func (p *Perpetual) SubmitDataPoint(ctx context.Context, price *num.Uint, t int64) error {
   640  	// since all external data and funding period triggers are to seconds-precision we also want to truncate
   641  	// internal times to seconds to avoid sub-second backwards intervals that are dependent on the order data arrives
   642  	t = time.Unix(0, t).Truncate(time.Second).UnixNano()
   643  	twap, err := p.internalTWAP.addPoint(&dataPoint{price: price.Clone(), t: t})
   644  	if err != nil {
   645  		return err
   646  	}
   647  	p.broker.Send(events.NewFundingPeriodDataPointEvent(ctx, p.id, price.String(), t, p.seq, dataPointSourceInternal, twap))
   648  	return nil
   649  }
   650  
   651  func (p *Perpetual) receiveDataPoint(ctx context.Context, data dscommon.Data) error {
   652  	if p.log.GetLevel() == logging.DebugLevel {
   653  		p.log.Debug("new oracle data received", data.Debug()...)
   654  	}
   655  
   656  	settlDataDecimals := int64(p.oracle.binding.settlementDecimals)
   657  	odata := &oracleData{
   658  		settlData: &num.Numeric{},
   659  	}
   660  	switch p.oracle.binding.settlementType {
   661  	case datapb.PropertyKey_TYPE_DECIMAL:
   662  		settlDataAsDecimal, err := data.GetDecimal(p.oracle.binding.settlementProperty)
   663  		if err != nil {
   664  			p.log.Error(
   665  				"could not parse decimal type property acting as settlement data",
   666  				logging.Error(err),
   667  			)
   668  			return err
   669  		}
   670  
   671  		odata.settlData.SetDecimal(&settlDataAsDecimal)
   672  
   673  	default:
   674  		settlDataAsUint, err := data.GetUint(p.oracle.binding.settlementProperty)
   675  		if err != nil {
   676  			p.log.Error(
   677  				"could not parse integer type property acting as settlement data",
   678  				logging.Error(err),
   679  			)
   680  			return err
   681  		}
   682  
   683  		odata.settlData.SetUint(settlDataAsUint)
   684  	}
   685  
   686  	// get scaled uint
   687  	assetPrice, err := odata.settlData.ScaleTo(settlDataDecimals, int64(p.assetDP))
   688  	if err != nil {
   689  		p.log.Error("Could not scale the settle data received to asset decimals",
   690  			logging.String("settle-data", odata.settlData.String()),
   691  			logging.Error(err),
   692  		)
   693  		return err
   694  	}
   695  	pTime, err := data.GetDataTimestampNano()
   696  	if err != nil {
   697  		p.log.Error("No timestamp associated with data point",
   698  			logging.Error(err),
   699  		)
   700  		return err
   701  	}
   702  
   703  	// now add the price
   704  	p.addExternalDataPoint(ctx, assetPrice, pTime)
   705  	if p.log.GetLevel() == logging.DebugLevel {
   706  		p.log.Debug(
   707  			"perp settlement data updated",
   708  			logging.String("settlementData", odata.settlData.String()),
   709  		)
   710  	}
   711  	return nil
   712  }
   713  
   714  // receiveDataPoint will be hooked up as a subscriber to the oracle data for incoming settlement data from a data-source.
   715  func (p *Perpetual) addExternalDataPoint(ctx context.Context, price *num.Uint, t int64) {
   716  	if !p.readyForData() {
   717  		p.log.Debug("external data point for perpetual received before initial period", logging.String("id", p.id), logging.Int64("t", t))
   718  		return
   719  	}
   720  	twap, err := p.externalTWAP.addPoint(&dataPoint{price: price.Clone(), t: t})
   721  	if err != nil {
   722  		p.log.Error("unable to add external data point",
   723  			logging.String("id", p.id),
   724  			logging.Error(err),
   725  			logging.String("price", price.String()),
   726  			logging.Int64("t", t))
   727  		return
   728  	}
   729  	p.broker.Send(events.NewFundingPeriodDataPointEvent(ctx, p.id, price.String(), t, p.seq, dataPointSourceExternal, twap))
   730  
   731  	// send it out to anyone thats listening (AMM's mostly)
   732  	p.dataPointListener(ctx, price)
   733  }
   734  
   735  func (p *Perpetual) receiveSettlementCue(ctx context.Context, data dscommon.Data) error {
   736  	if p.log.GetLevel() == logging.DebugLevel {
   737  		p.log.Debug("new schedule oracle data received", data.Debug()...)
   738  	}
   739  	t, err := data.GetTimestamp(p.oracle.binding.scheduleProperty)
   740  	if err != nil {
   741  		p.log.Error("schedule data not valid", data.Debug()...)
   742  		return err
   743  	}
   744  
   745  	// the internal cue gives us the time in seconds, so convert to nanoseconds
   746  	t = time.Unix(t, 0).UnixNano()
   747  
   748  	p.handleSettlementCue(ctx, t)
   749  	if p.log.GetLevel() == logging.DebugLevel {
   750  		p.log.Debug("perp schedule trigger processed")
   751  	}
   752  	return nil
   753  }
   754  
   755  // handleSettlementCue will be hooked up as a subscriber to the oracle data for the notification that the settlement period has ended.
   756  func (p *Perpetual) handleSettlementCue(ctx context.Context, t int64) {
   757  	if !p.readyForData() {
   758  		if p.log.GetLevel() == logging.DebugLevel {
   759  			p.log.Debug("first funding period not started -- ignoring settlement-cue")
   760  		}
   761  		return
   762  	}
   763  
   764  	if !p.haveDataBeforeGivenTime(t) || t == p.startedAt {
   765  		// we have no points, or the interval is zero length so we just start a new interval
   766  		p.broker.Send(events.NewFundingPeriodEvent(ctx, p.id, p.seq, p.startedAt, ptr.From(t), nil, nil, p.internalTWAP.getTWAP(t), p.externalTWAP.getTWAP(t)))
   767  		p.startNewFundingPeriod(ctx, t)
   768  		return
   769  	}
   770  
   771  	// do the calculation
   772  	r := p.calculateFundingPayment(t)
   773  
   774  	// send it away!
   775  	fp := &num.Numeric{}
   776  	p.settlementDataListener(ctx, fp.SetInt(r.fundingPayment))
   777  
   778  	// now restart the interval
   779  	p.broker.Send(events.NewFundingPeriodEvent(ctx, p.id, p.seq, p.startedAt, ptr.From(t),
   780  		ptr.From(r.fundingPayment.String()),
   781  		ptr.From(r.fundingRate.String()),
   782  		ptr.From(r.internalTWAP.String()),
   783  		ptr.From(r.externalTWAP.String())),
   784  	)
   785  	p.startNewFundingPeriod(ctx, t)
   786  }
   787  
   788  func (p *Perpetual) GetData(t int64) *types.ProductData {
   789  	if !p.readyForData() || !p.haveData() {
   790  		return nil
   791  	}
   792  
   793  	t = time.Unix(0, t).Truncate(time.Second).UnixNano()
   794  	r := p.calculateFundingPayment(t)
   795  
   796  	var underlyingIndexPrice *num.Uint
   797  	if len(p.externalTWAP.points) > 0 {
   798  		underlyingIndexPrice = p.externalTWAP.points[len(p.externalTWAP.points)-1].price.Clone()
   799  	}
   800  
   801  	return &types.ProductData{
   802  		Data: &types.PerpetualData{
   803  			FundingRate:          r.fundingRate.String(),
   804  			FundingPayment:       r.fundingPayment.String(),
   805  			InternalTWAP:         r.internalTWAP.String(),
   806  			ExternalTWAP:         r.externalTWAP.String(),
   807  			SeqNum:               p.seq,
   808  			StartTime:            p.startedAt,
   809  			UnderlyingIndexPrice: underlyingIndexPrice,
   810  		},
   811  	}
   812  }
   813  
   814  // restarts the funcing period at time st.
   815  func (p *Perpetual) startNewFundingPeriod(ctx context.Context, endAt int64) {
   816  	if p.terminated {
   817  		// the perpetual market has been terminated so we won't start a new funding period
   818  		return
   819  	}
   820  
   821  	// increment seq and set start to the time the previous ended
   822  	p.seq += 1
   823  	p.startedAt = endAt
   824  	p.log.Info("new settlement period",
   825  		logging.MarketID(p.id),
   826  		logging.Int64("t", endAt),
   827  	)
   828  
   829  	carryOver := func(points []*dataPoint) []*dataPoint {
   830  		carry := []*dataPoint{}
   831  		for i := len(points) - 1; i >= 0; i-- {
   832  			carry = append(carry, points[i])
   833  			if points[i].t <= endAt {
   834  				break
   835  			}
   836  		}
   837  		return carry
   838  	}
   839  
   840  	// carry over data-points at times > endAt and the first data-points that is <= endAt
   841  	external := carryOver(p.externalTWAP.points)
   842  	internal := carryOver(p.internalTWAP.points)
   843  
   844  	// refresh the auction tracker
   845  	p.auctions.restart()
   846  
   847  	// new period new life
   848  	p.externalTWAP = NewCachedTWAP(p.log, endAt, p.auctions)
   849  	p.internalTWAP = NewCachedTWAP(p.log, endAt, p.auctions)
   850  
   851  	// send events for all the data-points that were carried over
   852  	evts := make([]events.Event, 0, len(external)+len(internal))
   853  	for _, dp := range external {
   854  		eTWAP, _ := p.externalTWAP.addPoint(dp)
   855  		evts = append(evts, events.NewFundingPeriodDataPointEvent(ctx, p.id, dp.price.String(), dp.t, p.seq, dataPointSourceExternal, eTWAP))
   856  	}
   857  	for _, dp := range internal {
   858  		iTWAP, _ := p.internalTWAP.addPoint(dp)
   859  		evts = append(evts, events.NewFundingPeriodDataPointEvent(ctx, p.id, dp.price.String(), dp.t, p.seq, dataPointSourceInternal, iTWAP))
   860  	}
   861  	// send event to say our new period has started
   862  	p.broker.Send(events.NewFundingPeriodEvent(ctx, p.id, p.seq, p.startedAt, nil, nil, nil, p.internalTWAP.getTWAP(p.startedAt), p.externalTWAP.getTWAP(p.startedAt)))
   863  	if len(evts) > 0 {
   864  		p.broker.SendBatch(evts)
   865  	}
   866  }
   867  
   868  // readyForData returns whether not we are ready to start accepting data points.
   869  func (p *Perpetual) readyForData() bool {
   870  	return p.startedAt > 0
   871  }
   872  
   873  // haveDataBeforeGivenTime returns whether we have at least one data point from each of the internal and external price series before the given time.
   874  func (p *Perpetual) haveDataBeforeGivenTime(endAt int64) bool {
   875  	if !p.readyForData() {
   876  		return false
   877  	}
   878  
   879  	if !p.haveData() {
   880  		return false
   881  	}
   882  
   883  	if p.internalTWAP.points[0].t > endAt || p.externalTWAP.points[0].t > endAt {
   884  		return false
   885  	}
   886  
   887  	return true
   888  }
   889  
   890  // haveData returns whether we have at least one data point from each of the internal and external price series.
   891  func (p *Perpetual) haveData() bool {
   892  	return len(p.internalTWAP.points) > 0 && len(p.externalTWAP.points) > 0
   893  }
   894  
   895  // calculateFundingPayment returns the funding payment and funding rate for the interval between when the current funding period
   896  // started and the given time. Used on settlement-cues and for margin calculations.
   897  func (p *Perpetual) calculateFundingPayment(t int64) *fundingData {
   898  	internalTWAP := p.internalTWAP.calculate(t)
   899  	externalTWAP := p.externalTWAP.calculate(t)
   900  
   901  	if p.log.GetLevel() == logging.DebugLevel {
   902  		p.log.Debug("twap-calculations",
   903  			logging.MarketID(p.id),
   904  			logging.String("internal", internalTWAP.String()),
   905  			logging.String("external", externalTWAP.String()),
   906  		)
   907  	}
   908  
   909  	// the funding payment is the difference between the two, the sign representing the direction of cash flow
   910  	fundingPayment := num.DecimalFromUint(internalTWAP).Sub(num.DecimalFromUint(externalTWAP))
   911  
   912  	delta := t - p.startedAt
   913  	// apply interest-rates if necessary
   914  	if !p.p.InterestRate.IsZero() {
   915  		if p.log.GetLevel() == logging.DebugLevel {
   916  			p.log.Debug("applying interest-rate with clamping", logging.String("funding-payment", fundingPayment.String()), logging.Int64("delta", delta))
   917  		}
   918  		fundingPayment = fundingPayment.Add(p.calculateInterestTerm(externalTWAP, internalTWAP, delta))
   919  	}
   920  
   921  	// scale funding payment by fraction of funding period spent outside of auction
   922  	timeSpentInAuction := p.auctions.timeSpent(p.startedAt, t)
   923  	if timeSpentInAuction > 0 && delta > 0 {
   924  		scaling := num.DecimalOne().Sub(num.DecimalFromInt64(timeSpentInAuction).Div(num.DecimalFromInt64(delta)))
   925  		fundingPayment = fundingPayment.Mul(scaling)
   926  	}
   927  
   928  	// apply funding scaling factor
   929  	if p.p.FundingRateScalingFactor != nil {
   930  		fundingPayment = fundingPayment.Mul(*p.p.FundingRateScalingFactor)
   931  	}
   932  
   933  	fundingRate := num.DecimalZero()
   934  	if !externalTWAP.IsZero() {
   935  		fundingRate = fundingPayment.Div(num.DecimalFromUint(externalTWAP))
   936  	}
   937  
   938  	// apply upper/lower bound capping
   939  	if p.p.FundingRateUpperBound != nil && p.p.FundingRateUpperBound.LessThan(fundingRate) {
   940  		fundingRate = p.p.FundingRateUpperBound.Copy()
   941  		fundingPayment = fundingRate.Mul(num.DecimalFromUint(externalTWAP))
   942  	}
   943  
   944  	if p.p.FundingRateLowerBound != nil && p.p.FundingRateLowerBound.GreaterThan(fundingRate) {
   945  		fundingRate = p.p.FundingRateLowerBound.Copy()
   946  		fundingPayment = fundingRate.Mul(num.DecimalFromUint(externalTWAP))
   947  	}
   948  
   949  	if p.log.GetLevel() == logging.DebugLevel {
   950  		p.log.Debug("funding payment calculated",
   951  			logging.MarketID(p.id),
   952  			logging.Uint64("seq", p.seq),
   953  			logging.String("funding-payment", fundingPayment.String()),
   954  			logging.String("funding-rate", fundingRate.String()))
   955  	}
   956  	fundingPaymentInt, _ := num.IntFromDecimal(fundingPayment)
   957  	return &fundingData{
   958  		fundingPayment: fundingPaymentInt,
   959  		fundingRate:    fundingRate,
   960  		externalTWAP:   externalTWAP,
   961  		internalTWAP:   internalTWAP,
   962  	}
   963  }
   964  
   965  func (p *Perpetual) calculateInterestTerm(externalTWAP, internalTWAP *num.Uint, delta int64) num.Decimal {
   966  	// get delta in terms of years
   967  	td := num.DecimalFromInt64(delta).Div(year)
   968  
   969  	// convert into num types we need
   970  	sTWAP := num.DecimalFromUint(externalTWAP)
   971  	fTWAP := num.DecimalFromUint(internalTWAP)
   972  
   973  	// interest = (1 + r * td) * s_swap - f_swap
   974  	interest := num.DecimalOne().Add(p.p.InterestRate.Mul(td)).Mul(sTWAP)
   975  	interest = interest.Sub(fTWAP)
   976  
   977  	upperBound := num.DecimalFromUint(externalTWAP).Mul(p.p.ClampUpperBound)
   978  	lowerBound := num.DecimalFromUint(externalTWAP).Mul(p.p.ClampLowerBound)
   979  
   980  	clampedInterest := num.MinD(upperBound, num.MaxD(lowerBound, interest))
   981  	if p.log.GetLevel() == logging.DebugLevel {
   982  		p.log.Debug("clamped interest and bounds",
   983  			logging.MarketID(p.id),
   984  			logging.String("lower-bound", p.p.ClampLowerBound.String()),
   985  			logging.String("interest", interest.String()),
   986  			logging.String("upper-bound", p.p.ClampUpperBound.String()),
   987  			logging.String("clamped-interest", clampedInterest.String()),
   988  		)
   989  	}
   990  	return clampedInterest
   991  }
   992  
   993  // GetMarginIncrease returns the estimated extra margin required to account for the next funding payment
   994  // for a party with a position of +1.
   995  func (p *Perpetual) GetMarginIncrease(t int64) num.Decimal {
   996  	t = time.Unix(0, t).Truncate(time.Second).UnixNano()
   997  
   998  	// if we have no data, or the funding factor is zero, then the margin increase will always be zero
   999  	if !p.haveDataBeforeGivenTime(t) || p.p.MarginFundingFactor.IsZero() {
  1000  		return num.DecimalZero()
  1001  	}
  1002  
  1003  	fundingPayment := p.calculateFundingPayment(t).fundingPayment
  1004  
  1005  	// apply factor
  1006  	return num.DecimalFromInt(fundingPayment).Mul(p.p.MarginFundingFactor)
  1007  }