code.vegaprotocol.io/vega@v0.79.0/core/settlement/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 settlement 17 18 import ( 19 "context" 20 "sort" 21 "sync" 22 "time" 23 24 "code.vegaprotocol.io/vega/core/events" 25 "code.vegaprotocol.io/vega/core/metrics" 26 "code.vegaprotocol.io/vega/core/products" 27 "code.vegaprotocol.io/vega/core/types" 28 "code.vegaprotocol.io/vega/libs/num" 29 "code.vegaprotocol.io/vega/logging" 30 ) 31 32 // MarketPosition ... 33 // 34 //go:generate go run github.com/golang/mock/mockgen -destination mocks/market_position_mock.go -package mocks code.vegaprotocol.io/vega/core/settlement MarketPosition 35 type MarketPosition interface { 36 Party() string 37 Size() int64 38 Buy() int64 39 Sell() int64 40 Price() *num.Uint 41 BuySumProduct() *num.Uint 42 SellSumProduct() *num.Uint 43 VWBuy() *num.Uint 44 VWSell() *num.Uint 45 ClearPotentials() 46 AverageEntryPrice() *num.Uint 47 } 48 49 // Product ... 50 // 51 //go:generate go run github.com/golang/mock/mockgen -destination mocks/settlement_product_mock.go -package mocks code.vegaprotocol.io/vega/core/settlement Product 52 type Product interface { 53 Settle(*num.Uint, *num.Uint, num.Decimal) (*types.FinancialAmount, bool, num.Decimal, error) 54 GetAsset() string 55 } 56 57 // TimeService. 58 // 59 //go:generate go run github.com/golang/mock/mockgen -destination mocks/time_service_mock.go -package mocks code.vegaprotocol.io/vega/core/settlement TimeService 60 type TimeService interface { 61 GetTimeNow() time.Time 62 } 63 64 // Broker - the event bus broker, send events here. 65 type Broker interface { 66 Send(event events.Event) 67 SendBatch(events []events.Event) 68 } 69 70 // Engine - the main type (of course). 71 type Engine struct { 72 Config 73 log *logging.Logger 74 75 market string 76 product Product 77 settledPosition map[string]int64 // party -> last mark-to-market position 78 mu *sync.Mutex 79 trades map[string][]*settlementTrade 80 timeService TimeService 81 broker Broker 82 positionFactor num.Decimal 83 lastMarkPrice *num.Uint // price at last mark to market 84 } 85 86 // New instantiates a new instance of the settlement engine. 87 func New(log *logging.Logger, conf Config, product Product, market string, timeService TimeService, broker Broker, positionFactor num.Decimal) *Engine { 88 // setup logger 89 log = log.Named(namedLogger) 90 log.SetLevel(conf.Level.Get()) 91 92 return &Engine{ 93 Config: conf, 94 log: log, 95 market: market, 96 product: product, 97 settledPosition: map[string]int64{}, 98 mu: &sync.Mutex{}, 99 trades: map[string][]*settlementTrade{}, 100 timeService: timeService, 101 broker: broker, 102 positionFactor: positionFactor, 103 } 104 } 105 106 func (e *Engine) UpdateProduct(product products.Product) { 107 e.product = product 108 } 109 110 // ReloadConf update the internal configuration of the settlement engined. 111 func (e *Engine) ReloadConf(cfg Config) { 112 e.log.Info("reloading configuration") 113 if e.log.GetLevel() != cfg.Level.Get() { 114 e.log.Info("updating log level", 115 logging.String("old", e.log.GetLevel().String()), 116 logging.String("new", cfg.Level.String()), 117 ) 118 e.log.SetLevel(cfg.Level.Get()) 119 } 120 121 e.Config = cfg 122 } 123 124 // Update merely adds positions to the settlement engine, and won't be useful for a MTM settlement 125 // this function is mainly used for testing, and should be used with extreme caution as a result 126 // perhaps the tests should be refactored to use the Settle call to create positions first. 127 func (e *Engine) Update(positions []events.MarketPosition) { 128 e.mu.Lock() 129 for _, p := range positions { 130 party := p.Party() 131 e.settledPosition[party] = p.Size() 132 e.lastMarkPrice = p.Price() 133 } 134 e.mu.Unlock() 135 } 136 137 // Settle run settlement over all the positions. 138 func (e *Engine) Settle(t time.Time, settlementData *num.Uint) ([]*types.Transfer, *num.Uint, error) { 139 e.log.Debugf("Settling market, closed at %s", t.Format(time.RFC3339)) 140 positions, round, err := e.settleAll(settlementData) 141 if err != nil { 142 e.log.Error( 143 "Something went wrong trying to settle positions", 144 logging.Error(err), 145 ) 146 return nil, nil, err 147 } 148 return positions, round, nil 149 } 150 151 // AddTrade - this call is required to get the correct MTM settlement values 152 // each change in position has to be calculated using the exact price of the trade. 153 func (e *Engine) AddTrade(trade *types.Trade) { 154 e.mu.Lock() 155 defer e.mu.Unlock() 156 var buyerSize, sellerSize int64 157 // checking the len of cd shouldn't be required here, but it is needed in the second if 158 // in case the buyer and seller are one and the same... 159 if cd, ok := e.trades[trade.Buyer]; !ok || len(cd) == 0 { 160 e.trades[trade.Buyer] = []*settlementTrade{} 161 // check if the buyer already has a known position 162 if pos, ok := e.settledPosition[trade.Buyer]; ok { 163 buyerSize = pos 164 } 165 } else { 166 buyerSize = cd[len(cd)-1].newSize 167 } 168 if cd, ok := e.trades[trade.Seller]; !ok || len(cd) == 0 { 169 e.trades[trade.Seller] = []*settlementTrade{} 170 // check if seller has a known position 171 if pos, ok := e.settledPosition[trade.Seller]; ok { 172 sellerSize = pos 173 } 174 } else { 175 sellerSize = cd[len(cd)-1].newSize 176 } 177 size := int64(trade.Size) 178 // the parties both need to get a MTM settlement on the traded volume 179 // and this MTM part has to be based on the _actual_ trade value 180 price := trade.Price.Clone() 181 e.trades[trade.Buyer] = append(e.trades[trade.Buyer], &settlementTrade{ 182 price: price, 183 marketPrice: trade.MarketPrice, 184 size: size, 185 newSize: buyerSize + size, 186 }) 187 e.trades[trade.Seller] = append(e.trades[trade.Seller], &settlementTrade{ 188 price: price.Clone(), 189 marketPrice: trade.MarketPrice, 190 size: -size, 191 newSize: sellerSize - size, 192 }) 193 } 194 195 func (e *Engine) HasTraded() bool { 196 return len(e.trades) > 0 197 } 198 199 func (e *Engine) getFundingTransfer(mtmShare *num.Uint, neg bool, mpos events.MarketPosition, owner string) (*mtmTransfer, bool) { 200 tf := e.getMtmTransfer(mtmShare, neg, mpos, owner) 201 if tf.transfer == nil { 202 tf.transfer = &types.Transfer{ 203 Type: types.TransferTypePerpFundingWin, 204 Owner: owner, 205 Amount: &types.FinancialAmount{ 206 Amount: mtmShare, 207 Asset: e.product.GetAsset(), 208 }, 209 } 210 return tf, false 211 } 212 if tf.transfer.Type == types.TransferTypeMTMLoss { 213 tf.transfer.Type = types.TransferTypePerpFundingLoss 214 } else { 215 tf.transfer.Type = types.TransferTypePerpFundingWin 216 } 217 return tf, true 218 } 219 220 func (e *Engine) getMtmTransfer(mtmShare *num.Uint, neg bool, mpos events.MarketPosition, owner string) *mtmTransfer { 221 if mtmShare.IsZero() { 222 return &mtmTransfer{ 223 MarketPosition: mpos, 224 transfer: nil, 225 } 226 } 227 typ := types.TransferTypeMTMWin 228 if neg { 229 typ = types.TransferTypeMTMLoss 230 } 231 return &mtmTransfer{ 232 MarketPosition: mpos, 233 transfer: &types.Transfer{ 234 Type: typ, 235 Owner: owner, 236 Amount: &types.FinancialAmount{ 237 Amount: mtmShare, 238 Asset: e.product.GetAsset(), 239 }, 240 }, 241 } 242 } 243 244 func (e *Engine) winSocialisationUpdate(transfer *mtmTransfer, amt *num.Uint) { 245 if amt.IsZero() { 246 return 247 } 248 if transfer.transfer == nil { 249 transfer.transfer = &types.Transfer{ 250 Type: types.TransferTypeMTMWin, 251 Owner: transfer.Party(), 252 Amount: &types.FinancialAmount{ 253 Amount: num.UintZero(), 254 Asset: e.product.GetAsset(), 255 }, 256 } 257 } 258 transfer.transfer.Amount.Amount.AddSum(amt) 259 } 260 261 func (e *Engine) SettleMTM(ctx context.Context, markPrice *num.Uint, positions []events.MarketPosition) []events.Transfer { 262 timer := metrics.NewTimeCounter("-", "settlement", "SettleOrder") 263 defer func() { e.lastMarkPrice = markPrice.Clone() }() 264 e.mu.Lock() 265 tCap := e.transferCap(positions) 266 transfers := make([]events.Transfer, 0, tCap) 267 // roughly half of the transfers should be wins, half losses 268 wins := make([]events.Transfer, 0, tCap/2) 269 trades := e.trades 270 e.trades = map[string][]*settlementTrade{} // remove here, once we've processed it all here, we're done 271 evts := make([]events.Event, 0, len(positions)) 272 var ( 273 largestShare *mtmTransfer // pointer to whomever gets the last remaining amount from the loss 274 zeroShares = []*mtmTransfer{} // all zero shares for equal distribution if possible 275 zeroAmts = false 276 mtmDec = num.NewDecimalFromFloat(0) 277 lossTotal = num.UintZero() 278 winTotal = num.UintZero() 279 lossTotalDec = num.NewDecimalFromFloat(0) 280 winTotalDec = num.NewDecimalFromFloat(0) 281 appendLargest = false 282 ) 283 284 // network is treated as a regular party 285 for _, evt := range positions { 286 party := evt.Party() 287 current, lastSettledPrice := e.getOrCreateCurrentPosition(party, evt.Size()) 288 traded, hasTraded := trades[party] 289 tradeset := make([]events.TradeSettlement, 0, len(traded)) 290 // empty position 291 skip := current == 0 && lastSettledPrice.IsZero() && evt.Buy() == 0 && evt.Sell() == 0 292 for _, t := range traded { 293 tradeset = append(tradeset, t) 294 } 295 // create (and add position to buffer) 296 evts = append(evts, events.NewSettlePositionEvent(ctx, party, e.market, evt.Price(), tradeset, e.timeService.GetTimeNow().UnixNano(), e.positionFactor)) 297 // no changes in position, and the MTM price hasn't changed, we don't need to do anything 298 // or an empty position that isn't the result of the party closing itself out 299 if !hasTraded && (lastSettledPrice.EQ(markPrice) || skip) { 300 // no changes in position and markPrice hasn't changed -> nothing needs to be marked 301 continue 302 } 303 // calculate MTM value, we need the signed mark-price, the OLD open position/volume 304 // the new position is either the same, or accounted for by the traded var (added trades) 305 // and the old mark price at which the party held the position 306 // the trades slice contains all trade positions (position changes for the party) 307 // at their exact trade price, so we can MTM that volume correctly, too 308 mtmShare, mtmDShare, neg := calcMTM(markPrice, lastSettledPrice, current, traded, e.positionFactor) 309 // we've marked this party to market, their position can now reflect this 310 e.settledPosition[party] = evt.Size() 311 // we don't want to accidentally MTM a party who closed out completely when they open 312 // a new position at a later point, so remove if size == 0 313 if evt.Size() == 0 && evt.Buy() == 0 && evt.Sell() == 0 { 314 // broke this up into its own func for symmetry 315 e.rmPosition(party) 316 } 317 318 // there's still a subset of potential-only positions, their MTM will be zero 319 // but they don't hold an open position, and are excluded from win-socialisation. 320 skip = !hasTraded && evt.Size() == 0 321 posEvent := newPos(evt, markPrice) 322 mtmTransfer := e.getMtmTransfer(mtmShare.Clone(), neg, posEvent, party) 323 324 if !neg { 325 wins = append(wins, mtmTransfer) 326 winTotal.AddSum(mtmShare) 327 winTotalDec = winTotalDec.Add(mtmDShare) 328 if !skip && mtmShare.IsZero() { 329 zeroShares = append(zeroShares, mtmTransfer) 330 zeroAmts = true 331 } 332 if mtmDShare.GreaterThan(mtmDec) { 333 mtmDec = mtmDShare 334 largestShare = mtmTransfer 335 } 336 } else if mtmShare.IsZero() { 337 // zero value loss 338 wins = append(wins, mtmTransfer) 339 lossTotalDec = lossTotalDec.Add(mtmDShare) 340 } else { 341 transfers = append(transfers, mtmTransfer) 342 lossTotal.AddSum(mtmShare) 343 lossTotalDec = lossTotalDec.Add(mtmDShare) 344 } 345 } 346 // no need for this lock anymore 347 e.mu.Unlock() 348 delta := num.UintZero() 349 if lossTotal.GT(winTotal) { 350 delta.Sub(lossTotal, winTotal) 351 } 352 // make sure largests share is never nil 353 if largestShare == nil { 354 largestShare = &mtmTransfer{ 355 MarketPosition: &npos{ 356 price: markPrice.Clone(), 357 }, 358 } 359 appendLargest = true 360 } 361 if !delta.IsZero() { 362 if zeroAmts { 363 if appendLargest { 364 zeroShares = append(zeroShares, largestShare) 365 } 366 zRound := num.DecimalFromInt64(int64(len(zeroShares))) 367 zeroShares = append(zeroShares, largestShare) 368 // there are more transfers from losses than we pay out to wins, but some winning parties have zero transfers 369 // this delta should == combined win decimals, let's sanity check this! 370 if winTotalDec.LessThan(lossTotalDec) && winTotalDec.LessThan(lossTotalDec.Sub(zRound)) { 371 e.log.Panic("There's less MTM wins than losses, even accounting for decimals", 372 logging.Decimal("total loss", lossTotalDec), 373 logging.Decimal("total wins", winTotalDec), 374 ) 375 } 376 // parties with a zero win transfer should get AT MOST a transfer of value 1 377 // any remainder after that should go to the largest win share, unless we only have parties 378 // with a win share of 0. that shouldn't be possible however, and so we can ignore that case 379 // should this happen at any point, the collateral engine will panic on settlement balance > 0 380 // which is the correct behaviour 381 382 // start distributing the delta 383 one := num.NewUint(1) 384 for _, transfer := range zeroShares { 385 e.winSocialisationUpdate(transfer, one) 386 delta.Sub(delta, one) 387 if delta.IsZero() { 388 break // all done 389 } 390 } 391 } 392 // delta is whatever amount the largest share win party gets, this shouldn't be too much 393 // delta can be zero at this stage, which is fine 394 e.winSocialisationUpdate(largestShare, delta) 395 } 396 // append wins after loss transfers 397 transfers = append(transfers, wins...) 398 if len(transfers) > 0 && appendLargest && largestShare.transfer != nil { 399 transfers = append(transfers, largestShare) 400 } 401 if len(evts) > 0 { 402 e.broker.SendBatch(evts) 403 } 404 timer.EngineTimeCounterAdd() 405 return transfers 406 } 407 408 // RemoveDistressed - remove whatever settlement data we have for distressed parties 409 // they are being closed out, and shouldn't be part of any MTM settlement or closing settlement. 410 func (e *Engine) RemoveDistressed(ctx context.Context, evts []events.Margin) { 411 devts := make([]events.Event, 0, len(evts)) 412 e.mu.Lock() 413 netSize := e.settledPosition[types.NetworkParty] 414 netTradeSize := netSize 415 netTrades := e.trades[types.NetworkParty] 416 if len(netTrades) > 0 { 417 netTradeSize = netTrades[len(netTrades)-1].newSize 418 } 419 for _, v := range evts { 420 key := v.Party() 421 margin := num.Sum(v.MarginBalance(), v.GeneralBalance()) 422 devts = append(devts, events.NewSettleDistressed(ctx, key, e.market, v.Price(), margin, e.timeService.GetTimeNow().UnixNano())) 423 settled := e.settledPosition[key] 424 // first, set the base size for all trades to include the settled position 425 for i, t := range netTrades { 426 t.newSize += settled 427 netTrades[i] = t 428 } 429 // the last trade or settled position should include this value 430 netTradeSize += settled 431 // transfer trades from the distressed party over to the network 432 // update the new sizes accordingly 433 if trades := e.trades[key]; len(trades) > 0 { 434 for _, t := range trades { 435 t.newSize = netTradeSize + t.size 436 netTradeSize += t.size 437 netTrades = append(netTrades, t) 438 } 439 } 440 // the network settled size should be updated 441 netSize += settled 442 delete(e.settledPosition, key) 443 delete(e.trades, key) 444 } 445 e.settledPosition[types.NetworkParty] = netSize 446 if len(netTrades) > 0 { 447 e.trades[types.NetworkParty] = netTrades 448 } 449 e.mu.Unlock() 450 e.broker.SendBatch(devts) 451 } 452 453 // simplified settle call. 454 func (e *Engine) settleAll(settlementData *num.Uint) ([]*types.Transfer, *num.Uint, error) { 455 e.mu.Lock() 456 457 // there should be as many positions as there are parties (obviously) 458 aggregated := make([]*types.Transfer, 0, len(e.settledPosition)) 459 // parties who are in profit should be appended (collect first). 460 // The split won't always be 50-50, but it's a reasonable approximation 461 owed := make([]*types.Transfer, 0, len(e.settledPosition)/2) 462 // ensure we iterate over the positions in the same way by getting all the parties (keys) 463 // and sort them 464 keys := make([]string, 0, len(e.settledPosition)) 465 for p := range e.settledPosition { 466 keys = append(keys, p) 467 } 468 sort.Strings(keys) 469 var delta num.Decimal 470 for _, party := range keys { 471 pos := e.settledPosition[party] 472 // this is possible now, with the Mark to Market stuff, it's possible we've settled any and all positions for a given party 473 if pos == 0 { 474 continue 475 } 476 e.log.Debug("Settling position for party", logging.String("party-id", party)) 477 // @TODO - there was something here... the final amount had to be oracle - market or something 478 amt, neg, rem, err := e.product.Settle(e.lastMarkPrice, settlementData.Clone(), num.DecimalFromInt64(pos).Div(e.positionFactor)) 479 // for now, product.Settle returns the total value, we need to only settle the delta between a parties current position 480 // and the final price coming from the oracle, so oracle_price - mark_price * volume (check with Tamlyn whether this should be absolute or not) 481 if err != nil { 482 e.log.Error( 483 "Failed to settle position for party", 484 logging.String("party-id", party), 485 logging.Error(err), 486 ) 487 e.mu.Unlock() 488 return nil, nil, err 489 } 490 settlePos := &types.Transfer{ 491 Owner: party, 492 Amount: amt, 493 } 494 e.log.Debug( 495 "Settled position for party", 496 logging.String("party-id", party), 497 logging.String("amount", amt.Amount.String()), 498 ) 499 500 if neg { // this is a loss transfer 501 settlePos.Type = types.TransferTypeLoss 502 aggregated = append(aggregated, settlePos) 503 // truncated loss amount will not be transferred to the settlement balance 504 // so remove it from the total delta (aka rounding) 505 delta = delta.Sub(rem) 506 } else { // this is a win transfer 507 settlePos.Type = types.TransferTypeWin 508 owed = append(owed, settlePos) 509 // Truncated win transfer won't be withdrawn from the settlement balance 510 // so add it to the total delta (aka rounding) 511 delta = delta.Add(rem) 512 } 513 } 514 // we only care about the int part 515 round := num.UintZero() 516 // if delta > 0, the settlement account will have a non-zero balance at the end 517 if !delta.IsNegative() { 518 round, _ = num.UintFromDecimal(delta) 519 } 520 // append the parties in profit to the end 521 aggregated = append(aggregated, owed...) 522 e.mu.Unlock() 523 return aggregated, round, nil 524 } 525 526 func (e *Engine) getOrCreateCurrentPosition(party string, size int64) (int64, *num.Uint) { 527 p, ok := e.settledPosition[party] 528 if !ok { 529 e.settledPosition[party] = size 530 return 0, num.UintZero() 531 } 532 return p, e.lastMarkPrice 533 } 534 535 func (e *Engine) HasPosition(party string) bool { 536 _, okPos := e.settledPosition[party] 537 _, okTrades := e.trades[party] 538 539 return okPos && okTrades 540 } 541 542 func (e *Engine) rmPosition(party string) { 543 delete(e.settledPosition, party) 544 } 545 546 // just get the max len as cap. 547 func (e *Engine) transferCap(evts []events.MarketPosition) int { 548 curLen, evtLen := len(e.settledPosition), len(evts) 549 if curLen >= evtLen { 550 return curLen 551 } 552 return evtLen 553 } 554 555 // party.PREV_OPEN_VOLUME * (product.value(current_price) - product.value(prev_mark_price)) + SUM(from i=1 to new_trades.length)( new_trade(i).volume(party) * (product.value(current_price) - new_trade(i).price ) ) 556 // the sum bit is a worry, we do not have all the trades available at this point... 557 558 // calcMTM only handles futures ATM. The formula is simple: 559 // amount = prev_vol * (current_price - prev_mark_price) + SUM(new_trade := range trades)( new_trade(i).volume(party)*(current_price - new_trade(i).price ) 560 // given that the new trades price will equal new mark price, the sum(trades) bit will probably == 0 for nicenet 561 // the size here is the _new_ position size, the price is the OLD price!! 562 func calcMTM(markPrice, price *num.Uint, size int64, trades []*settlementTrade, positionFactor num.Decimal) (*num.Uint, num.Decimal, bool) { 563 delta, sign := num.UintZero().Delta(markPrice, price) 564 // this shouldn't be possible I don't think, but just in case 565 if size < 0 { 566 size = -size 567 // swap sign 568 sign = !sign 569 } 570 mtmShare := delta.Mul(delta, num.NewUint(uint64(size))) 571 for _, c := range trades { 572 delta, neg := num.UintZero().Delta(markPrice, c.price) 573 size := num.NewUint(uint64(c.size)) 574 if c.size < 0 { 575 size = size.SetUint64(uint64(-c.size)) 576 neg = !neg 577 } 578 add := delta.Mul(delta, size) 579 if mtmShare.IsZero() { 580 mtmShare.Set(add) 581 sign = neg 582 } else if neg == sign { 583 // both mtmShare and add are the same sign 584 mtmShare = mtmShare.Add(mtmShare, add) 585 } else if mtmShare.GTE(add) { 586 // regardless of sign, we just have to subtract 587 mtmShare = mtmShare.Sub(mtmShare, add) 588 } else { 589 // add > mtmShare, we don't care about signs here 590 // just subtract mtmShare and switch signs 591 mtmShare = add.Sub(add, mtmShare) 592 sign = neg 593 } 594 } 595 596 // as mtmShare was calculated with the volumes as integers (not decimals in pdp space) we need to divide by position factor 597 decShare := mtmShare.ToDecimal().Div(positionFactor) 598 res, _ := num.UintFromDecimal(decShare) 599 return res, decShare, sign 600 } 601 602 // SettleFundingPeriod takes positions and a funding-payement and returns a slice of transfers. 603 // returns the slice of transfers to perform, and the max remainder on the settlement account due to rounding issues. 604 func (e *Engine) SettleFundingPeriod(ctx context.Context, positions []events.MarketPosition, fundingPayment *num.Int) ([]events.Transfer, *num.Uint) { 605 if fundingPayment.IsZero() || len(positions) == 0 { 606 // nothing to do here 607 return nil, nil 608 } 609 610 // colletral engine expects all the losses before the wins 611 transfers := make([]events.Transfer, 0, len(positions)) 612 wins := make([]events.Transfer, 0, len(positions)) 613 zeroTransfers := make([]events.Transfer, 0, len(positions)/2) 614 totalW, totalL := num.UintZero(), num.UintZero() 615 var delta num.Decimal 616 for _, p := range positions { 617 // per-party cash flow is -openVolume * fundingPayment 618 flow, rem, neg := calcFundingFlow(fundingPayment, p, e.positionFactor) 619 if neg { 620 // amount of loss not collected, this never gets added to the settlement account 621 delta = delta.Sub(rem) 622 } else { 623 // amount of wins never collected, remains in the settlement account 624 delta = delta.Add(rem) 625 } 626 627 if tf, valid := e.getFundingTransfer(flow, neg, p, p.Party()); valid { 628 if tf.transfer.Type == types.TransferTypePerpFundingWin { 629 wins = append(wins, tf) 630 totalW.AddSum(flow) 631 } else { 632 transfers = append(transfers, tf) 633 totalL.AddSum(flow) 634 } 635 } else { 636 // we could use deltas to order these transfers to prioritise the right people 637 zeroTransfers = append(zeroTransfers, tf) 638 } 639 if e.log.IsDebug() { 640 e.log.Debug("cash flow", logging.String("mid", e.market), logging.String("pid", p.Party()), logging.String("flow", flow.String())) 641 } 642 } 643 // account for cases where the winning side never even accounts for an amount of 1 644 if len(wins) == 0 && len(zeroTransfers) > 0 { 645 wins = zeroTransfers 646 } 647 // profit and loss balances out perfectly, or profit > loss 648 if totalL.LTE(totalW) { 649 // this rounding shouldn't be needed, losses will be distributed in their entirety 650 round, _ := num.UintFromDecimal(delta.Abs()) 651 return append(transfers, wins...), round 652 } 653 round := totalL.Sub(totalL, totalW) // loss - win is what will be left over 654 // we have a remainder, make sure it's an expected amount due to rounding 655 if dU, _ := num.UintFromDecimal(delta.Ceil().Abs()); dU.LT(round) { 656 e.log.Panic("Excess loss transfer amount found, cannot be explained by rounding", 657 logging.String("loss-win delta", round.String()), 658 logging.Decimal("rounding delta", delta.Abs()), 659 ) 660 } 661 return append(transfers, wins...), round 662 } 663 664 func calcFundingFlow(fp *num.Int, p events.MarketPosition, posFac num.Decimal) (*num.Uint, num.Decimal, bool) { 665 // -openVolume * fundingPayment 666 // divide by position factor to account for position decimal places 667 flowD := num.DecimalFromInt64(-p.Size()).Mul(num.DecimalFromInt(fp)).Div(posFac) 668 neg := flowD.IsNegative() 669 flow, frac := num.UintFromDecimalWithFraction(flowD.Abs()) 670 return flow, frac, neg 671 }