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 }