Skip to content

Commit

Permalink
feat: exclude time spent in auction from perpetual TWAP
Browse files Browse the repository at this point in the history
  • Loading branch information
wwestgarth committed Nov 14, 2023
1 parent aa29800 commit 85ac321
Show file tree
Hide file tree
Showing 16 changed files with 2,693 additions and 2,081 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

- [9930](https://github.com/vegaprotocol/vega/issues/9930) - `LiquidityFeeSettings` can now be used in market proposals to choose how liquidity fees are calculated.
- [9985](https://github.com/vegaprotocol/vega/issues/9985) - Add update margin mode transaction.
- [9936](https://github.com/vegaprotocol/vega/issues/9936) - Time spent in auction no longer contributes to a perpetual market's funding payment.
- [9982](https://github.com/vegaprotocol/vega/issues/9982) - Remove fees and minimal transfer amount from vested account
- [9955](https://github.com/vegaprotocol/vega/issues/9955) - Add data node subscription for transaction results.
- [10004](https://github.com/vegaprotocol/vega/issues/10004) Track average entry price in position engine
Expand Down
3 changes: 0 additions & 3 deletions core/execution/future/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ func (m *Market) checkAuction(ctx context.Context, now time.Time, idgen common.I
m.log.Info("leaving opening auction for market", logging.String("market-id", m.mkt.ID))
m.leaveAuction(ctx, now)

// tell the product we're ready to start
m.tradableInstrument.Instrument.Product.OnLeaveOpeningAuction(ctx, now.UnixNano())

m.equityShares.OpeningAuctionEnded()
// start the market fee window
m.feeSplitter.TimeWindowStart(now)
Expand Down
3 changes: 3 additions & 0 deletions core/execution/future/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ func (m *Market) UpdateMarketState(ctx context.Context, changes *types.MarketSta
}
} else {
m.as.StartGovernanceSuspensionAuction(m.timeService.GetTimeNow())
m.tradableInstrument.Instrument.UpdateAuctionState(ctx, true)
m.enterAuction(ctx)
m.broker.Send(events.NewMarketUpdatedEvent(ctx, *m.mkt))
}
Expand Down Expand Up @@ -1296,6 +1297,7 @@ func (m *Market) enterAuction(ctx context.Context) {
if m.as.InAuction() && m.as.IsPriceAuction() {
m.mkt.State = types.MarketStateSuspended
m.mkt.TradingMode = types.MarketTradingModeMonitoringAuction
m.tradableInstrument.Instrument.UpdateAuctionState(ctx, true)
m.broker.Send(events.NewMarketUpdatedEvent(ctx, *m.mkt))
}
}
Expand Down Expand Up @@ -1357,6 +1359,7 @@ func (m *Market) leaveAuction(ctx context.Context, now time.Time) {

m.mkt.State = types.MarketStateActive
m.mkt.TradingMode = types.MarketTradingModeContinuous
m.tradableInstrument.Instrument.UpdateAuctionState(ctx, false)
m.broker.Send(events.NewMarketUpdatedEvent(ctx, *m.mkt))

m.updateLiquidityFee(ctx)
Expand Down
21 changes: 13 additions & 8 deletions core/execution/future/market_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ func NewMarketFromSnapshot(
) (*Market, error) {
mkt := em.Market

if vgcontext.InProgressUpgradeFrom(ctx, "v0.73.2") {
// protocol upgrade from v0.73.2, lets populate the new liquidity-fee-settings with a default marginal-cost method
log.Info("migrating liquidity fee settings for existing market", logging.String("mid", mkt.ID))
mkt.Fees.LiquidityFeeSettings = &types.LiquidityFeeSettings{
Method: types.LiquidityFeeMethodMarginalCost,
}
}

positionFactor := num.DecimalFromFloat(10).Pow(num.DecimalFromInt64(mkt.PositionDecimalPlaces))
if len(em.Market.ID) == 0 {
return nil, common.ErrEmptyMarketID
Expand All @@ -89,6 +81,19 @@ func NewMarketFromSnapshot(

as := monitor.NewAuctionStateFromSnapshot(mkt, em.AuctionState)

if vgcontext.InProgressUpgradeFrom(ctx, "v0.73.4") {
// protocol upgrade from v0.73.4, lets populate the new liquidity-fee-settings with a default marginal-cost method
log.Info("migrating liquidity fee settings for existing market", logging.String("mid", mkt.ID))
mkt.Fees.LiquidityFeeSettings = &types.LiquidityFeeSettings{
Method: types.LiquidityFeeMethodMarginalCost,
}

// if the market is in a none opening-auction we need to tell the instrument
if as.InAuction() && !as.IsOpeningAuction() {
tradableInstrument.Instrument.Product.UpdateAuctionState(ctx, true)
}
}

// @TODO -> the raw auctionstate shouldn't be something exposed to the matching engine
// as far as matching goes: it's either an auction or not
book := matching.NewCachedOrderBook(
Expand Down
2 changes: 1 addition & 1 deletion core/execution/future/market_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func TestRestoreMarketUpgradeV0_73_2(t *testing.T) {
em.Market.Fees.LiquidityFeeSettings = nil

// and set in the context the information that says we are upgrading
ctx := vegacontext.WithSnapshotInfo(context.Background(), "v0.73.2", true)
ctx := vegacontext.WithSnapshotInfo(context.Background(), "v0.73.4", true)
snap, err := newMarketFromSnapshot(t, ctx, ctrl, em, oracleEngine)
require.NoError(t, err)
require.NotEmpty(t, snap)
Expand Down
2 changes: 1 addition & 1 deletion core/governance/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func (e *Engine) restoreActiveProposals(ctx context.Context, active *types.Gover
invalidVotes: votesAsMap(p.Invalid),
}

if vgcontext.InProgressUpgradeFrom(ctx, "v0.73.2") {
if vgcontext.InProgressUpgradeFrom(ctx, "v0.73.4") {
if nm := pp.Proposal.Terms.GetNewMarket(); nm != nil {
e.log.Info("migrating liquidity fee settings for new market proposal", logging.String("pid", pp.ID))
nm.Changes.LiquidityFeeSettings = &types.LiquidityFeeSettings{
Expand Down
4 changes: 4 additions & 0 deletions core/markets/instrument.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func NewInstrument(ctx context.Context, log *logging.Logger, pi *types.Instrumen
}, err
}

func (i *Instrument) UpdateAuctionState(ctx context.Context, enter bool) {
i.Product.UpdateAuctionState(ctx, enter)
}

func (i *Instrument) UnsubscribeTradingTerminated(ctx context.Context) {
i.Product.UnsubscribeTradingTerminated(ctx)
}
Expand Down
2 changes: 1 addition & 1 deletion core/products/future.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (f *Future) SubmitDataPoint(_ context.Context, _ *num.Uint, _ int64) error
return nil
}

func (f *Future) OnLeaveOpeningAuction(_ context.Context, _ int64) {
func (f *Future) UpdateAuctionState(_ context.Context, _ bool) {
}

func (f *Future) GetMarginIncrease(_ int64) num.Decimal {
Expand Down
143 changes: 130 additions & 13 deletions core/products/perpetual.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,71 @@ type fundingData struct {
externalTWAP *num.Uint
}

type auctionIntervals struct {
auctions []int64 // complete auction intervals as pairs of enter/leave time values, always of even length
total int64 // the sum of all the complete auction intervals giving the total time spent in auction
auctionStart int64 // if we are currently in an auction, this is the time we entered it
}

// restart resets the auction interval tracking, remembering the current "in-auction" state by carrying over `auctionStart`.
func (a *auctionIntervals) restart() {
a.total = 0
a.auctions = []int64{}
}

// update adds a new aution enter/leave boundary to the auction intervals being tracked.
func (a *auctionIntervals) update(t int64, enter bool) {
if (a.auctionStart != 0) == enter {
panic("flags out of sync - double entry or double leave auction detected")
}

if enter {
a.auctionStart = t
return
}

// we've left an auction period, add the new st/nd to the completed auction periods
a.auctions = append(a.auctions, a.auctionStart, t)

// update our running total
a.total += t - a.auctionStart
a.auctionStart = 0
}

// timeSpent returns how long the time interval [st, nd] was spent in auction in the current funding period.
func (a *auctionIntervals) timeSpent(st, nd int64) int64 {
if nd < st {
panic("cannot process backwards interval")
}

var sum int64
if a.auctionStart != 0 && a.auctionStart < nd {
if st > a.auctionStart {
return nd - st
}
// we want to include the in-progress auction period, so add on how much into it we are
sum = nd - a.auctionStart
}

// if [st, nd] contains all the auction intervals we can just return the running total
if len(a.auctions) != 0 && st <= a.auctions[0] && a.auctions[len(a.auctions)-1] <= nd {
return a.total + sum
}

// iterare over the completed auction periods in pairs and add regions of the auction intervals
// that overlap with [st, nd]
for i := len(a.auctions) - 2; i >= 0; i = i - 2 {
// [st, nd] is entirely after this auction period, we can stop now
if a.auctions[i+1] < st {
break
}
// calculate
sum += num.MaxV(0, num.MinV(nd, a.auctions[i+1])-num.MaxV(st, a.auctions[i]))
}

return sum
}

type cachedTWAP struct {
log *logging.Logger

Expand All @@ -64,15 +129,18 @@ type cachedTWAP struct {
end int64 // time of the last calculated sub-product that was >= the last added data-point
sumProduct *num.Uint // the sum-product of all the intervals between the data-points from `start` -> `end`
points []*dataPoint // the data-points used to calculate the twap

auctions *auctionIntervals
}

func NewCachedTWAP(log *logging.Logger, t int64) *cachedTWAP {
func NewCachedTWAP(log *logging.Logger, t int64, auctions *auctionIntervals) *cachedTWAP {
return &cachedTWAP{
log: log,
start: t,
periodStart: t,
end: t,
sumProduct: num.UintZero(),
auctions: auctions,
}
}

Expand Down Expand Up @@ -103,6 +171,9 @@ func (c *cachedTWAP) unwind(t int64) (*num.Uint, int) {

// now we need to remove the contribution from this interval
delta := point.t - num.MaxV(prev.t, c.start)

// minus time in auction
delta -= c.auctions.timeSpent(num.MaxV(prev.t, c.start), point.t)
sub := num.UintZero().Mul(prev.price, num.NewUint(uint64(delta)))

// before we subtract, lets sanity check some things
Expand Down Expand Up @@ -131,33 +202,49 @@ func (c *cachedTWAP) calculate(t int64) *num.Uint {
if t < c.start || len(c.points) == 0 {
return num.UintZero()
}
if t == c.start {
return c.points[0].price.Clone()
}

if t == c.end {
// already have the sum product here, just twap-it
return num.UintZero().Div(c.sumProduct, num.NewUint(uint64(c.end-c.start)))
period := c.end - c.start
period -= c.auctions.timeSpent(c.start, c.end)
return num.UintZero().Div(c.sumProduct, num.NewUint(uint64(period)))
}

// if the time we want the twap from is before the last data-point we need to unwind the intervals
point := c.points[len(c.points)-1]
if t < point.t {
sumProduct, idx := c.unwind(t)
p := c.points[idx]

delta := t - p.t
delta -= c.auctions.timeSpent(p.t, t)

period := t - c.start
period -= c.auctions.timeSpent(c.start, t)

sumProduct.Add(sumProduct, num.UintZero().Mul(p.price, num.NewUint(uint64(delta))))
return num.UintZero().Div(sumProduct, num.NewUint(uint64(t-c.start)))
return num.UintZero().Div(sumProduct, num.NewUint(uint64(period)))
}

// the twap we want is after the final data-point so we can just extend the calculation (or shortern if we've already extended)
delta := t - c.end
period := t - c.start
period -= c.auctions.timeSpent(c.start, t)

sumProduct := c.sumProduct.Clone()
newPeriod := num.NewUint(uint64(t - c.start))
newPeriod := num.NewUint(uint64(period))
lastPrice := point.price.Clone()

// add or subtract from the sum-product based on if we are extending/shortening the interval
switch {
case delta < 0:
delta += c.auctions.timeSpent(t, c.end)
sumProduct.Sub(sumProduct, lastPrice.Mul(lastPrice, num.NewUint(uint64(-delta))))
case delta > 0:
delta -= c.auctions.timeSpent(c.end, t)
sumProduct.Add(sumProduct, lastPrice.Mul(lastPrice, num.NewUint(uint64(delta))))
}
// store these as the last calculated as its likely to be asked again
Expand Down Expand Up @@ -207,7 +294,7 @@ func (c *cachedTWAP) addPoint(point *dataPoint) (*num.Uint, error) {
c.points = []*dataPoint{point}
c.setPeriod(point.t, point.t)
c.sumProduct = num.UintZero()
return num.UintZero(), nil
return point.price.Clone(), nil
}

// point to add is before the very first point we added, a little weird but ok
Expand All @@ -220,7 +307,7 @@ func (c *cachedTWAP) addPoint(point *dataPoint) (*num.Uint, error) {
c.calculate(p.t)
c.points = append(c.points, p)
}
return num.UintZero(), nil
return point.price.Clone(), nil
}

// new point is after the last point, just calculate the TWAP at point.t and append
Expand Down Expand Up @@ -276,6 +363,7 @@ type Perpetual struct {
// twap calculators
internalTWAP *cachedTWAP
externalTWAP *cachedTWAP
auctions *auctionIntervals
}

func (p Perpetual) GetCurrentPeriod() uint64 {
Expand Down Expand Up @@ -330,15 +418,17 @@ func NewPerpetual(ctx context.Context, log *logging.Logger, p *types.Perps, mark
return nil, err
}
// check decimal places for settlement data
auctions := &auctionIntervals{}
perp := &Perpetual{
p: p,
id: marketID,
log: log,
timeService: ts,
broker: broker,
assetDP: assetDP,
externalTWAP: NewCachedTWAP(log, 0),
internalTWAP: NewCachedTWAP(log, 0),
auctions: auctions,
externalTWAP: NewCachedTWAP(log, 0, auctions),
internalTWAP: NewCachedTWAP(log, 0, auctions),
}
// create specs from source
osForSettle, err := spec.New(*datasource.SpecFromDefinition(*p.DataSourceSpecForSettlementData.Data))
Expand Down Expand Up @@ -431,10 +521,30 @@ func (p *Perpetual) UnsubscribeSettlementData(ctx context.Context) {
p.oracle.unsubAll(ctx)
}

func (p *Perpetual) OnLeaveOpeningAuction(ctx context.Context, t int64) {
func (p *Perpetual) UpdateAuctionState(ctx context.Context, enter bool) {
t := p.timeService.GetTimeNow().Truncate(time.Second).UnixNano()
if p.log.GetLevel() == logging.DebugLevel {
p.log.Debug(
"perpetual auction period start/end",
logging.String("id", p.id),
logging.Bool("enter", enter),
logging.Int64("t", t),
)
}

if p.readyForData() {
p.auctions.update(t, enter)
return
}

if enter {
return
}

// left first auction, we can start the first funding-period
p.startedAt = t
p.internalTWAP = NewCachedTWAP(p.log, t)
p.externalTWAP = NewCachedTWAP(p.log, t)
p.internalTWAP = NewCachedTWAP(p.log, t, p.auctions)
p.externalTWAP = NewCachedTWAP(p.log, t, p.auctions)
p.broker.Send(events.NewFundingPeriodEvent(ctx, p.id, p.seq, p.startedAt, nil, nil, nil, nil, nil))
}

Expand All @@ -444,6 +554,9 @@ func (p *Perpetual) SubmitDataPoint(ctx context.Context, price *num.Uint, t int6
return ErrInitialPeriodNotStarted
}

// since all external data and funding period triggers are to seconds-precision we also want to truncate
// internal times to seconds to avoid sub-second backwards intervals that are dependent on the order data arrives
t = time.Unix(0, t).Truncate(time.Second).UnixNano()
twap, err := p.internalTWAP.addPoint(&dataPoint{price: price.Clone(), t: t})
if err != nil {
return err
Expand Down Expand Up @@ -592,6 +705,7 @@ func (p *Perpetual) GetData(t int64) *types.ProductData {
return nil
}

t = time.Unix(0, t).Truncate(time.Second).UnixNano()
r := p.calculateFundingPayment(t)
return &types.ProductData{
Data: &types.PerpetualData{
Expand Down Expand Up @@ -633,9 +747,12 @@ func (p *Perpetual) startNewFundingPeriod(ctx context.Context, endAt int64) {
external := carryOver(p.externalTWAP.points)
internal := carryOver(p.internalTWAP.points)

// refresh the auction tracker
p.auctions.restart()

// new period new life
p.externalTWAP = NewCachedTWAP(p.log, endAt)
p.internalTWAP = NewCachedTWAP(p.log, endAt)
p.externalTWAP = NewCachedTWAP(p.log, endAt, p.auctions)
p.internalTWAP = NewCachedTWAP(p.log, endAt, p.auctions)

// send events for all the data-points that were carried over
evts := make([]events.Event, 0, len(external)+len(internal))
Expand Down
Loading

0 comments on commit 85ac321

Please sign in to comment.