code.vegaprotocol.io/vega@v0.79.0/core/positions/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 positions 17 18 import ( 19 "context" 20 "encoding/binary" 21 "fmt" 22 "sort" 23 "sync" 24 25 "code.vegaprotocol.io/vega/core/events" 26 "code.vegaprotocol.io/vega/core/types" 27 "code.vegaprotocol.io/vega/libs/crypto" 28 "code.vegaprotocol.io/vega/libs/num" 29 "code.vegaprotocol.io/vega/logging" 30 31 "golang.org/x/exp/maps" 32 ) 33 34 // Broker (no longer need to mock this, use the broker/mocks wrapper). 35 type Broker interface { 36 Send(event events.Event) 37 SendBatch(events []events.Event) 38 } 39 40 // Engine represents the positions engine. 41 type Engine struct { 42 marketID string 43 log *logging.Logger 44 Config 45 46 cfgMu sync.Mutex 47 // partyID -> MarketPosition 48 positions map[string]*MarketPosition 49 50 // this is basically tracking all position to 51 // not perform a copy when positions a retrieved by other engines 52 // the pointer is hidden behind the interface, and do not expose 53 // any function to mutate them, so we can consider it safe to return 54 // this slice. 55 positionsCpy []events.MarketPosition 56 57 // keep track of the position updated during the current block to avoid sending 58 updatedPositions map[string]struct{} 59 positionUpdated func(context.Context, *MarketPosition) 60 61 broker Broker 62 63 // keep track of open, but distressed positions, this speeds things up when creating the event data 64 // and when generating snapshots 65 distressedPos map[string]struct{} 66 67 // keeps track of the lowest openInterest 68 // per party over a whole epoch 69 // should be reseted by the market on new epochs 70 partiesHighestVolume map[string]*openVolumeRecord 71 72 // keep track of the traded volume during the epoch 73 // will be reset 74 partiesTradedSize map[string]uint64 75 } 76 77 type openVolumeRecord struct { 78 Latest uint64 79 Highest uint64 80 } 81 82 // RecordLatest will save the new openInterest for a party 83 // but also register it as the lowest if it goes below 84 // the existing lowest openInterest. 85 func (o *openVolumeRecord) RecordLatest(size int64) { 86 new := size 87 if size < 0 { 88 new = -size 89 } 90 91 o.Latest = uint64(new) 92 if o.Highest < uint64(new) { 93 o.Highest = uint64(new) 94 } 95 } 96 97 // Reset will be used at the end of the epoch, 98 // in order to set the lowest for the epoch to 99 // be the latest of the previous epoch. 100 func (o *openVolumeRecord) Reset() uint64 { 101 highest := o.Highest 102 o.Highest = o.Latest 103 104 return highest 105 } 106 107 // New instantiates a new positions engine. 108 func New(log *logging.Logger, config Config, marketID string, broker Broker) *Engine { 109 // setup logger 110 log = log.Named(namedLogger) 111 log.SetLevel(config.Level.Get()) 112 113 e := &Engine{ 114 marketID: marketID, 115 Config: config, 116 log: log, 117 positions: map[string]*MarketPosition{}, 118 positionsCpy: []events.MarketPosition{}, 119 broker: broker, 120 updatedPositions: map[string]struct{}{}, 121 distressedPos: map[string]struct{}{}, 122 partiesHighestVolume: map[string]*openVolumeRecord{}, 123 partiesTradedSize: map[string]uint64{}, 124 } 125 e.positionUpdated = e.bufferPosition 126 if config.StreamPositionVerbose { 127 e.positionUpdated = e.sendPosition 128 } 129 130 return e 131 } 132 133 func (e *Engine) FlushPositionEvents(ctx context.Context) { 134 if e.StreamPositionVerbose || len(e.updatedPositions) <= 0 { 135 return 136 } 137 138 e.sendBufferedPosition(ctx) 139 } 140 141 func (e *Engine) bufferPosition(_ context.Context, pos *MarketPosition) { 142 e.updatedPositions[pos.partyID] = struct{}{} 143 } 144 145 func (e *Engine) sendBufferedPosition(ctx context.Context) { 146 parties := maps.Keys(e.updatedPositions) 147 sort.Strings(parties) 148 evts := make([]events.Event, 0, len(parties)) 149 150 for _, v := range parties { 151 // ensure the position exists, 152 // party might have been distressed or something 153 if pos, ok := e.positions[v]; ok { 154 evts = append(evts, events.NewPositionStateEvent(ctx, pos, e.marketID)) 155 } 156 } 157 158 e.broker.SendBatch(evts) 159 e.updatedPositions = make(map[string]struct{}, len(e.updatedPositions)) 160 } 161 162 func (e *Engine) sendPosition(ctx context.Context, pos *MarketPosition) { 163 e.broker.Send(events.NewPositionStateEvent(ctx, pos, e.marketID)) 164 } 165 166 func (e *Engine) Hash() []byte { 167 // Fields * FieldSize = (8 * 3) 168 // Prices = 32 * 2 169 output := make([]byte, len(e.positionsCpy)*((8*3)+(32*2))) 170 var i int 171 for _, p := range e.positionsCpy { 172 values := []uint64{ 173 uint64(p.Size()), 174 uint64(p.Buy()), 175 uint64(p.Sell()), 176 } 177 178 for _, v := range values { 179 binary.BigEndian.PutUint64(output[i:], v) 180 i += 8 181 } 182 183 // Add bytes for VWBuy and VWSell here 184 b := p.BuySumProduct().Bytes() 185 copy(output[i:], b[:]) 186 i += 32 187 s := p.SellSumProduct().Bytes() 188 copy(output[i:], s[:]) 189 i += 32 190 } 191 192 return crypto.Hash(output) 193 } 194 195 // ReloadConf update the internal configuration of the positions engine. 196 func (e *Engine) ReloadConf(cfg Config) { 197 e.log.Info("reloading configuration") 198 if e.log.GetLevel() != cfg.Level.Get() { 199 e.log.Info("updating log level", 200 logging.String("old", e.log.GetLevel().String()), 201 logging.String("new", cfg.Level.String()), 202 ) 203 e.log.SetLevel(cfg.Level.Get()) 204 } 205 206 e.cfgMu.Lock() 207 e.Config = cfg 208 e.cfgMu.Unlock() 209 } 210 211 // ValidateOrder checks that the given order can be registered. 212 func (e *Engine) ValidateOrder(order *types.Order) error { 213 pos, found := e.positions[order.Party] 214 if !found { 215 pos = NewMarketPosition(order.Party) 216 } 217 return pos.ValidateOrderRegistration(order.TrueRemaining(), order.Side) 218 } 219 220 // ValidateAmendOrder checks that replace the given order with a new order will no cause issues with position tracking. 221 func (e *Engine) ValidateAmendOrder(order *types.Order, newOrder *types.Order) error { 222 pos, found := e.positions[order.Party] 223 if !found { 224 pos = NewMarketPosition(order.Party) 225 } 226 227 if order.TrueRemaining() >= newOrder.TrueRemaining() { 228 return nil 229 } 230 return pos.ValidateOrderRegistration(newOrder.TrueRemaining()-order.TrueRemaining(), order.Side) 231 } 232 233 // RegisterOrder updates the potential positions for a submitted order, as though 234 // the order were already accepted. 235 // It returns the updated position. 236 // The margins+risk engines need the updated position to determine whether the 237 // order should be accepted. 238 func (e *Engine) RegisterOrder(ctx context.Context, order *types.Order) *MarketPosition { 239 pos, found := e.positions[order.Party] 240 if !found { 241 pos = NewMarketPosition(order.Party) 242 e.positions[order.Party] = pos 243 // append the pointer to the slice as well 244 e.positionsCpy = append(e.positionsCpy, pos) 245 // create the entry in the open interest map 246 e.partiesHighestVolume[order.Party] = &openVolumeRecord{} 247 } 248 249 pos.RegisterOrder(e.log, order) 250 e.positionUpdated(ctx, pos) 251 return pos 252 } 253 254 // UnregisterOrder undoes the actions of RegisterOrder. It is used when an order 255 // has been rejected by the Risk Engine, or when an order is amended or canceled. 256 func (e *Engine) UnregisterOrder(ctx context.Context, order *types.Order) *MarketPosition { 257 pos, found := e.positions[order.Party] 258 if !found { 259 e.log.Panic("could not find position in engine when unregistering order", 260 logging.Order(*order)) 261 } 262 pos.UnregisterOrder(e.log, order) 263 e.positionUpdated(ctx, pos) 264 return pos 265 } 266 267 // AmendOrder unregisters the original order and then registers the newly amended order 268 // this method is a quicker way of handling separate unregister+register pairs. 269 func (e *Engine) AmendOrder(ctx context.Context, originalOrder, newOrder *types.Order) *MarketPosition { 270 pos, found := e.positions[originalOrder.Party] 271 if !found { 272 e.log.Panic("could not find position in engine when amending order", 273 logging.Order(*originalOrder), 274 logging.Order(*newOrder)) 275 } 276 277 pos.AmendOrder(e.log, originalOrder, newOrder) 278 e.positionUpdated(ctx, pos) 279 return pos 280 } 281 282 func (e *Engine) updatePartiesTradedSize(party string, size uint64) { 283 e.partiesTradedSize[party] = e.partiesTradedSize[party] + size 284 } 285 286 // Update pushes the previous positions on the channel + the updated open volumes of buyer/seller. 287 func (e *Engine) Update(ctx context.Context, trade *types.Trade, passiveOrder, aggressiveOrder *types.Order) []events.MarketPosition { 288 buyer, ok := e.positions[trade.Buyer] 289 if !ok { 290 e.log.Panic("could not find buyer position", 291 logging.Trade(*trade)) 292 } 293 294 seller, ok := e.positions[trade.Seller] 295 if !ok { 296 e.log.Panic("could not find seller position", 297 logging.Trade(*trade)) 298 } 299 300 // now we check if the trade is possible based on the potential positions 301 // this should always be true, no trade can happen without the equivalent 302 // potential position 303 if buyer.buy < int64(trade.Size) { 304 e.log.Panic("trade with a potential buy position < to the trade size", 305 logging.PartyID(trade.Buyer), 306 logging.Int64("potential-buy", buyer.buy), 307 logging.Trade(*trade)) 308 } 309 if seller.sell < int64(trade.Size) { 310 e.log.Panic("trade with a potential sell position < to the trade size", 311 logging.PartyID(trade.Seller), 312 logging.Int64("potential-sell", buyer.sell), 313 logging.Trade(*trade)) 314 } 315 316 // Update long/short actual position for buyer and seller. 317 // The buyer's position increases and the seller's position decreases. 318 buyer.averageEntryPrice = CalcVWAP(buyer.averageEntryPrice, buyer.size, int64(trade.Size), trade.Price) 319 buyer.size += int64(trade.Size) 320 seller.averageEntryPrice = CalcVWAP(seller.averageEntryPrice, -seller.size, int64(trade.Size), trade.Price) 321 seller.size -= int64(trade.Size) 322 323 aggressive := buyer 324 passive := seller 325 if aggressiveOrder.Side == types.SideSell { 326 aggressive = seller 327 passive = buyer 328 } 329 330 // Update potential positions & vwaps. Potential positions decrease for both buyer and seller. 331 aggressive.UpdateOnOrderChange(e.log, aggressiveOrder.Side, aggressiveOrder.Price, trade.Size, false) 332 passive.UpdateOnOrderChange(e.log, passiveOrder.Side, passiveOrder.Price, trade.Size, false) 333 // if the network opens a position here, the price will not be set, which breaks the snapshot 334 // we know the network will trade at the current mark price, and therefore it will hold the position at this price. 335 // so we should just make sure the price is set correctly here. 336 if aggressive.partyID == types.NetworkParty && (aggressive.price == nil || aggressive.price.IsZero()) { 337 aggressive.price = trade.Price.Clone() 338 } 339 340 ret := []events.MarketPosition{ 341 *buyer.Clone(), 342 *seller.Clone(), 343 } 344 345 e.positionUpdated(ctx, buyer) 346 e.positionUpdated(ctx, seller) 347 348 e.partiesHighestVolume[buyer.partyID].RecordLatest(buyer.size) 349 e.updatePartiesTradedSize(buyer.partyID, trade.Size) 350 e.partiesHighestVolume[seller.partyID].RecordLatest(seller.size) 351 e.updatePartiesTradedSize(seller.partyID, trade.Size) 352 353 if e.log.GetLevel() == logging.DebugLevel { 354 e.log.Debug("Positions Updated for trade", 355 logging.Trade(*trade), 356 logging.String("buyer-position", fmt.Sprintf("%+v", buyer)), 357 logging.String("seller-position", fmt.Sprintf("%+v", seller))) 358 } 359 return ret 360 } 361 362 // RemoveDistressed Removes positions for distressed parties, and returns the most up to date positions we have. 363 func (e *Engine) RemoveDistressed(parties []events.MarketPosition) []events.MarketPosition { 364 ret := make([]events.MarketPosition, 0, len(parties)) 365 for _, party := range parties { 366 e.log.Warn("removing party from positions engine", 367 logging.String("party-id", party.Party())) 368 369 party := party.Party() 370 if current, ok := e.positions[party]; ok { 371 ret = append(ret, current) 372 } 373 // remove from the map 374 delete(e.positions, party) 375 delete(e.distressedPos, party) 376 delete(e.partiesHighestVolume, party) 377 // remove from the slice 378 for i := range e.positionsCpy { 379 if e.positionsCpy[i].Party() == party { 380 e.log.Warn("removing party from positions engine (cpy slice)", 381 logging.String("party-id", party)) 382 e.positionsCpy = append(e.positionsCpy[:i], e.positionsCpy[i+1:]...) 383 384 break 385 } 386 } 387 } 388 return ret 389 } 390 391 // UpdateMarkPrice update the mark price on all positions and return a slice 392 // of the updated positions. 393 func (e *Engine) UpdateMarkPrice(markPrice *num.Uint) []events.MarketPosition { 394 for _, pos := range e.positions { 395 pos.price.Set(markPrice) 396 } 397 return e.positionsCpy 398 } 399 400 func (e *Engine) GetOpenInterest() uint64 { 401 openInterest := uint64(0) 402 for _, pos := range e.positions { 403 if pos.size > 0 { 404 openInterest += uint64(pos.size) 405 } 406 } 407 return openInterest 408 } 409 410 func (e *Engine) GetOpenInterestGivenTrades(trades []*types.Trade) uint64 { 411 oi := e.GetOpenInterest() 412 d := int64(0) 413 // Store changes to positions across trades locally 414 posLocal := make(map[string]int64) 415 var ok bool 416 for _, t := range trades { 417 if t.Seller == t.Buyer { 418 // ignore wash trade 419 continue 420 } 421 bPos, sPos := int64(0), int64(0) 422 if bPos, ok = posLocal[t.Buyer]; !ok { 423 if p, ok := e.positions[t.Buyer]; ok { 424 bPos = p.size 425 } 426 } 427 if sPos, ok = posLocal[t.Seller]; !ok { 428 if p, ok := e.positions[t.Seller]; ok { 429 sPos = p.size 430 } 431 } 432 433 bPosNew := bPos + int64(t.Size) 434 sPosNew := sPos - int64(t.Size) 435 posLocal[t.Buyer] = bPosNew 436 posLocal[t.Seller] = sPosNew 437 // Change in open interest due to trades equals change in longs for both parties 438 d += max(0, bPosNew) - max(0, bPos) + max(0, sPosNew) - max(0, sPos) 439 } 440 if d < 0 { 441 return oi - uint64(-d) 442 } 443 return oi + uint64(d) 444 } 445 446 //nolint:unparam 447 func max(a int64, b int64) int64 { 448 if a >= b { 449 return a 450 } 451 return b 452 } 453 454 // Positions is just the logic to update buyer, will eventually return the MarketPosition we need to push. 455 func (e *Engine) Positions() []events.MarketPosition { 456 return e.positionsCpy 457 } 458 459 // MarkDistressed - mark any distressed parties as such, returns the IDs of the parties who weren't distressed before. 460 func (e *Engine) MarkDistressed(closed []string) ([]string, []string) { 461 // assume number of distressed parties is roughly equal, it isn't but should overall reduce reallocs 462 // create a copy, otherwise newly distressed positions are unmarked 463 prevDistressed := make(map[string]struct{}, len(e.distressedPos)) 464 for k := range e.distressedPos { 465 prevDistressed[k] = struct{}{} 466 } 467 nIDs := make([]string, 0, len(closed)) 468 for _, c := range closed { 469 if _, ok := prevDistressed[c]; ok { 470 // this party was already known to be distressed, leave it as-is 471 // delete from this map, so we are left with only parties who were distressed, but aren't anymore 472 delete(prevDistressed, c) 473 continue 474 } 475 // not in distressed map -> get position, mark as distressed 476 e.positions[c].distressed = true 477 // add the new distressed party to the map 478 e.distressedPos[c] = struct{}{} 479 // add the ID to the slice of distressed ID's for the event 480 nIDs = append(nIDs, c) 481 } 482 // if we didn't have any previously distressed parties to update, we're done 483 if len(prevDistressed) == 0 { 484 return nIDs, nil 485 } 486 // mark parties who are no longer distressed accoringly 487 nd := make([]string, 0, len(prevDistressed)) 488 for k := range prevDistressed { 489 // mark as no longer distressed 490 e.positions[k].distressed = false 491 // remove from the map 492 delete(e.distressedPos, k) 493 nd = append(nd, k) 494 } 495 sort.Strings(nIDs) 496 sort.Strings(nd) 497 return nIDs, nd 498 } 499 500 // GetPositionByPartyID - return current position for a given party, it's used in margin checks during auctions 501 // we're not specifying an interface of the return type, and we return a pointer to a copy for the nil. 502 func (e *Engine) GetPositionByPartyID(partyID string) (*MarketPosition, bool) { 503 pos, ok := e.positions[partyID] 504 if !ok { 505 return nil, false 506 } 507 cpy := pos.Clone() 508 // return a copy 509 return cpy, true 510 } 511 512 func (e *Engine) GetPositionsByParty(ids ...string) []events.MarketPosition { 513 ret := make([]events.MarketPosition, 0, len(ids)) 514 for _, id := range ids { 515 pos, ok := e.positions[id] 516 if ok { 517 ret = append(ret, pos.Clone()) 518 } 519 } 520 return ret 521 } 522 523 // Parties returns a list of all the parties in the position engine. 524 func (e *Engine) Parties() []string { 525 parties := make([]string, 0, len(e.positions)) 526 for _, v := range e.positions { 527 parties = append(parties, v.Party()) 528 } 529 return parties 530 } 531 532 func (e *Engine) GetClosedPositions() []events.MarketPosition { 533 out := []events.MarketPosition{} 534 535 for _, v := range e.positions { 536 if v.Closed() { 537 e.remove(v) 538 out = append(out, v) 539 } 540 } 541 542 sort.Slice(out, func(i, j int) bool { return out[i].Party() < out[j].Party() }) 543 544 return out 545 } 546 547 // GetOpenPositionCount returns the number of open positions in the market. 548 func (e *Engine) GetOpenPositionCount() uint64 { 549 var total uint64 550 for _, mp := range e.positionsCpy { 551 if mp.Size() != 0 { 552 total++ 553 } 554 } 555 return total 556 } 557 558 // GetPartiesLowestOpenInterestForEpoch will return a map of parties 559 // and minimal open interest for the epoch, it is meant to be called 560 // at the end of the epoch, and will reset the lowest open interest 561 // of the party to the latest open interest recored for the epoch. 562 func (e *Engine) GetPartiesLowestOpenInterestForEpoch() map[string]uint64 { 563 out := map[string]uint64{} 564 565 for party, oi := range e.partiesHighestVolume { 566 out[party] = oi.Reset() 567 } 568 569 return out 570 } 571 572 // GetPartiesTradedVolumeForEpoch will return a map of parties 573 // and their traded volume recorded during this epoch. 574 func (e *Engine) GetPartiesTradedVolumeForEpoch() (out map[string]uint64) { 575 out, e.partiesTradedSize = e.partiesTradedSize, map[string]uint64{} 576 return 577 } 578 579 func (e *Engine) remove(p *MarketPosition) { 580 // delete from the map first 581 delete(e.positions, p.partyID) 582 delete(e.partiesHighestVolume, p.partyID) 583 delete(e.distressedPos, p.partyID) // in case party was previously flagged as distressed 584 585 // remove from the slice 586 for i := range e.positionsCpy { 587 if e.positionsCpy[i].Party() == p.partyID { 588 e.positionsCpy = append(e.positionsCpy[:i], e.positionsCpy[i+1:]...) 589 break 590 } 591 } 592 } 593 594 // CalcVWAP calculates the volume weighted average entry price. 595 func CalcVWAP(vwap *num.Uint, pos int64, addVolume int64, addPrice *num.Uint) *num.Uint { 596 if pos+addVolume == 0 || addPrice == nil { 597 return num.UintZero() 598 } 599 600 newPos := pos + addVolume 601 if newPos*pos < 0 { // switching from short/long or long/short 602 if newPos < 0 { 603 newPos = -newPos 604 } 605 newAcquiredPositionUint := num.NewUint(uint64(newPos)) 606 if pos < 0 { 607 pos = -pos 608 } 609 posUint := num.NewUint(uint64(pos)) 610 // cost of closing the old position 611 closePositionCost := num.UintZero().Mul(posUint, vwap) 612 // cost of opening the new position 613 openNewPositionCost := num.UintZero().Mul(newAcquiredPositionUint, addPrice) 614 return num.UintZero().Div(num.Sum(closePositionCost, openNewPositionCost), num.Sum(posUint, newAcquiredPositionUint)) 615 } 616 // only decreasing position 617 if (pos > 0 && addVolume < 0) || (pos < 0 && addVolume > 0) { 618 return vwap 619 } 620 // increasing position 621 if pos < 0 { 622 pos = -pos 623 } 624 posUint := num.NewUint(uint64(pos)) 625 if addVolume < 0 { 626 addVolume = -addVolume 627 } 628 addVolumeUint := num.NewUint(uint64(addVolume)) 629 if newPos < 0 { 630 newPos = -newPos 631 } 632 newPosUint := num.NewUint(uint64(newPos)) 633 numerator := num.UintZero().Mul(vwap, posUint).AddSum(num.UintZero().Mul(addPrice, addVolumeUint)) 634 return num.UintZero().Div(numerator, newPosUint) 635 }