code.vegaprotocol.io/vega@v0.79.0/core/liquidity/target/engine.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 target
    17  
    18  import (
    19  	"errors"
    20  	"time"
    21  
    22  	"code.vegaprotocol.io/vega/core/types"
    23  	"code.vegaprotocol.io/vega/libs/num"
    24  )
    25  
    26  var (
    27  	// ErrTimeSequence signals that time sequence is not in a non-decreasing order.
    28  	ErrTimeSequence = errors.New("received a time that's before the last received time")
    29  	// ErrNegativeScalingFactor indicates that a negative scaling factor was supplied to the engine.
    30  	ErrNegativeScalingFactor = errors.New("scaling factor can't be negative")
    31  )
    32  
    33  var (
    34  	exp    = num.UintZero().Exp(num.NewUint(10), num.NewUint(5))
    35  	exp2   = num.UintZero().Exp(num.NewUint(10), num.NewUint(10))
    36  	expDec = num.DecimalFromUint(exp)
    37  )
    38  
    39  // Engine allows tracking price changes and verifying them against the theoretical levels implied by the RangeProvider (risk model).
    40  type Engine struct {
    41  	marketID string
    42  
    43  	tWindow time.Duration
    44  	sFactor *num.Uint
    45  	oiCalc  OpenInterestCalculator
    46  
    47  	now               time.Time
    48  	scheduledTruncate time.Time
    49  	current           []uint64
    50  	previous          []timestampedOI
    51  	max               timestampedOI
    52  	positionFactor    num.Decimal
    53  }
    54  
    55  type timestampedOI struct {
    56  	Time time.Time
    57  	OI   uint64
    58  }
    59  
    60  //go:generate go run github.com/golang/mock/mockgen -destination mocks/open_interest_calculator_mock.go -package mocks code.vegaprotocol.io/vega/core/liquidity/target OpenInterestCalculator
    61  type OpenInterestCalculator interface {
    62  	GetOpenInterestGivenTrades(trades []*types.Trade) uint64
    63  }
    64  
    65  // NewEngine returns a new instance of target stake calculation Engine.
    66  func NewEngine(parameters types.TargetStakeParameters, oiCalc OpenInterestCalculator, marketID string, positionFactor num.Decimal) *Engine {
    67  	factor, _ := num.UintFromDecimal(parameters.ScalingFactor.Mul(expDec))
    68  
    69  	return &Engine{
    70  		marketID:       marketID,
    71  		tWindow:        time.Duration(parameters.TimeWindow) * time.Second,
    72  		sFactor:        factor,
    73  		oiCalc:         oiCalc,
    74  		positionFactor: positionFactor,
    75  	}
    76  }
    77  
    78  // UpdateTimeWindow updates the time windows used in target stake calculation.
    79  func (e *Engine) UpdateTimeWindow(tWindow time.Duration) {
    80  	e.tWindow = tWindow
    81  }
    82  
    83  // UpdateScalingFactor updates the scaling factor used in target stake calculation
    84  // if it's non-negative and returns an error otherwise.
    85  func (e *Engine) UpdateScalingFactor(sFactor num.Decimal) error {
    86  	if sFactor.IsNegative() {
    87  		return ErrNegativeScalingFactor
    88  	}
    89  	factor, _ := num.UintFromDecimal(sFactor.Mul(expDec))
    90  	e.sFactor = factor
    91  
    92  	return nil
    93  }
    94  
    95  // RecordOpenInterest records open interest history so that target stake can be calculated.
    96  func (e *Engine) RecordOpenInterest(oi uint64, now time.Time) error {
    97  	if now.Before(e.now) {
    98  		return ErrTimeSequence
    99  	}
   100  
   101  	if oi >= e.max.OI {
   102  		e.max = timestampedOI{Time: now, OI: oi}
   103  	}
   104  
   105  	if now.After(e.now) {
   106  		// get max from current before updating timestamp
   107  		e.previous = append(e.previous, e.getMaxFromCurrent())
   108  		e.current = make([]uint64, 0, len(e.current))
   109  		e.now = now
   110  	}
   111  	e.current = append(e.current, oi)
   112  
   113  	if e.now.After(e.scheduledTruncate) {
   114  		e.truncateHistory(e.minTime(now))
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // GetTargetStake returns target stake based current time, risk factors
   121  // and the open interest time series constructed by calls to RecordOpenInterest.
   122  func (e *Engine) GetTargetStake(rf types.RiskFactor, now time.Time, markPrice *num.Uint) (*num.Uint, bool) {
   123  	var changed bool
   124  	if minTime := e.minTime(now); minTime.After(e.max.Time) {
   125  		e.computeMaxOI(minTime)
   126  		changed = true
   127  	}
   128  
   129  	// float64(markPrice.Uint64()*e.max.OI) * math.Max(rf.Short, rf.Long) * e.sFactor
   130  	factor := rf.Long
   131  	if factor.LessThan(rf.Short) {
   132  		factor = rf.Short
   133  	}
   134  	factorUint, _ := num.UintFromDecimal(factor.Mul(expDec))
   135  
   136  	value, _ := num.UintFromDecimal(markPrice.ToDecimal().Mul(num.DecimalFromInt64(int64(e.max.OI))).Div(e.positionFactor))
   137  	return num.UintZero().Div(num.UintZero().Mul(value, factorUint.Mul(factorUint, e.sFactor)), exp2), changed
   138  }
   139  
   140  // GetTheoreticalTargetStake returns target stake based current time, risk factors
   141  // and the supplied trades without modifying the internal state.
   142  func (e *Engine) GetTheoreticalTargetStake(rf types.RiskFactor, now time.Time, markPrice *num.Uint, trades []*types.Trade) (*num.Uint, bool) {
   143  	var changed bool
   144  	theoreticalOI := e.oiCalc.GetOpenInterestGivenTrades(trades)
   145  
   146  	timeWindowStart := e.minTime(now)
   147  	maxExpired := timeWindowStart.After(e.max.Time)
   148  	if maxExpired {
   149  		e.computeMaxOI(timeWindowStart)
   150  		changed = true
   151  	}
   152  
   153  	maxOI := e.max.OI
   154  	if theoreticalOI > maxOI || maxExpired {
   155  		maxOI = theoreticalOI
   156  	}
   157  
   158  	factor := rf.Long
   159  	if factor.LessThan(rf.Short) {
   160  		factor = rf.Short
   161  	}
   162  
   163  	factorUint, _ := num.UintFromDecimal(factor.Mul(expDec))
   164  	value, _ := num.UintFromDecimal(markPrice.ToDecimal().Mul(num.DecimalFromInt64(int64(maxOI))).Div(e.positionFactor))
   165  	return num.UintZero().Div(num.UintZero().Mul(value, factorUint.Mul(factorUint, e.sFactor)), exp2), changed
   166  }
   167  
   168  func (e *Engine) UpdateParameters(parameters types.TargetStakeParameters) {
   169  	factor, _ := num.UintFromDecimal(parameters.ScalingFactor.Mul(expDec))
   170  	e.sFactor = factor
   171  	e.tWindow = time.Duration(parameters.TimeWindow) * time.Second
   172  }
   173  
   174  func (e *Engine) getMaxFromCurrent() timestampedOI {
   175  	if len(e.current) == 0 {
   176  		return timestampedOI{Time: e.now, OI: 0}
   177  	}
   178  	m := e.current[0]
   179  	for i := 1; i < len(e.current); i++ {
   180  		if e.current[i] > m {
   181  			m = e.current[i]
   182  		}
   183  	}
   184  	return timestampedOI{Time: e.now, OI: m}
   185  }
   186  
   187  func (e *Engine) computeMaxOI(minTime time.Time) {
   188  	m := e.getMaxFromCurrent()
   189  	e.truncateHistory(minTime)
   190  	var j int
   191  	for i := 0; i < len(e.previous); i++ {
   192  		if e.previous[i].OI > m.OI {
   193  			m = e.previous[i]
   194  			j = i
   195  		}
   196  	}
   197  	e.max = m
   198  
   199  	// remove entries less than max as these won't ever be needed anyway
   200  	e.previous = e.previous[j:]
   201  }
   202  
   203  // minTime returns the lower bound of the sliding time window.
   204  func (e *Engine) minTime(now time.Time) time.Time {
   205  	return now.Add(-e.tWindow)
   206  }
   207  
   208  func (e *Engine) truncateHistory(minTime time.Time) {
   209  	var i int
   210  	for i = 0; i < len(e.previous); i++ {
   211  		if !e.previous[i].Time.Before(minTime) {
   212  			break
   213  		}
   214  	}
   215  	e.previous = e.previous[i:]
   216  	// Truncate at least every 2 time windows in case not called before to prevent excessive memory usage
   217  	e.scheduledTruncate = e.now.Add(2 * e.tWindow)
   218  }