code.vegaprotocol.io/vega@v0.79.0/core/execution/common/market_activity_tracker.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 common 17 18 import ( 19 "context" 20 "encoding/hex" 21 "fmt" 22 "sort" 23 "strings" 24 "time" 25 26 "code.vegaprotocol.io/vega/core/events" 27 "code.vegaprotocol.io/vega/core/types" 28 "code.vegaprotocol.io/vega/libs/crypto" 29 "code.vegaprotocol.io/vega/libs/num" 30 lproto "code.vegaprotocol.io/vega/libs/proto" 31 "code.vegaprotocol.io/vega/logging" 32 "code.vegaprotocol.io/vega/protos/vega" 33 34 "github.com/shopspring/decimal" 35 ) 36 37 const ( 38 // this the maximum supported window size for any metric. 39 maxWindowSize = 100 40 // to avoid using decimal calculation we're scaling the time weight by the scaling factor and keep working with integers. 41 scalingFactor = int64(10000000) 42 u64ScalingFactor = uint64(scalingFactor) 43 ) 44 45 var ( 46 uScalingFactor = num.NewUint(u64ScalingFactor) 47 dScalingFactor = num.DecimalFromInt64(scalingFactor) 48 ) 49 50 type QuantumGetter interface { 51 GetAssetQuantum(asset string) (num.Decimal, error) 52 GetAllParties() []string 53 } 54 55 type twPosition struct { 56 position uint64 // abs last recorded position 57 t time.Time // time of last recorded position 58 currentEpochTWPosition uint64 // current epoch's running time weighted position (scaled by scaling factor) 59 } 60 61 type twNotional struct { 62 price *num.Uint // last position's price 63 notional *num.Uint // last position's notional value 64 t time.Time // time of last recorded notional position 65 currentEpochTWNotional *num.Uint // current epoch's running time-weighted notional position 66 } 67 68 // marketTracker tracks the activity in the markets in terms of fees and value. 69 type marketTracker struct { 70 asset string 71 makerFeesReceived map[string]*num.Uint 72 makerFeesPaid map[string]*num.Uint 73 lpFees map[string]*num.Uint 74 infraFees map[string]*num.Uint 75 lpPaidFees map[string]*num.Uint 76 buybackFeesPaid map[string]*num.Uint 77 treasuryFeesPaid map[string]*num.Uint 78 markPrice *num.Uint 79 80 notionalVolumeForEpoch *num.Uint 81 82 totalMakerFeesReceived *num.Uint 83 totalMakerFeesPaid *num.Uint 84 totalLpFees *num.Uint 85 86 twPosition map[string]*twPosition 87 partyM2M map[string]num.Decimal 88 partyRealisedReturn map[string]num.Decimal 89 twNotional map[string]*twNotional 90 91 // historical data. 92 epochMakerFeesReceived []map[string]*num.Uint 93 epochMakerFeesPaid []map[string]*num.Uint 94 epochLpFees []map[string]*num.Uint 95 epochTotalMakerFeesReceived []*num.Uint 96 epochTotalMakerFeesPaid []*num.Uint 97 epochTotalLpFees []*num.Uint 98 epochTimeWeightedPosition []map[string]uint64 99 epochTimeWeightedNotional []map[string]*num.Uint 100 epochPartyM2M []map[string]num.Decimal 101 epochPartyRealisedReturn []map[string]num.Decimal 102 epochNotionalVolume []*num.Uint 103 104 valueTraded *num.Uint 105 proposersPaid map[string]struct{} // identifier of payout_asset : funder : markets_in_scope 106 proposer string 107 readyToDelete bool 108 allPartiesCache map[string]struct{} 109 // keys of automated market makers 110 ammPartiesCache map[string]struct{} 111 } 112 113 // MarketActivityTracker tracks how much fees are paid and received for a market by parties by epoch. 114 type MarketActivityTracker struct { 115 log *logging.Logger 116 117 teams Teams 118 balanceChecker AccountBalanceChecker 119 eligibilityChecker EligibilityChecker 120 collateral QuantumGetter 121 122 currentEpoch uint64 123 epochStartTime time.Time 124 minEpochsInTeamForRewardEligibility uint64 125 assetToMarketTrackers map[string]map[string]*marketTracker 126 partyContributionCache map[string][]*types.PartyContributionScore 127 partyTakerNotionalVolume map[string]*num.Uint 128 marketToPartyTakerNotionalVolume map[string]map[string]*num.Uint 129 takerFeesPaidInEpoch []map[string]map[string]map[string]*num.Uint 130 // maps game id to eligible parties over time window 131 eligibilityInEpoch map[string][]map[string]struct{} 132 133 ss *snapshotState 134 broker Broker 135 } 136 137 // NewMarketActivityTracker instantiates the fees tracker. 138 func NewMarketActivityTracker(log *logging.Logger, teams Teams, balanceChecker AccountBalanceChecker, broker Broker, collateral QuantumGetter) *MarketActivityTracker { 139 mat := &MarketActivityTracker{ 140 log: log, 141 balanceChecker: balanceChecker, 142 teams: teams, 143 assetToMarketTrackers: map[string]map[string]*marketTracker{}, 144 partyContributionCache: map[string][]*types.PartyContributionScore{}, 145 partyTakerNotionalVolume: map[string]*num.Uint{}, 146 marketToPartyTakerNotionalVolume: map[string]map[string]*num.Uint{}, 147 ss: &snapshotState{}, 148 takerFeesPaidInEpoch: []map[string]map[string]map[string]*num.Uint{}, 149 eligibilityInEpoch: map[string][]map[string]struct{}{}, 150 broker: broker, 151 collateral: collateral, 152 } 153 154 return mat 155 } 156 157 func (mat *MarketActivityTracker) OnMinEpochsInTeamForRewardEligibilityUpdated(_ context.Context, value int64) error { 158 mat.minEpochsInTeamForRewardEligibility = uint64(value) 159 return nil 160 } 161 162 // NeedsInitialisation is a heuristic migration - if there is no time weighted position data when restoring from snapshot, we will restore 163 // positions from the market. This will only happen on the one time migration from a version preceding the new metrics. If we're already on a 164 // new version, either there are no time-weighted positions and no positions or there are time weighted positions and they will not be restored. 165 func (mat *MarketActivityTracker) NeedsInitialisation(asset, market string) bool { 166 if tracker, ok := mat.getMarketTracker(asset, market); ok { 167 return len(tracker.twPosition) == 0 168 } 169 return false 170 } 171 172 // GetProposer returns the proposer of the market or empty string if the market doesn't exist. 173 func (mat *MarketActivityTracker) GetProposer(market string) string { 174 for _, markets := range mat.assetToMarketTrackers { 175 m, ok := markets[market] 176 if ok { 177 return m.proposer 178 } 179 } 180 return "" 181 } 182 183 func (mat *MarketActivityTracker) SetEligibilityChecker(eligibilityChecker EligibilityChecker) { 184 mat.eligibilityChecker = eligibilityChecker 185 } 186 187 // MarketProposed is called when the market is proposed and adds the market to the tracker. 188 func (mat *MarketActivityTracker) MarketProposed(asset, marketID, proposer string) { 189 markets, ok := mat.assetToMarketTrackers[asset] 190 if ok { 191 if _, ok := markets[marketID]; ok { 192 return 193 } 194 } 195 196 tracker := &marketTracker{ 197 asset: asset, 198 proposer: proposer, 199 proposersPaid: map[string]struct{}{}, 200 readyToDelete: false, 201 valueTraded: num.UintZero(), 202 makerFeesReceived: map[string]*num.Uint{}, 203 makerFeesPaid: map[string]*num.Uint{}, 204 lpFees: map[string]*num.Uint{}, 205 infraFees: map[string]*num.Uint{}, 206 lpPaidFees: map[string]*num.Uint{}, 207 buybackFeesPaid: map[string]*num.Uint{}, 208 treasuryFeesPaid: map[string]*num.Uint{}, 209 notionalVolumeForEpoch: num.UintZero(), 210 totalMakerFeesReceived: num.UintZero(), 211 totalMakerFeesPaid: num.UintZero(), 212 totalLpFees: num.UintZero(), 213 twPosition: map[string]*twPosition{}, 214 partyM2M: map[string]num.Decimal{}, 215 partyRealisedReturn: map[string]num.Decimal{}, 216 twNotional: map[string]*twNotional{}, 217 epochTotalMakerFeesReceived: []*num.Uint{}, 218 epochTotalMakerFeesPaid: []*num.Uint{}, 219 epochTotalLpFees: []*num.Uint{}, 220 epochMakerFeesReceived: []map[string]*num.Uint{}, 221 epochMakerFeesPaid: []map[string]*num.Uint{}, 222 epochLpFees: []map[string]*num.Uint{}, 223 epochPartyM2M: []map[string]num.Decimal{}, 224 epochPartyRealisedReturn: []map[string]decimal.Decimal{}, 225 epochTimeWeightedPosition: []map[string]uint64{}, 226 epochNotionalVolume: []*num.Uint{}, 227 epochTimeWeightedNotional: []map[string]*num.Uint{}, 228 allPartiesCache: map[string]struct{}{}, 229 ammPartiesCache: map[string]struct{}{}, 230 } 231 232 if ok { 233 markets[marketID] = tracker 234 } else { 235 mat.assetToMarketTrackers[asset] = map[string]*marketTracker{marketID: tracker} 236 } 237 } 238 239 // UpdateMarkPrice is called for a futures market when the mark price is recalculated. 240 func (mat *MarketActivityTracker) UpdateMarkPrice(asset, market string, markPrice *num.Uint) { 241 if amt, ok := mat.assetToMarketTrackers[asset]; ok { 242 if mt, ok := amt[market]; ok { 243 mt.markPrice = markPrice.Clone() 244 } 245 } 246 } 247 248 // RestoreMarkPrice is called when a market is loaded from a snapshot and will set the price of the notional to 249 // the mark price is none is set (for migration). 250 func (mat *MarketActivityTracker) RestoreMarkPrice(asset, market string, markPrice *num.Uint) { 251 if amt, ok := mat.assetToMarketTrackers[asset]; ok { 252 if mt, ok := amt[market]; ok { 253 mt.markPrice = markPrice.Clone() 254 for _, twn := range mt.twNotional { 255 if twn.price == nil { 256 twn.price = markPrice.Clone() 257 } 258 } 259 } 260 } 261 } 262 263 func (mat *MarketActivityTracker) PublishGameMetric(ctx context.Context, dispatchStrategy []*vega.DispatchStrategy, now time.Time) { 264 m := map[string]map[string]map[string]*num.Uint{} 265 266 for asset, market := range mat.assetToMarketTrackers { 267 m[asset] = map[string]map[string]*num.Uint{} 268 for mkt, mt := range market { 269 m[asset][mkt] = mt.aggregatedFees() 270 mt.processNotionalAtMilestone(mat.epochStartTime, now) 271 mt.processPositionAtMilestone(mat.epochStartTime, now) 272 mt.processM2MAtMilestone() 273 mt.processPartyRealisedReturnAtMilestone() 274 mt.calcFeesAtMilestone() 275 } 276 } 277 mat.takerFeesPaidInEpoch = append(mat.takerFeesPaidInEpoch, m) 278 for ds := range mat.eligibilityInEpoch { 279 mat.eligibilityInEpoch[ds] = append(mat.eligibilityInEpoch[ds], map[string]struct{}{}) 280 } 281 282 for _, ds := range dispatchStrategy { 283 if ds.Metric == vega.DispatchMetric_DISPATCH_METRIC_VALIDATOR_RANKING || ds.Metric == vega.DispatchMetric_DISPATCH_METRIC_LP_FEES_RECEIVED || 284 ds.Metric == vega.DispatchMetric_DISPATCH_METRIC_MARKET_VALUE { 285 continue 286 } 287 mat.publishMetricForDispatchStrategy(ctx, ds, now) 288 } 289 290 for _, market := range mat.assetToMarketTrackers { 291 for _, mt := range market { 292 mt.epochTimeWeightedNotional = mt.epochTimeWeightedNotional[:len(mt.epochTimeWeightedNotional)-1] 293 mt.epochTimeWeightedPosition = mt.epochTimeWeightedPosition[:len(mt.epochTimeWeightedPosition)-1] 294 mt.epochPartyM2M = mt.epochPartyM2M[:len(mt.epochPartyM2M)-1] 295 mt.epochPartyRealisedReturn = mt.epochPartyRealisedReturn[:len(mt.epochPartyRealisedReturn)-1] 296 mt.epochMakerFeesReceived = mt.epochMakerFeesReceived[:len(mt.epochMakerFeesReceived)-1] 297 mt.epochMakerFeesPaid = mt.epochMakerFeesPaid[:len(mt.epochMakerFeesPaid)-1] 298 mt.epochLpFees = mt.epochLpFees[:len(mt.epochLpFees)-1] 299 mt.epochTotalMakerFeesReceived = mt.epochTotalMakerFeesReceived[:len(mt.epochTotalMakerFeesReceived)-1] 300 mt.epochTotalMakerFeesPaid = mt.epochTotalMakerFeesPaid[:len(mt.epochTotalMakerFeesPaid)-1] 301 mt.epochTotalLpFees = mt.epochTotalLpFees[:len(mt.epochTotalLpFees)-1] 302 } 303 } 304 mat.takerFeesPaidInEpoch = mat.takerFeesPaidInEpoch[:len(mat.takerFeesPaidInEpoch)-1] 305 mat.partyContributionCache = map[string][]*types.PartyContributionScore{} 306 for ds := range mat.eligibilityInEpoch { 307 mat.eligibilityInEpoch[ds] = mat.eligibilityInEpoch[ds][:len(mat.eligibilityInEpoch)-1] 308 } 309 } 310 311 func (mat *MarketActivityTracker) publishMetricForDispatchStrategy(ctx context.Context, ds *vega.DispatchStrategy, now time.Time) { 312 if ds.EntityScope == vega.EntityScope_ENTITY_SCOPE_INDIVIDUALS { 313 partyScores := mat.CalculateMetricForIndividuals(ctx, ds) 314 gs := events.NewPartyGameScoresEvent(ctx, int64(mat.currentEpoch), getGameID(ds), now, partyScores) 315 mat.broker.Send(gs) 316 } else { 317 teamScores, partyScores := mat.CalculateMetricForTeams(ctx, ds) 318 gs := events.NewTeamGameScoresEvent(ctx, int64(mat.currentEpoch), getGameID(ds), now, teamScores, partyScores) 319 mat.broker.Send(gs) 320 } 321 } 322 323 // AddValueTraded records the value of a trade done in the given market. 324 func (mat *MarketActivityTracker) AddValueTraded(asset, marketID string, value *num.Uint) { 325 markets, ok := mat.assetToMarketTrackers[asset] 326 if !ok || markets[marketID] == nil { 327 return 328 } 329 markets[marketID].valueTraded.AddSum(value) 330 } 331 332 // AddAMMSubAccount records sub account entries for AMM in given market. 333 func (mat *MarketActivityTracker) AddAMMSubAccount(asset, marketID, subAccount string) { 334 markets, ok := mat.assetToMarketTrackers[asset] 335 if !ok || markets[marketID] == nil { 336 return 337 } 338 markets[marketID].ammPartiesCache[subAccount] = struct{}{} 339 } 340 341 // RemoveAMMParty removes amm party entries for AMM in given market. 342 func (mat *MarketActivityTracker) RemoveAMMParty(asset, marketID, ammParty string) { 343 markets, ok := mat.assetToMarketTrackers[asset] 344 if !ok || markets[marketID] == nil { 345 return 346 } 347 delete(markets[marketID].ammPartiesCache, ammParty) 348 } 349 350 // GetMarketsWithEligibleProposer gets all the markets within the given asset (or just all the markets in scope passed as a parameter) that 351 // are eligible for proposer bonus. 352 func (mat *MarketActivityTracker) GetMarketsWithEligibleProposer(asset string, markets []string, payoutAsset string, funder string, eligibleKeys []string) []*types.MarketContributionScore { 353 eligibleKeySet := make(map[string]struct{}, len(eligibleKeys)) 354 for _, ek := range eligibleKeys { 355 eligibleKeySet[ek] = struct{}{} 356 } 357 358 var mkts []string 359 if len(markets) > 0 { 360 mkts = markets 361 } else { 362 if len(asset) > 0 { 363 for m := range mat.assetToMarketTrackers[asset] { 364 mkts = append(mkts, m) 365 } 366 } else { 367 for _, markets := range mat.assetToMarketTrackers { 368 for mkt := range markets { 369 mkts = append(mkts, mkt) 370 } 371 } 372 } 373 sort.Strings(mkts) 374 } 375 376 assets := []string{} 377 if len(asset) > 0 { 378 assets = append(assets, asset) 379 } else { 380 for k := range mat.assetToMarketTrackers { 381 assets = append(assets, k) 382 } 383 sort.Strings(assets) 384 } 385 386 eligibleMarkets := []string{} 387 for _, a := range assets { 388 for _, v := range mkts { 389 if t, ok := mat.getMarketTracker(a, v); ok && (len(asset) == 0 || t.asset == asset) && mat.IsMarketEligibleForBonus(a, v, payoutAsset, markets, funder) { 390 proposer := mat.GetProposer(v) 391 if _, ok := eligibleKeySet[proposer]; len(eligibleKeySet) == 0 || ok { 392 eligibleMarkets = append(eligibleMarkets, v) 393 } 394 } 395 } 396 } 397 398 if len(eligibleMarkets) <= 0 { 399 return nil 400 } 401 scores := make([]*types.MarketContributionScore, 0, len(eligibleMarkets)) 402 numMarkets := num.DecimalFromInt64(int64(len(eligibleMarkets))) 403 totalScore := num.DecimalZero() 404 for _, v := range eligibleMarkets { 405 score := num.DecimalFromInt64(1).Div(numMarkets) 406 scores = append(scores, &types.MarketContributionScore{ 407 Asset: asset, 408 Market: v, 409 Score: score, 410 Metric: vega.DispatchMetric_DISPATCH_METRIC_MARKET_VALUE, 411 }) 412 totalScore = totalScore.Add(score) 413 } 414 415 mat.clipScoresAt1(scores, totalScore) 416 scoresString := "" 417 418 for _, mcs := range scores { 419 scoresString += mcs.Market + ":" + mcs.Score.String() + "," 420 } 421 mat.log.Info("markets contributions:", logging.String("asset", asset), logging.String("metric", vega.DispatchMetric_name[int32(vega.DispatchMetric_DISPATCH_METRIC_MARKET_VALUE)]), logging.String("market-scores", scoresString[:len(scoresString)-1])) 422 423 return scores 424 } 425 426 func (mat *MarketActivityTracker) clipScoresAt1(scores []*types.MarketContributionScore, totalScore num.Decimal) { 427 if totalScore.LessThanOrEqual(num.DecimalFromInt64(1)) { 428 return 429 } 430 // if somehow the total scores are > 1 clip the largest one 431 sort.SliceStable(scores, func(i, j int) bool { return scores[i].Score.GreaterThan(scores[j].Score) }) 432 delta := totalScore.Sub(num.DecimalFromInt64(1)) 433 scores[0].Score = num.MaxD(num.DecimalZero(), scores[0].Score.Sub(delta)) 434 // sort by market id for consistency 435 sort.SliceStable(scores, func(i, j int) bool { return scores[i].Market < scores[j].Market }) 436 } 437 438 // MarkPaidProposer marks the proposer of the market as having been paid proposer bonus. 439 func (mat *MarketActivityTracker) MarkPaidProposer(asset, market, payoutAsset string, marketsInScope []string, funder string) { 440 markets := strings.Join(marketsInScope[:], "_") 441 if len(marketsInScope) == 0 { 442 markets = "all" 443 } 444 445 if mts, ok := mat.assetToMarketTrackers[asset]; ok { 446 t, ok := mts[market] 447 if !ok { 448 return 449 } 450 ID := fmt.Sprintf("%s:%s:%s", payoutAsset, funder, markets) 451 if _, ok := t.proposersPaid[ID]; !ok { 452 t.proposersPaid[ID] = struct{}{} 453 } 454 } 455 } 456 457 // IsMarketEligibleForBonus returns true is the market proposer is eligible for market proposer bonus and has not been 458 // paid for the combination of payout asset and marketsInScope. 459 // The proposer is not market as having been paid until told to do so (if actually paid). 460 func (mat *MarketActivityTracker) IsMarketEligibleForBonus(asset, market, payoutAsset string, marketsInScope []string, funder string) bool { 461 t, ok := mat.getMarketTracker(asset, market) 462 if !ok { 463 return false 464 } 465 466 markets := strings.Join(marketsInScope[:], "_") 467 if len(marketsInScope) == 0 { 468 markets = "all" 469 } 470 471 marketIsInScope := false 472 for _, v := range marketsInScope { 473 if v == market { 474 marketIsInScope = true 475 break 476 } 477 } 478 479 if len(marketsInScope) == 0 { 480 markets = "all" 481 marketIsInScope = true 482 } 483 484 if !marketIsInScope { 485 return false 486 } 487 488 ID := fmt.Sprintf("%s:%s:%s", payoutAsset, funder, markets) 489 _, paid := t.proposersPaid[ID] 490 491 return !paid && mat.eligibilityChecker.IsEligibleForProposerBonus(market, t.valueTraded) 492 } 493 494 // GetAllMarketIDs returns all the current market IDs. 495 func (mat *MarketActivityTracker) GetAllMarketIDs() []string { 496 mIDs := []string{} 497 for _, markets := range mat.assetToMarketTrackers { 498 for k := range markets { 499 mIDs = append(mIDs, k) 500 } 501 } 502 503 sort.Strings(mIDs) 504 return mIDs 505 } 506 507 // MarketTrackedForAsset returns whether the given market is seen to have the given asset by the tracker. 508 func (mat *MarketActivityTracker) MarketTrackedForAsset(market, asset string) bool { 509 if markets, ok := mat.assetToMarketTrackers[asset]; ok { 510 if _, ok = markets[market]; ok { 511 return true 512 } 513 } 514 return false 515 } 516 517 // RemoveMarket is called when the market is removed from the network. It is not immediately removed to give a chance for rewards to be paid at the end of the epoch for activity during the epoch. 518 // Instead it is marked for removal and will be removed at the beginning of the next epoch. 519 func (mat *MarketActivityTracker) RemoveMarket(asset, marketID string) { 520 if markets, ok := mat.assetToMarketTrackers[asset]; ok { 521 if m, ok := markets[marketID]; ok { 522 m.readyToDelete = true 523 } 524 } 525 } 526 527 func (mt *marketTracker) aggregatedFees() map[string]*num.Uint { 528 totalFees := map[string]*num.Uint{} 529 fees := []map[string]*num.Uint{mt.infraFees, mt.lpPaidFees, mt.makerFeesPaid, mt.buybackFeesPaid, mt.treasuryFeesPaid} 530 for _, fee := range fees { 531 for party, paid := range fee { 532 if _, ok := totalFees[party]; !ok { 533 totalFees[party] = num.UintZero() 534 } 535 totalFees[party].AddSum(paid) 536 } 537 } 538 return totalFees 539 } 540 541 // OnEpochEvent is called when the state of the epoch changes, we only care about new epochs starting. 542 func (mat *MarketActivityTracker) OnEpochEvent(ctx context.Context, epoch types.Epoch) { 543 if epoch.Action == vega.EpochAction_EPOCH_ACTION_START { 544 mat.epochStartTime = epoch.StartTime 545 mat.partyContributionCache = map[string][]*types.PartyContributionScore{} 546 mat.clearDeletedMarkets() 547 mat.clearNotionalTakerVolume() 548 } else if epoch.Action == vega.EpochAction_EPOCH_ACTION_END { 549 m := map[string]map[string]map[string]*num.Uint{} 550 for asset, market := range mat.assetToMarketTrackers { 551 m[asset] = map[string]map[string]*num.Uint{} 552 for mkt, mt := range market { 553 m[asset][mkt] = mt.aggregatedFees() 554 mt.processNotionalEndOfEpoch(epoch.StartTime, epoch.EndTime) 555 mt.processPositionEndOfEpoch(epoch.StartTime, epoch.EndTime) 556 mt.processM2MEndOfEpoch() 557 mt.processPartyRealisedReturnOfEpoch() 558 mt.clearFeeActivity() 559 if len(mt.epochNotionalVolume) == maxWindowSize { 560 mt.epochNotionalVolume = mt.epochNotionalVolume[1:] 561 } 562 mt.epochNotionalVolume = append(mt.epochNotionalVolume, mt.notionalVolumeForEpoch) 563 mt.notionalVolumeForEpoch = num.UintZero() 564 } 565 } 566 if len(mat.takerFeesPaidInEpoch) == maxWindowSize { 567 mat.takerFeesPaidInEpoch = mat.takerFeesPaidInEpoch[1:] 568 } 569 mat.takerFeesPaidInEpoch = append(mat.takerFeesPaidInEpoch, m) 570 for ds := range mat.eligibilityInEpoch { 571 mat.eligibilityInEpoch[ds] = append(mat.eligibilityInEpoch[ds], map[string]struct{}{}) 572 } 573 } 574 mat.currentEpoch = epoch.Seq 575 } 576 577 func (mat *MarketActivityTracker) clearDeletedMarkets() { 578 for _, mts := range mat.assetToMarketTrackers { 579 for k, mt := range mts { 580 if mt.readyToDelete { 581 delete(mts, k) 582 } 583 } 584 } 585 } 586 587 func (mat *MarketActivityTracker) GetNotionalVolumeForAsset(asset string, markets []string, windowSize int) *num.Uint { 588 total := num.UintZero() 589 trackers, ok := mat.assetToMarketTrackers[asset] 590 if !ok { 591 return total 592 } 593 marketsInScope := map[string]struct{}{} 594 for _, mkt := range markets { 595 marketsInScope[mkt] = struct{}{} 596 } 597 if len(markets) == 0 { 598 for mkt := range trackers { 599 marketsInScope[mkt] = struct{}{} 600 } 601 } 602 for mkt := range marketsInScope { 603 for i := 0; i < windowSize; i++ { 604 idx := len(trackers[mkt].epochNotionalVolume) - i - 1 605 if idx < 0 { 606 break 607 } 608 total.AddSum(trackers[mkt].epochNotionalVolume[idx]) 609 } 610 } 611 return total 612 } 613 614 func (mat *MarketActivityTracker) CalculateTotalMakerContributionInQuantum(windowSize int) (map[string]*num.Uint, map[string]num.Decimal) { 615 m := map[string]*num.Uint{} 616 total := num.UintZero() 617 for ast, trackers := range mat.assetToMarketTrackers { 618 quantum, err := mat.collateral.GetAssetQuantum(ast) 619 if err != nil { 620 continue 621 } 622 for _, trckr := range trackers { 623 for i := 0; i < windowSize; i++ { 624 idx := len(trckr.epochMakerFeesReceived) - i - 1 625 if idx < 0 { 626 break 627 } 628 partyFees := trckr.epochMakerFeesReceived[len(trckr.epochMakerFeesReceived)-i-1] 629 for party, fees := range partyFees { 630 if _, ok := m[party]; !ok { 631 m[party] = num.UintZero() 632 } 633 feesInQunatum, overflow := num.UintFromDecimal(fees.ToDecimal().Div(quantum)) 634 if overflow { 635 continue 636 } 637 m[party].AddSum(feesInQunatum) 638 total.AddSum(feesInQunatum) 639 } 640 } 641 } 642 } 643 if total.IsZero() { 644 return m, map[string]decimal.Decimal{} 645 } 646 totalFrac := num.DecimalZero() 647 fractions := []*types.PartyContributionScore{} 648 for p, f := range m { 649 frac := f.ToDecimal().Div(total.ToDecimal()) 650 fractions = append(fractions, &types.PartyContributionScore{Party: p, Score: frac}) 651 totalFrac = totalFrac.Add(frac) 652 } 653 capAtOne(fractions, totalFrac) 654 fracMap := make(map[string]num.Decimal, len(fractions)) 655 for _, partyFraction := range fractions { 656 fracMap[partyFraction.Party] = partyFraction.Score 657 } 658 return m, fracMap 659 } 660 661 func capAtOne(partyFractions []*types.PartyContributionScore, total num.Decimal) { 662 if total.LessThanOrEqual(num.DecimalOne()) { 663 return 664 } 665 666 sort.SliceStable(partyFractions, func(i, j int) bool { return partyFractions[i].Score.GreaterThan(partyFractions[j].Score) }) 667 delta := total.Sub(num.DecimalFromInt64(1)) 668 partyFractions[0].Score = num.MaxD(num.DecimalZero(), partyFractions[0].Score.Sub(delta)) 669 } 670 671 func (mt *marketTracker) calcFeesAtMilestone() { 672 mt.epochMakerFeesReceived = append(mt.epochMakerFeesReceived, mt.makerFeesReceived) 673 mt.epochMakerFeesPaid = append(mt.epochMakerFeesPaid, mt.makerFeesPaid) 674 mt.epochLpFees = append(mt.epochLpFees, mt.lpFees) 675 mt.epochTotalMakerFeesReceived = append(mt.epochTotalMakerFeesReceived, mt.totalMakerFeesReceived) 676 mt.epochTotalMakerFeesPaid = append(mt.epochTotalMakerFeesPaid, mt.totalMakerFeesPaid) 677 mt.epochTotalLpFees = append(mt.epochTotalLpFees, mt.totalLpFees) 678 } 679 680 // clearFeeActivity is called at the end of the epoch. It deletes markets that are pending to be removed and resets the fees paid for the epoch. 681 func (mt *marketTracker) clearFeeActivity() { 682 if len(mt.epochMakerFeesReceived) == maxWindowSize { 683 mt.epochMakerFeesReceived = mt.epochMakerFeesReceived[1:] 684 mt.epochMakerFeesPaid = mt.epochMakerFeesPaid[1:] 685 mt.epochLpFees = mt.epochLpFees[1:] 686 mt.epochTotalMakerFeesReceived = mt.epochTotalMakerFeesReceived[1:] 687 mt.epochTotalMakerFeesPaid = mt.epochTotalMakerFeesPaid[1:] 688 mt.epochTotalLpFees = mt.epochTotalLpFees[1:] 689 } 690 mt.epochMakerFeesReceived = append(mt.epochMakerFeesReceived, mt.makerFeesReceived) 691 mt.epochMakerFeesPaid = append(mt.epochMakerFeesPaid, mt.makerFeesPaid) 692 mt.epochLpFees = append(mt.epochLpFees, mt.lpFees) 693 mt.makerFeesReceived = map[string]*num.Uint{} 694 mt.makerFeesPaid = map[string]*num.Uint{} 695 mt.lpFees = map[string]*num.Uint{} 696 mt.infraFees = map[string]*num.Uint{} 697 mt.lpPaidFees = map[string]*num.Uint{} 698 mt.treasuryFeesPaid = map[string]*num.Uint{} 699 mt.buybackFeesPaid = map[string]*num.Uint{} 700 701 mt.epochTotalMakerFeesReceived = append(mt.epochTotalMakerFeesReceived, mt.totalMakerFeesReceived) 702 mt.epochTotalMakerFeesPaid = append(mt.epochTotalMakerFeesPaid, mt.totalMakerFeesPaid) 703 mt.epochTotalLpFees = append(mt.epochTotalLpFees, mt.totalLpFees) 704 mt.totalMakerFeesReceived = num.UintZero() 705 mt.totalMakerFeesPaid = num.UintZero() 706 mt.totalLpFees = num.UintZero() 707 } 708 709 // UpdateFeesFromTransfers takes a slice of transfers and if they represent fees it updates the market fee tracker. 710 // market is guaranteed to exist in the mapping as it is added when proposed. 711 func (mat *MarketActivityTracker) UpdateFeesFromTransfers(asset, market string, transfers []*types.Transfer) { 712 for _, t := range transfers { 713 mt, ok := mat.getMarketTracker(asset, market) 714 if !ok { 715 continue 716 } 717 mt.allPartiesCache[t.Owner] = struct{}{} 718 switch t.Type { 719 case types.TransferTypeMakerFeePay: 720 mat.addFees(mt.makerFeesPaid, t.Owner, t.Amount.Amount, mt.totalMakerFeesPaid) 721 case types.TransferTypeMakerFeeReceive: 722 mat.addFees(mt.makerFeesReceived, t.Owner, t.Amount.Amount, mt.totalMakerFeesReceived) 723 case types.TransferTypeLiquidityFeeNetDistribute, types.TransferTypeSlaPerformanceBonusDistribute: 724 mat.addFees(mt.lpFees, t.Owner, t.Amount.Amount, mt.totalLpFees) 725 case types.TransferTypeInfrastructureFeePay: 726 mat.addFees(mt.infraFees, t.Owner, t.Amount.Amount, num.UintZero()) 727 case types.TransferTypeLiquidityFeePay: 728 mat.addFees(mt.lpPaidFees, t.Owner, t.Amount.Amount, num.UintZero()) 729 case types.TransferTypeBuyBackFeePay: 730 mat.addFees(mt.buybackFeesPaid, t.Owner, t.Amount.Amount, num.UintZero()) 731 case types.TransferTypeTreasuryPay: 732 mat.addFees(mt.treasuryFeesPaid, t.Owner, t.Amount.Amount, num.UintZero()) 733 case types.TransferTypeHighMakerRebateReceive: 734 // we count high maker fee receive as maker fees for that purpose. 735 mat.addFees(mt.makerFeesReceived, t.Owner, t.Amount.Amount, mt.totalMakerFeesReceived) 736 default: 737 } 738 } 739 } 740 741 // addFees records fees paid/received in a given metric to a given party. 742 func (mat *MarketActivityTracker) addFees(m map[string]*num.Uint, party string, amount *num.Uint, total *num.Uint) { 743 if _, ok := m[party]; !ok { 744 m[party] = amount.Clone() 745 total.AddSum(amount) 746 return 747 } 748 m[party].AddSum(amount) 749 total.AddSum(amount) 750 } 751 752 // getMarketTracker finds the market tracker for a market if one exists (one must exist if the market is active). 753 func (mat *MarketActivityTracker) getMarketTracker(asset, market string) (*marketTracker, bool) { 754 if _, ok := mat.assetToMarketTrackers[asset]; !ok { 755 return nil, false 756 } 757 tracker, ok := mat.assetToMarketTrackers[asset][market] 758 if !ok { 759 return nil, false 760 } 761 return tracker, true 762 } 763 764 // RestorePosition restores a position as if it were acquired at the beginning of the epoch. This is purely for migration from an old version. 765 func (mat *MarketActivityTracker) RestorePosition(asset, party, market string, pos int64, price *num.Uint, positionFactor num.Decimal) { 766 mat.RecordPosition(asset, party, market, pos, price, positionFactor, mat.epochStartTime) 767 } 768 769 // RecordPosition passes the position of the party in the asset/market to the market tracker to be recorded. 770 func (mat *MarketActivityTracker) RecordPosition(asset, party, market string, pos int64, price *num.Uint, positionFactor num.Decimal, time time.Time) { 771 if tracker, ok := mat.getMarketTracker(asset, market); ok { 772 tracker.allPartiesCache[party] = struct{}{} 773 absPos := uint64(0) 774 if pos > 0 { 775 absPos = uint64(pos) 776 } else if pos < 0 { 777 absPos = uint64(-pos) 778 } 779 notional, _ := num.UintFromDecimal(num.UintZero().Mul(num.NewUint(absPos), price).ToDecimal().Div(positionFactor)) 780 tracker.recordPosition(party, absPos, positionFactor, time, mat.epochStartTime) 781 tracker.recordNotional(party, notional, price, time, mat.epochStartTime) 782 } 783 } 784 785 // RecordRealisedPosition updates the market tracker on decreased position. 786 func (mat *MarketActivityTracker) RecordRealisedPosition(asset, party, market string, positionDecrease num.Decimal) { 787 if tracker, ok := mat.getMarketTracker(asset, market); ok { 788 tracker.allPartiesCache[party] = struct{}{} 789 tracker.recordRealisedPosition(party, positionDecrease) 790 } 791 } 792 793 // RecordM2M passes the mark to market win/loss transfer amount to the asset/market tracker to be recorded. 794 func (mat *MarketActivityTracker) RecordM2M(asset, party, market string, amount num.Decimal) { 795 if tracker, ok := mat.getMarketTracker(asset, market); ok { 796 tracker.allPartiesCache[party] = struct{}{} 797 tracker.recordM2M(party, amount) 798 } 799 } 800 801 // RecordFundingPayment passes the mark to market win/loss transfer amount to the asset/market tracker to be recorded. 802 func (mat *MarketActivityTracker) RecordFundingPayment(asset, party, market string, amount num.Decimal) { 803 if tracker, ok := mat.getMarketTracker(asset, market); ok { 804 tracker.allPartiesCache[party] = struct{}{} 805 tracker.recordFundingPayment(party, amount) 806 } 807 } 808 809 func (mat *MarketActivityTracker) filterParties( 810 asset string, 811 mkts []string, 812 cacheFilter func(*marketTracker) map[string]struct{}, 813 ) map[string]struct{} { 814 parties := map[string]struct{}{} 815 includedMarkets := mkts 816 if len(mkts) == 0 { 817 includedMarkets = mat.GetAllMarketIDs() 818 } 819 assets := []string{} 820 if len(asset) == 0 { 821 assets = make([]string, 0, len(mat.assetToMarketTrackers)) 822 for k := range mat.assetToMarketTrackers { 823 assets = append(assets, k) 824 } 825 sort.Strings(assets) 826 } else { 827 assets = append(assets, asset) 828 } 829 830 if len(includedMarkets) > 0 { 831 for _, ast := range assets { 832 trackers, ok := mat.assetToMarketTrackers[ast] 833 if !ok { 834 continue 835 } 836 for _, mkt := range includedMarkets { 837 mt, ok := trackers[mkt] 838 if !ok { 839 continue 840 } 841 mktParties := cacheFilter(mt) 842 for k := range mktParties { 843 parties[k] = struct{}{} 844 } 845 } 846 } 847 } 848 return parties 849 } 850 851 func (mat *MarketActivityTracker) getAllParties(asset string, mkts []string) map[string]struct{} { 852 return mat.filterParties(asset, mkts, func(mt *marketTracker) map[string]struct{} { 853 return mt.allPartiesCache 854 }) 855 } 856 857 func (mat *MarketActivityTracker) GetAllAMMParties(asset string, mkts []string) map[string]struct{} { 858 return mat.filterParties(asset, mkts, func(mt *marketTracker) map[string]struct{} { 859 return mt.ammPartiesCache 860 }) 861 } 862 863 func (mat *MarketActivityTracker) getPartiesInScope(ds *vega.DispatchStrategy) []string { 864 var parties []string 865 if ds.IndividualScope == vega.IndividualScope_INDIVIDUAL_SCOPE_IN_TEAM { 866 parties = mat.teams.GetAllPartiesInTeams(mat.minEpochsInTeamForRewardEligibility) 867 } else if ds.IndividualScope == vega.IndividualScope_INDIVIDUAL_SCOPE_ALL { 868 if ds.Metric == vega.DispatchMetric_DISPATCH_METRIC_ELIGIBLE_ENTITIES { 869 notionalReq := num.UintZero() 870 stakingReq := num.UintZero() 871 if len(ds.NotionalTimeWeightedAveragePositionRequirement) > 0 { 872 notionalReq = num.MustUintFromString(ds.NotionalTimeWeightedAveragePositionRequirement, 10) 873 } 874 if len(ds.StakingRequirement) > 0 { 875 stakingReq = num.MustUintFromString(ds.StakingRequirement, 10) 876 } 877 if !notionalReq.IsZero() { 878 parties = sortedK(mat.getAllParties(ds.AssetForMetric, ds.Markets)) 879 } else if !stakingReq.IsZero() { 880 parties = mat.balanceChecker.GetAllStakingParties() 881 } else { 882 parties = mat.collateral.GetAllParties() 883 } 884 } else { 885 parties = sortedK(mat.getAllParties(ds.AssetForMetric, ds.Markets)) 886 } 887 } else if ds.IndividualScope == vega.IndividualScope_INDIVIDUAL_SCOPE_NOT_IN_TEAM { 888 parties = sortedK(excludePartiesInTeams(mat.getAllParties(ds.AssetForMetric, ds.Markets), mat.teams.GetAllPartiesInTeams(mat.minEpochsInTeamForRewardEligibility))) 889 } else if ds.IndividualScope == vega.IndividualScope_INDIVIDUAL_SCOPE_AMM { 890 parties = sortedK(mat.GetAllAMMParties(ds.AssetForMetric, ds.Markets)) 891 } 892 if len(ds.EligibleKeys) > 0 { 893 eligibleParties := make([]string, 0, len(parties)) 894 ep := make(map[string]struct{}, len(ds.EligibleKeys)) 895 for _, ek := range ds.EligibleKeys { 896 ep[ek] = struct{}{} 897 } 898 for _, pp := range parties { 899 if _, ok := ep[pp]; ok { 900 eligibleParties = append(eligibleParties, pp) 901 } 902 } 903 parties = eligibleParties 904 } 905 return parties 906 } 907 908 func getGameID(ds *vega.DispatchStrategy) string { 909 p, _ := lproto.Marshal(ds) 910 return hex.EncodeToString(crypto.Hash(p)) 911 } 912 913 func (mat *MarketActivityTracker) GameFinished(gameID string) { 914 delete(mat.eligibilityInEpoch, gameID) 915 } 916 917 // CalculateMetricForIndividuals calculates the metric corresponding to the dispatch strategy and returns a slice of the contribution scores of the parties. 918 // Markets in scope are the ones passed in the dispatch strategy if any or all available markets for the asset for metric. 919 // Parties in scope depend on the `IndividualScope_INDIVIDUAL_SCOPE_IN_TEAM` and can include all parties, only those in teams, and only those not in teams. 920 func (mat *MarketActivityTracker) CalculateMetricForIndividuals(ctx context.Context, ds *vega.DispatchStrategy) []*types.PartyContributionScore { 921 hash := getGameID(ds) 922 if pc, ok := mat.partyContributionCache[hash]; ok { 923 return pc 924 } 925 926 parties := mat.getPartiesInScope(ds) 927 stakingRequirement, _ := num.UintFromString(ds.StakingRequirement, 10) 928 notionalRequirement, _ := num.UintFromString(ds.NotionalTimeWeightedAveragePositionRequirement, 10) 929 interval := int32(1) 930 if ds.TransferInterval != nil { 931 interval = *ds.TransferInterval 932 } 933 partyContributions := mat.calculateMetricForIndividuals(ctx, ds.AssetForMetric, parties, ds.Markets, ds.Metric, stakingRequirement, notionalRequirement, int(ds.WindowLength), hash, interval) 934 935 // we do this calculation at the end of the epoch and clear it in the beginning of the next epoch, i.e. within the same block therefore it saves us 936 // redundant calculation and has no snapshot implication 937 mat.partyContributionCache[hash] = partyContributions 938 return partyContributions 939 } 940 941 // CalculateMetricForTeams calculates the metric for teams and their respective team members for markets in scope of the dispatch strategy. 942 func (mat *MarketActivityTracker) CalculateMetricForTeams(ctx context.Context, ds *vega.DispatchStrategy) ([]*types.PartyContributionScore, map[string][]*types.PartyContributionScore) { 943 var teamMembers map[string][]string 944 interval := int32(1) 945 if ds.TransferInterval != nil { 946 interval = *ds.TransferInterval 947 } 948 paidFees := mat.GetLastEpochTakeFees(ds.AssetForMetric, ds.Markets, interval) 949 if tsl := len(ds.TeamScope); tsl > 0 { 950 teamMembers = make(map[string][]string, len(ds.TeamScope)) 951 for _, team := range ds.TeamScope { 952 teamMembers[team] = mat.teams.GetTeamMembers(team, mat.minEpochsInTeamForRewardEligibility) 953 } 954 } else { 955 teamMembers = mat.teams.GetAllTeamsWithParties(mat.minEpochsInTeamForRewardEligibility) 956 } 957 stakingRequirement, _ := num.UintFromString(ds.StakingRequirement, 10) 958 notionalRequirement, _ := num.UintFromString(ds.NotionalTimeWeightedAveragePositionRequirement, 10) 959 topNDecimal := num.MustDecimalFromString(ds.NTopPerformers) 960 961 p, _ := lproto.Marshal(ds) 962 gameID := hex.EncodeToString(crypto.Hash(p)) 963 964 return mat.calculateMetricForTeams(ctx, ds.AssetForMetric, teamMembers, ds.Markets, ds.Metric, stakingRequirement, notionalRequirement, int(ds.WindowLength), topNDecimal, gameID, paidFees) 965 } 966 967 func (mat *MarketActivityTracker) isEligibleForReward(ctx context.Context, asset, party string, markets []string, minStakingBalanceRequired *num.Uint, notionalTimeWeightedAveragePositionRequired *num.Uint, gameID string) (bool, *num.Uint, *num.Uint) { 968 eligiblByBalance := true 969 eligibleByNotional := true 970 var balance, notional *num.Uint 971 var err error 972 973 balance, err = mat.balanceChecker.GetAvailableBalance(party) 974 if err != nil || balance.LT(minStakingBalanceRequired) { 975 eligiblByBalance = false 976 if balance == nil { 977 balance = num.UintZero() 978 } 979 } 980 981 notional = mat.getTWNotionalPosition(asset, party, markets) 982 mat.broker.Send(events.NewTimeWeightedNotionalPositionUpdated(ctx, mat.currentEpoch, asset, party, gameID, notional.String())) 983 if notional.LT(notionalTimeWeightedAveragePositionRequired) { 984 eligibleByNotional = false 985 } 986 987 isEligible := (eligiblByBalance || minStakingBalanceRequired.IsZero()) && (eligibleByNotional || notionalTimeWeightedAveragePositionRequired.IsZero()) 988 return isEligible, balance, notional 989 } 990 991 func getEligibilityScore(party, gameID string, eligibilityInEpoch map[string][]map[string]struct{}, balance *num.Uint, notional *num.Uint, paidFees map[string]*num.Uint, windowSize int) *types.PartyContributionScore { 992 if _, ok := eligibilityInEpoch[gameID]; !ok { 993 eligibilityInEpoch[gameID] = []map[string]struct{}{{}} 994 eligibilityInEpoch[gameID][0][party] = struct{}{} 995 return &types.PartyContributionScore{Party: party, Score: num.DecimalOne(), IsEligible: true, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1} 996 } 997 m := eligibilityInEpoch[gameID] 998 if len(m) > windowSize { 999 m = m[1:] 1000 } 1001 m[len(m)-1][party] = struct{}{} 1002 for _, mm := range m { 1003 if _, ok := mm[party]; !ok { 1004 return &types.PartyContributionScore{Party: party, Score: num.DecimalZero(), IsEligible: false, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1} 1005 } 1006 } 1007 return &types.PartyContributionScore{Party: party, Score: num.DecimalOne(), IsEligible: true, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1} 1008 } 1009 1010 func (mat *MarketActivityTracker) calculateMetricForIndividuals(ctx context.Context, asset string, parties []string, markets []string, metric vega.DispatchMetric, minStakingBalanceRequired *num.Uint, notionalTimeWeightedAveragePositionRequired *num.Uint, windowSize int, gameID string, interval int32) []*types.PartyContributionScore { 1011 ret := make([]*types.PartyContributionScore, 0, len(parties)) 1012 paidFees := mat.GetLastEpochTakeFees(asset, markets, interval) 1013 for _, party := range parties { 1014 eligible, balance, notional := mat.isEligibleForReward(ctx, asset, party, markets, minStakingBalanceRequired, notionalTimeWeightedAveragePositionRequired, gameID) 1015 if !eligible { 1016 ret = append(ret, &types.PartyContributionScore{Party: party, Score: num.DecimalZero(), IsEligible: eligible, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1017 continue 1018 } 1019 if metric == vega.DispatchMetric_DISPATCH_METRIC_ELIGIBLE_ENTITIES { 1020 ret = append(ret, getEligibilityScore(party, gameID, mat.eligibilityInEpoch, balance, notional, paidFees, windowSize)) 1021 continue 1022 } 1023 score, ok := mat.calculateMetricForParty(asset, party, markets, metric, windowSize) 1024 if !ok { 1025 ret = append(ret, &types.PartyContributionScore{Party: party, Score: num.DecimalZero(), IsEligible: false, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1026 continue 1027 } 1028 ret = append(ret, &types.PartyContributionScore{Party: party, Score: score, IsEligible: true, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1029 } 1030 return ret 1031 } 1032 1033 // CalculateMetricForTeams returns a slice of metrics for the team and a slice of metrics for each team member. 1034 func (mat *MarketActivityTracker) calculateMetricForTeams(ctx context.Context, asset string, teams map[string][]string, marketsInScope []string, metric vega.DispatchMetric, minStakingBalanceRequired *num.Uint, notionalTimeWeightedAveragePositionRequired *num.Uint, windowSize int, topN num.Decimal, gameID string, paidFees map[string]*num.Uint) ([]*types.PartyContributionScore, map[string][]*types.PartyContributionScore) { 1035 teamScores := make([]*types.PartyContributionScore, 0, len(teams)) 1036 teamKeys := make([]string, 0, len(teams)) 1037 for k := range teams { 1038 teamKeys = append(teamKeys, k) 1039 } 1040 sort.Strings(teamKeys) 1041 ps := make(map[string][]*types.PartyContributionScore, len(teamScores)) 1042 for _, t := range teamKeys { 1043 ts, teamMemberScores := mat.calculateMetricForTeam(ctx, asset, teams[t], marketsInScope, metric, minStakingBalanceRequired, notionalTimeWeightedAveragePositionRequired, windowSize, topN, gameID, paidFees) 1044 if ts.IsZero() { 1045 continue 1046 } 1047 teamScores = append(teamScores, &types.PartyContributionScore{Party: t, Score: ts}) 1048 ps[t] = teamMemberScores 1049 } 1050 1051 return teamScores, ps 1052 } 1053 1054 // calculateMetricForTeam returns the metric score for team and a slice of the score for each of its members. 1055 func (mat *MarketActivityTracker) calculateMetricForTeam(ctx context.Context, asset string, parties []string, marketsInScope []string, metric vega.DispatchMetric, minStakingBalanceRequired *num.Uint, notionalTimeWeightedAveragePositionRequired *num.Uint, windowSize int, topN num.Decimal, gameID string, paidFees map[string]*num.Uint) (num.Decimal, []*types.PartyContributionScore) { 1056 return calculateMetricForTeamUtil(ctx, asset, parties, marketsInScope, metric, minStakingBalanceRequired, notionalTimeWeightedAveragePositionRequired, windowSize, topN, mat.isEligibleForReward, mat.calculateMetricForParty, gameID, paidFees, mat.eligibilityInEpoch) 1057 } 1058 1059 func calculateMetricForTeamUtil(ctx context.Context, 1060 asset string, 1061 parties []string, 1062 marketsInScope []string, 1063 metric vega.DispatchMetric, 1064 minStakingBalanceRequired *num.Uint, 1065 notionalTimeWeightedAveragePositionRequired *num.Uint, 1066 windowSize int, 1067 topN num.Decimal, 1068 isEligibleForReward func(ctx context.Context, asset, party string, markets []string, minStakingBalanceRequired *num.Uint, notionalTimeWeightedAveragePositionRequired *num.Uint, gameID string) (bool, *num.Uint, *num.Uint), 1069 calculateMetricForParty func(asset, party string, marketsInScope []string, metric vega.DispatchMetric, windowSize int) (num.Decimal, bool), 1070 gameID string, 1071 paidFees map[string]*num.Uint, 1072 eligibilityInEpoch map[string][]map[string]struct{}, 1073 ) (num.Decimal, []*types.PartyContributionScore) { 1074 teamPartyScores := []*types.PartyContributionScore{} 1075 eligibleTeamPartyScores := []*types.PartyContributionScore{} 1076 for _, party := range parties { 1077 eligible, balance, notional := isEligibleForReward(ctx, asset, party, marketsInScope, minStakingBalanceRequired, notionalTimeWeightedAveragePositionRequired, gameID) 1078 if !eligible { 1079 teamPartyScores = append(teamPartyScores, &types.PartyContributionScore{Party: party, Score: num.DecimalZero(), IsEligible: eligible, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1080 continue 1081 } 1082 1083 if metric == vega.DispatchMetric_DISPATCH_METRIC_ELIGIBLE_ENTITIES { 1084 score := getEligibilityScore(party, gameID, eligibilityInEpoch, balance, notional, paidFees, windowSize) 1085 teamPartyScores = append(teamPartyScores, score) 1086 if score.IsEligible { 1087 eligibleTeamPartyScores = append(eligibleTeamPartyScores, score) 1088 } 1089 continue 1090 } 1091 if score, ok := calculateMetricForParty(asset, party, marketsInScope, metric, windowSize); ok { 1092 teamPartyScores = append(teamPartyScores, &types.PartyContributionScore{Party: party, Score: score, IsEligible: eligible, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1093 eligibleTeamPartyScores = append(eligibleTeamPartyScores, &types.PartyContributionScore{Party: party, Score: score, IsEligible: eligible, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1094 } else { 1095 teamPartyScores = append(teamPartyScores, &types.PartyContributionScore{Party: party, Score: num.DecimalZero(), IsEligible: false, StakingBalance: balance, OpenVolume: notional, TotalFeesPaid: paidFees[party], RankingIndex: -1}) 1096 } 1097 } 1098 1099 if len(teamPartyScores) == 0 { 1100 return num.DecimalZero(), []*types.PartyContributionScore{} 1101 } 1102 1103 sort.Slice(eligibleTeamPartyScores, func(i, j int) bool { 1104 return eligibleTeamPartyScores[i].Score.GreaterThan(eligibleTeamPartyScores[j].Score) 1105 }) 1106 1107 sort.Slice(teamPartyScores, func(i, j int) bool { 1108 return teamPartyScores[i].Score.GreaterThan(teamPartyScores[j].Score) 1109 }) 1110 1111 lastUsed := int64(1) 1112 for _, tps := range teamPartyScores { 1113 if tps.IsEligible { 1114 tps.RankingIndex = lastUsed 1115 lastUsed += 1 1116 } 1117 } 1118 1119 maxIndex := int(topN.Mul(num.DecimalFromInt64(int64(len(parties)))).IntPart()) 1120 // ensure non-zero, otherwise we have a divide-by-zero panic on our hands 1121 if maxIndex == 0 { 1122 maxIndex = 1 1123 } 1124 if len(eligibleTeamPartyScores) < maxIndex { 1125 maxIndex = len(eligibleTeamPartyScores) 1126 } 1127 if maxIndex == 0 { 1128 return num.DecimalZero(), teamPartyScores 1129 } 1130 1131 total := num.DecimalZero() 1132 for i := 0; i < maxIndex; i++ { 1133 total = total.Add(eligibleTeamPartyScores[i].Score) 1134 } 1135 1136 return total.Div(num.DecimalFromInt64(int64(maxIndex))), teamPartyScores 1137 } 1138 1139 // calculateMetricForParty returns the value of a reward metric score for the given party for markets of the given assets which are in scope over the given window size. 1140 func (mat *MarketActivityTracker) calculateMetricForParty(asset, party string, marketsInScope []string, metric vega.DispatchMetric, windowSize int) (num.Decimal, bool) { 1141 // exclude unsupported metrics 1142 if metric == vega.DispatchMetric_DISPATCH_METRIC_MARKET_VALUE { 1143 mat.log.Panic("unexpected dispatch metric market value here") 1144 } 1145 if metric == vega.DispatchMetric_DISPATCH_METRIC_VALIDATOR_RANKING { 1146 mat.log.Panic("unexpected dispatch metric validator ranking here") 1147 } 1148 total := num.DecimalZero() 1149 marketTotal := num.DecimalZero() 1150 returns := make([]*num.Decimal, windowSize) 1151 found := false 1152 1153 assetTrackers, ok := mat.assetToMarketTrackers[asset] 1154 if !ok { 1155 return num.DecimalZero(), false 1156 } 1157 1158 markets := marketsInScope 1159 if len(markets) == 0 { 1160 markets = make([]string, 0, len(assetTrackers)) 1161 for k := range assetTrackers { 1162 markets = append(markets, k) 1163 } 1164 } 1165 1166 // for each market in scope, for each epoch in the time window get the metric entry, sum up for each epoch in the time window and divide by window size (or calculate variance - for volatility) 1167 for _, market := range markets { 1168 marketTracker := assetTrackers[market] 1169 if marketTracker == nil { 1170 continue 1171 } 1172 switch metric { 1173 case vega.DispatchMetric_DISPATCH_METRIC_AVERAGE_NOTIONAL: 1174 if t, ok := marketTracker.getNotionalMetricTotal(party, windowSize); ok { 1175 found = true 1176 total = total.Add(t) 1177 } 1178 case vega.DispatchMetric_DISPATCH_METRIC_RELATIVE_RETURN: 1179 if t, ok := marketTracker.getRelativeReturnMetricTotal(party, windowSize); ok { 1180 found = true 1181 total = total.Add(t) 1182 } 1183 case vega.DispatchMetric_DISPATCH_METRIC_REALISED_RETURN: 1184 if t, ok := marketTracker.getRealisedReturnMetricTotal(party, windowSize); ok { 1185 found = true 1186 total = total.Add(t) 1187 } 1188 case vega.DispatchMetric_DISPATCH_METRIC_RETURN_VOLATILITY: 1189 r, ok := marketTracker.getReturns(party, windowSize) 1190 if !ok { 1191 continue 1192 } 1193 found = true 1194 for i, ret := range r { 1195 if ret != nil { 1196 if returns[i] != nil { 1197 *returns[i] = returns[i].Add(*ret) 1198 } else { 1199 returns[i] = ret 1200 } 1201 } 1202 } 1203 case vega.DispatchMetric_DISPATCH_METRIC_MAKER_FEES_PAID: 1204 if t, ok := getFees(marketTracker.epochMakerFeesPaid, party, windowSize); ok { 1205 if t.IsPositive() { 1206 found = true 1207 } 1208 total = total.Add(t) 1209 } 1210 marketTotal = marketTotal.Add(getTotalFees(marketTracker.epochTotalMakerFeesPaid, windowSize)) 1211 case vega.DispatchMetric_DISPATCH_METRIC_MAKER_FEES_RECEIVED: 1212 if t, ok := getFees(marketTracker.epochMakerFeesReceived, party, windowSize); ok { 1213 if t.IsPositive() { 1214 found = true 1215 } 1216 total = total.Add(t) 1217 } 1218 marketTotal = marketTotal.Add(getTotalFees(marketTracker.epochTotalMakerFeesReceived, windowSize)) 1219 case vega.DispatchMetric_DISPATCH_METRIC_LP_FEES_RECEIVED: 1220 if t, ok := getFees(marketTracker.epochLpFees, party, windowSize); ok { 1221 if t.IsPositive() { 1222 found = true 1223 } 1224 total = total.Add(t) 1225 } 1226 marketTotal = marketTotal.Add(getTotalFees(marketTracker.epochTotalLpFees, windowSize)) 1227 } 1228 } 1229 1230 switch metric { 1231 case vega.DispatchMetric_DISPATCH_METRIC_AVERAGE_NOTIONAL: 1232 // descaling the total tw position metric by dividing by the scaling factor 1233 v := total.Div(num.DecimalFromInt64(int64(windowSize) * scalingFactor)) 1234 return v, found 1235 case vega.DispatchMetric_DISPATCH_METRIC_RELATIVE_RETURN, vega.DispatchMetric_DISPATCH_METRIC_REALISED_RETURN: 1236 return total.Div(num.DecimalFromInt64(int64(windowSize))), found 1237 case vega.DispatchMetric_DISPATCH_METRIC_RETURN_VOLATILITY: 1238 filteredReturns := []num.Decimal{} 1239 for _, d := range returns { 1240 if d != nil { 1241 filteredReturns = append(filteredReturns, *d) 1242 } 1243 } 1244 if len(filteredReturns) < 2 { 1245 return num.DecimalZero(), false 1246 } 1247 variance, _ := num.Variance(filteredReturns) 1248 if !variance.IsZero() { 1249 return num.DecimalOne().Div(variance), found 1250 } 1251 return variance, found 1252 case vega.DispatchMetric_DISPATCH_METRIC_MAKER_FEES_PAID, vega.DispatchMetric_DISPATCH_METRIC_MAKER_FEES_RECEIVED, vega.DispatchMetric_DISPATCH_METRIC_LP_FEES_RECEIVED: 1253 if marketTotal.IsZero() { 1254 return num.DecimalZero(), found 1255 } 1256 return total.Div(marketTotal), found 1257 default: 1258 mat.log.Panic("unexpected metric") 1259 } 1260 return num.DecimalZero(), found 1261 } 1262 1263 func (mat *MarketActivityTracker) RecordNotionalTraded(asset, marketID string, notional *num.Uint) { 1264 if tracker, ok := mat.getMarketTracker(asset, marketID); ok { 1265 tracker.notionalVolumeForEpoch.AddSum(notional) 1266 } 1267 } 1268 1269 func (mat *MarketActivityTracker) RecordNotionalTakerVolume(marketID string, party string, volumeToAdd *num.Uint) { 1270 if _, ok := mat.partyTakerNotionalVolume[party]; !ok { 1271 mat.partyTakerNotionalVolume[party] = volumeToAdd 1272 } else { 1273 mat.partyTakerNotionalVolume[party].AddSum(volumeToAdd) 1274 } 1275 1276 if _, ok := mat.marketToPartyTakerNotionalVolume[marketID]; !ok { 1277 mat.marketToPartyTakerNotionalVolume[marketID] = map[string]*num.Uint{ 1278 party: volumeToAdd.Clone(), 1279 } 1280 } else if _, ok := mat.marketToPartyTakerNotionalVolume[marketID][party]; !ok { 1281 mat.marketToPartyTakerNotionalVolume[marketID][party] = volumeToAdd.Clone() 1282 } else { 1283 mat.marketToPartyTakerNotionalVolume[marketID][party].AddSum(volumeToAdd) 1284 } 1285 } 1286 1287 func (mat *MarketActivityTracker) clearNotionalTakerVolume() { 1288 mat.partyTakerNotionalVolume = map[string]*num.Uint{} 1289 mat.marketToPartyTakerNotionalVolume = map[string]map[string]*num.Uint{} 1290 } 1291 1292 func (mat *MarketActivityTracker) NotionalTakerVolumeForAllParties() map[types.PartyID]*num.Uint { 1293 res := make(map[types.PartyID]*num.Uint, len(mat.partyTakerNotionalVolume)) 1294 for k, u := range mat.partyTakerNotionalVolume { 1295 res[types.PartyID(k)] = u.Clone() 1296 } 1297 return res 1298 } 1299 1300 func (mat *MarketActivityTracker) TeamStatsForMarkets(allMarketsForAssets, onlyTheseMarkets []string) map[string]map[string]*num.Uint { 1301 teams := mat.teams.GetAllTeamsWithParties(0) 1302 1303 // Pre-fill stats for all teams and their members. 1304 partyToTeam := map[string]string{} 1305 teamsStats := map[string]map[string]*num.Uint{} 1306 for teamID, members := range teams { 1307 teamsStats[teamID] = map[string]*num.Uint{} 1308 for _, member := range members { 1309 teamsStats[teamID][member] = num.UintZero() 1310 partyToTeam[member] = teamID 1311 } 1312 } 1313 1314 // Filter the markets to get data from. 1315 onlyMarketsStats := map[string]map[string]*num.Uint{} 1316 if len(onlyTheseMarkets) == 0 { 1317 onlyMarketsStats = mat.marketToPartyTakerNotionalVolume 1318 } else { 1319 for _, marketID := range onlyTheseMarkets { 1320 onlyMarketsStats[marketID] = mat.marketToPartyTakerNotionalVolume[marketID] 1321 } 1322 } 1323 1324 for _, asset := range allMarketsForAssets { 1325 mkts, ok := mat.assetToMarketTrackers[asset] 1326 if !ok { 1327 continue 1328 } 1329 for marketID := range mkts { 1330 onlyMarketsStats[marketID] = mat.marketToPartyTakerNotionalVolume[marketID] 1331 } 1332 } 1333 1334 // Gather only party's stats from those who are in a team. 1335 for _, marketStats := range onlyMarketsStats { 1336 for partyID, volume := range marketStats { 1337 teamID, inTeam := partyToTeam[partyID] 1338 if !inTeam { 1339 continue 1340 } 1341 teamsStats[teamID][partyID].AddSum(volume) 1342 } 1343 } 1344 1345 return teamsStats 1346 } 1347 1348 func (mat *MarketActivityTracker) NotionalTakerVolumeForParty(party string) *num.Uint { 1349 if _, ok := mat.partyTakerNotionalVolume[party]; !ok { 1350 return num.UintZero() 1351 } 1352 return mat.partyTakerNotionalVolume[party].Clone() 1353 } 1354 1355 func updateNotionalOnTrade(n *twNotional, notional, price *num.Uint, t, tn int64, time time.Time) { 1356 tnOverT := num.UintZero() 1357 tnOverTComp := uScalingFactor.Clone() 1358 if t != 0 { 1359 tnOverT = num.NewUint(uint64(tn / t)) 1360 tnOverTComp = tnOverTComp.Sub(tnOverTComp, tnOverT) 1361 } 1362 p1 := num.UintZero().Mul(n.currentEpochTWNotional, tnOverTComp) 1363 p2 := num.UintZero().Mul(n.notional, tnOverT) 1364 n.currentEpochTWNotional = num.UintZero().Div(p1.AddSum(p2), uScalingFactor) 1365 n.notional = notional 1366 n.price = price.Clone() 1367 n.t = time 1368 } 1369 1370 func updateNotionalOnEpochEnd(n *twNotional, notional, price *num.Uint, t, tn int64, time time.Time) { 1371 tnOverT := num.UintZero() 1372 tnOverTComp := uScalingFactor.Clone() 1373 if t != 0 { 1374 tnOverT = num.NewUint(uint64(tn / t)) 1375 tnOverTComp = tnOverTComp.Sub(tnOverTComp, tnOverT) 1376 } 1377 p1 := num.UintZero().Mul(n.currentEpochTWNotional, tnOverTComp) 1378 p2 := num.UintZero().Mul(notional, tnOverT) 1379 n.currentEpochTWNotional = num.UintZero().Div(p1.AddSum(p2), uScalingFactor) 1380 n.notional = notional 1381 if price != nil && !price.IsZero() { 1382 n.price = price.Clone() 1383 } 1384 n.t = time 1385 } 1386 1387 func calcNotionalAt(n *twNotional, t, tn int64, markPrice *num.Uint) *num.Uint { 1388 tnOverT := num.UintZero() 1389 tnOverTComp := uScalingFactor.Clone() 1390 if t != 0 { 1391 tnOverT = num.NewUint(uint64(tn / t)) 1392 tnOverTComp = tnOverTComp.Sub(tnOverTComp, tnOverT) 1393 } 1394 p1 := num.UintZero().Mul(n.currentEpochTWNotional, tnOverTComp) 1395 var notional *num.Uint 1396 if markPrice != nil && !markPrice.IsZero() && !(n.price.IsZero() || n.notional.IsZero()) { 1397 notional, _ = num.UintFromDecimal(n.notional.ToDecimal().Div(n.price.ToDecimal()).Mul(markPrice.ToDecimal())) 1398 } else { 1399 notional = n.notional 1400 } 1401 p2 := num.UintZero().Mul(notional, tnOverT) 1402 return num.UintZero().Div(p1.AddSum(p2), uScalingFactor) 1403 } 1404 1405 // recordNotional tracks the time weighted average notional for the party per market. 1406 // notional = abs(position) x price / position_factor 1407 // price in asset decimals. 1408 func (mt *marketTracker) recordNotional(party string, notional *num.Uint, price *num.Uint, time time.Time, epochStartTime time.Time) { 1409 if _, ok := mt.twNotional[party]; !ok { 1410 mt.twNotional[party] = &twNotional{ 1411 t: time, 1412 notional: notional, 1413 currentEpochTWNotional: num.UintZero(), 1414 price: price.Clone(), 1415 } 1416 return 1417 } 1418 t := int64(time.Sub(epochStartTime).Seconds()) 1419 n := mt.twNotional[party] 1420 tn := int64(time.Sub(n.t).Seconds()) * scalingFactor 1421 updateNotionalOnTrade(n, notional, price, t, tn, time) 1422 } 1423 1424 func (mt *marketTracker) processNotionalEndOfEpoch(epochStartTime time.Time, endEpochTime time.Time) { 1425 t := int64(endEpochTime.Sub(epochStartTime).Seconds()) 1426 m := make(map[string]*num.Uint, len(mt.twNotional)) 1427 for party, twNotional := range mt.twNotional { 1428 tn := int64(endEpochTime.Sub(twNotional.t).Seconds()) * scalingFactor 1429 var notional *num.Uint 1430 if mt.markPrice != nil && !mt.markPrice.IsZero() && twNotional.price != nil && !twNotional.price.IsZero() { 1431 notional, _ = num.UintFromDecimal(twNotional.notional.ToDecimal().Div(twNotional.price.ToDecimal()).Mul(mt.markPrice.ToDecimal())) 1432 } else { 1433 notional = twNotional.notional 1434 } 1435 updateNotionalOnEpochEnd(twNotional, notional, mt.markPrice, t, tn, endEpochTime) 1436 m[party] = twNotional.currentEpochTWNotional.Clone() 1437 } 1438 if len(mt.epochTimeWeightedNotional) == maxWindowSize { 1439 mt.epochTimeWeightedNotional = mt.epochTimeWeightedNotional[1:] 1440 } 1441 mt.epochTimeWeightedNotional = append(mt.epochTimeWeightedNotional, m) 1442 for p, twp := range mt.twNotional { 1443 // if the notional at the beginning of the epoch is 0 clear it so we don't keep zero notionals`` forever 1444 if twp.currentEpochTWNotional.IsZero() && twp.notional.IsZero() { 1445 delete(mt.twNotional, p) 1446 } 1447 } 1448 } 1449 1450 func (mt *marketTracker) processNotionalAtMilestone(epochStartTime time.Time, milestoneTime time.Time) { 1451 t := int64(milestoneTime.Sub(epochStartTime).Seconds()) 1452 m := make(map[string]*num.Uint, len(mt.twNotional)) 1453 for party, twNotional := range mt.twNotional { 1454 tn := int64(milestoneTime.Sub(twNotional.t).Seconds()) * scalingFactor 1455 m[party] = calcNotionalAt(twNotional, t, tn, mt.markPrice) 1456 } 1457 mt.epochTimeWeightedNotional = append(mt.epochTimeWeightedNotional, m) 1458 } 1459 1460 func (mat *MarketActivityTracker) getTWNotionalPosition(asset, party string, markets []string) *num.Uint { 1461 total := num.UintZero() 1462 mkts := markets 1463 if len(mkts) == 0 { 1464 mkts = make([]string, 0, len(mat.assetToMarketTrackers[asset])) 1465 for k := range mat.assetToMarketTrackers[asset] { 1466 mkts = append(mkts, k) 1467 } 1468 sort.Strings(mkts) 1469 } 1470 1471 for _, mkt := range mkts { 1472 if tracker, ok := mat.getMarketTracker(asset, mkt); ok { 1473 if len(tracker.epochTimeWeightedNotional) <= 0 { 1474 continue 1475 } 1476 if twNotional, ok := tracker.epochTimeWeightedNotional[len(tracker.epochTimeWeightedNotional)-1][party]; ok { 1477 total.AddSum(twNotional) 1478 } 1479 } 1480 } 1481 return total 1482 } 1483 1484 func updatePosition(toi *twPosition, scaledAbsPos uint64, t, tn int64, time time.Time) { 1485 tnOverT := uint64(0) 1486 if t != 0 { 1487 tnOverT = uint64(tn / t) 1488 } 1489 toi.currentEpochTWPosition = (toi.currentEpochTWPosition*(u64ScalingFactor-tnOverT) + (toi.position * tnOverT)) / u64ScalingFactor 1490 toi.position = scaledAbsPos 1491 toi.t = time 1492 } 1493 1494 func calculatePositionAt(toi *twPosition, t, tn int64) uint64 { 1495 tnOverT := uint64(0) 1496 if t != 0 { 1497 tnOverT = uint64(tn / t) 1498 } 1499 return (toi.currentEpochTWPosition*(u64ScalingFactor-tnOverT) + (toi.position * tnOverT)) / u64ScalingFactor 1500 } 1501 1502 // recordPosition records the current position of a party and the time of change. If there is a previous position then it is time weight updated with respect to the time 1503 // it has been in place during the epoch. 1504 func (mt *marketTracker) recordPosition(party string, absPos uint64, positionFactor num.Decimal, time time.Time, epochStartTime time.Time) { 1505 if party == "network" { 1506 return 1507 } 1508 // scale by scaling factor and divide by position factor 1509 // by design the scaling factor is greater than the max position factor which allows no loss of precision 1510 scaledAbsPos := num.UintZero().Mul(num.NewUint(absPos), uScalingFactor).ToDecimal().Div(positionFactor).IntPart() 1511 if _, ok := mt.twPosition[party]; !ok { 1512 mt.twPosition[party] = &twPosition{ 1513 position: uint64(scaledAbsPos), 1514 t: time, 1515 currentEpochTWPosition: 0, 1516 } 1517 return 1518 } 1519 toi := mt.twPosition[party] 1520 t := int64(time.Sub(epochStartTime).Seconds()) 1521 tn := int64(time.Sub(toi.t).Seconds()) * scalingFactor 1522 1523 updatePosition(toi, uint64(scaledAbsPos), t, tn, time) 1524 } 1525 1526 // processPositionEndOfEpoch is called at the end of the epoch, calculates the time weight of the current position and moves it to the next epoch, and records 1527 // the time weighted position of the current epoch in the history. 1528 func (mt *marketTracker) processPositionEndOfEpoch(epochStartTime time.Time, endEpochTime time.Time) { 1529 t := int64(endEpochTime.Sub(epochStartTime).Seconds()) 1530 m := make(map[string]uint64, len(mt.twPosition)) 1531 for party, toi := range mt.twPosition { 1532 tn := int64(endEpochTime.Sub(toi.t).Seconds()) * scalingFactor 1533 updatePosition(toi, toi.position, t, tn, endEpochTime) 1534 m[party] = toi.currentEpochTWPosition 1535 } 1536 1537 if len(mt.epochTimeWeightedPosition) == maxWindowSize { 1538 mt.epochTimeWeightedPosition = mt.epochTimeWeightedPosition[1:] 1539 } 1540 mt.epochTimeWeightedPosition = append(mt.epochTimeWeightedPosition, m) 1541 for p, twp := range mt.twPosition { 1542 // if the position at the beginning of the epoch is 0 clear it so we don't keep zero positions forever 1543 if twp.currentEpochTWPosition == 0 && twp.position == 0 { 1544 delete(mt.twPosition, p) 1545 } 1546 } 1547 } 1548 1549 func (mt *marketTracker) processPositionAtMilestone(epochStartTime time.Time, milestoneTime time.Time) { 1550 t := int64(milestoneTime.Sub(epochStartTime).Seconds()) 1551 m := make(map[string]uint64, len(mt.twPosition)) 1552 for party, toi := range mt.twPosition { 1553 tn := int64(milestoneTime.Sub(toi.t).Seconds()) * scalingFactor 1554 m[party] = calculatePositionAt(toi, t, tn) 1555 } 1556 mt.epochTimeWeightedPosition = append(mt.epochTimeWeightedPosition, m) 1557 } 1558 1559 // //// return metric ////// 1560 1561 // recordM2M records the amount corresponding to mark to market (profit or loss). 1562 func (mt *marketTracker) recordM2M(party string, amount num.Decimal) { 1563 if party == "network" || amount.IsZero() { 1564 return 1565 } 1566 if _, ok := mt.partyM2M[party]; !ok { 1567 mt.partyM2M[party] = amount 1568 return 1569 } 1570 mt.partyM2M[party] = mt.partyM2M[party].Add(amount) 1571 } 1572 1573 func (mt *marketTracker) recordFundingPayment(party string, amount num.Decimal) { 1574 if party == "network" || amount.IsZero() { 1575 return 1576 } 1577 if _, ok := mt.partyRealisedReturn[party]; !ok { 1578 mt.partyRealisedReturn[party] = amount 1579 return 1580 } 1581 mt.partyRealisedReturn[party] = mt.partyRealisedReturn[party].Add(amount) 1582 } 1583 1584 func (mt *marketTracker) recordRealisedPosition(party string, realisedPosition num.Decimal) { 1585 if party == "network" { 1586 return 1587 } 1588 if _, ok := mt.partyRealisedReturn[party]; !ok { 1589 mt.partyRealisedReturn[party] = realisedPosition 1590 return 1591 } 1592 mt.partyRealisedReturn[party] = mt.partyRealisedReturn[party].Add(realisedPosition) 1593 } 1594 1595 // processM2MEndOfEpoch is called at the end of the epoch to reset the running total for the next epoch and record the total m2m in the ended epoch. 1596 func (mt *marketTracker) processM2MEndOfEpoch() { 1597 m := map[string]num.Decimal{} 1598 for party, m2m := range mt.partyM2M { 1599 if _, ok := mt.twPosition[party]; !ok { 1600 continue 1601 } 1602 p := mt.twPosition[party].currentEpochTWPosition 1603 var v num.Decimal 1604 if p == 0 { 1605 v = num.DecimalZero() 1606 } else { 1607 v = m2m.Div(num.DecimalFromInt64(int64(p)).Div(dScalingFactor)) 1608 } 1609 m[party] = v 1610 } 1611 if len(mt.epochPartyM2M) == maxWindowSize { 1612 mt.epochPartyM2M = mt.epochPartyM2M[1:] 1613 } 1614 mt.partyM2M = map[string]decimal.Decimal{} 1615 mt.epochPartyM2M = append(mt.epochPartyM2M, m) 1616 } 1617 1618 func (mt *marketTracker) processM2MAtMilestone() { 1619 m := map[string]num.Decimal{} 1620 for party, m2m := range mt.partyM2M { 1621 if _, ok := mt.twPosition[party]; !ok { 1622 continue 1623 } 1624 p := mt.epochTimeWeightedPosition[len(mt.epochTimeWeightedPosition)-1][party] 1625 1626 var v num.Decimal 1627 if p == 0 { 1628 v = num.DecimalZero() 1629 } else { 1630 v = m2m.Div(num.DecimalFromInt64(int64(p)).Div(dScalingFactor)) 1631 } 1632 m[party] = v 1633 } 1634 mt.epochPartyM2M = append(mt.epochPartyM2M, m) 1635 } 1636 1637 // processPartyRealisedReturnOfEpoch is called at the end of the epoch to reset the running total for the next epoch and record the total m2m in the ended epoch. 1638 func (mt *marketTracker) processPartyRealisedReturnOfEpoch() { 1639 m := map[string]num.Decimal{} 1640 for party, realised := range mt.partyRealisedReturn { 1641 m[party] = realised 1642 } 1643 if len(mt.epochPartyRealisedReturn) == maxWindowSize { 1644 mt.epochPartyRealisedReturn = mt.epochPartyRealisedReturn[1:] 1645 } 1646 mt.epochPartyRealisedReturn = append(mt.epochPartyRealisedReturn, m) 1647 mt.partyRealisedReturn = map[string]decimal.Decimal{} 1648 } 1649 1650 func (mt *marketTracker) processPartyRealisedReturnAtMilestone() { 1651 m := map[string]num.Decimal{} 1652 for party, realised := range mt.partyRealisedReturn { 1653 m[party] = realised 1654 } 1655 mt.epochPartyRealisedReturn = append(mt.epochPartyRealisedReturn, m) 1656 } 1657 1658 // getReturns returns a slice of the total of the party's return by epoch in the given window. 1659 func (mt *marketTracker) getReturns(party string, windowSize int) ([]*num.Decimal, bool) { 1660 returns := make([]*num.Decimal, windowSize) 1661 if len(mt.epochPartyM2M) == 0 { 1662 return []*num.Decimal{}, false 1663 } 1664 found := false 1665 for i := 0; i < windowSize; i++ { 1666 ind := len(mt.epochPartyM2M) - i - 1 1667 if ind < 0 { 1668 break 1669 } 1670 epochData := mt.epochPartyM2M[ind] 1671 if t, ok := epochData[party]; ok { 1672 found = true 1673 returns[i] = &t 1674 } 1675 } 1676 return returns, found 1677 } 1678 1679 // getNotionalMetricTotal returns the sum of the epoch's time weighted notional over the time window. 1680 func (mt *marketTracker) getNotionalMetricTotal(party string, windowSize int) (num.Decimal, bool) { 1681 return calcTotalForWindowU(party, mt.epochTimeWeightedNotional, windowSize) 1682 } 1683 1684 // getPositionMetricTotal returns the sum of the epoch's time weighted position over the time window. 1685 func (mt *marketTracker) getPositionMetricTotal(party string, windowSize int) (uint64, bool) { 1686 return calcTotalForWindowUint64(party, mt.epochTimeWeightedPosition, windowSize) 1687 } 1688 1689 // getRelativeReturnMetricTotal returns the sum of the relative returns over the given window. 1690 func (mt *marketTracker) getRelativeReturnMetricTotal(party string, windowSize int) (num.Decimal, bool) { 1691 return calcTotalForWindowD(party, mt.epochPartyM2M, windowSize) 1692 } 1693 1694 // getRealisedReturnMetricTotal returns the sum of the relative returns over the given window. 1695 func (mt *marketTracker) getRealisedReturnMetricTotal(party string, windowSize int) (num.Decimal, bool) { 1696 return calcTotalForWindowD(party, mt.epochPartyRealisedReturn, windowSize) 1697 } 1698 1699 // getFees returns the total fees paid/received (depending on what feeData represents) by the party over the given window size. 1700 func getFees(feeData []map[string]*num.Uint, party string, windowSize int) (num.Decimal, bool) { 1701 return calcTotalForWindowU(party, feeData, windowSize) 1702 } 1703 1704 // getTotalFees returns the total fees of the given type measured over the window size. 1705 func getTotalFees(totalFees []*num.Uint, windowSize int) num.Decimal { 1706 if len(totalFees) == 0 { 1707 return num.DecimalZero() 1708 } 1709 total := num.UintZero() 1710 for i := 0; i < windowSize; i++ { 1711 ind := len(totalFees) - i - 1 1712 if ind < 0 { 1713 return total.ToDecimal() 1714 } 1715 total.AddSum(totalFees[ind]) 1716 } 1717 return total.ToDecimal() 1718 } 1719 1720 func (mat *MarketActivityTracker) getEpochTakeFees(asset string, markets []string, takerFeesPaidInEpoch map[string]map[string]map[string]*num.Uint) map[string]*num.Uint { 1721 takerFees := map[string]*num.Uint{} 1722 ast, ok := takerFeesPaidInEpoch[asset] 1723 if !ok { 1724 return takerFees 1725 } 1726 mkts := markets 1727 if len(mkts) == 0 { 1728 mkts = make([]string, 0, len(ast)) 1729 for mkt := range ast { 1730 mkts = append(mkts, mkt) 1731 } 1732 } 1733 1734 for _, m := range mkts { 1735 if fees, ok := ast[m]; ok { 1736 for party, fees := range fees { 1737 if _, ok := takerFees[party]; !ok { 1738 takerFees[party] = num.UintZero() 1739 } 1740 takerFees[party].AddSum(fees) 1741 } 1742 } 1743 } 1744 return takerFees 1745 } 1746 1747 func (mat *MarketActivityTracker) GetLastEpochTakeFees(asset string, markets []string, epochs int32) map[string]*num.Uint { 1748 takerFees := map[string]*num.Uint{} 1749 for i := 0; i < int(epochs); i++ { 1750 ind := len(mat.takerFeesPaidInEpoch) - i - 1 1751 if ind < 0 { 1752 break 1753 } 1754 m := mat.getEpochTakeFees(asset, markets, mat.takerFeesPaidInEpoch[ind]) 1755 for k, v := range m { 1756 if _, ok := takerFees[k]; !ok { 1757 takerFees[k] = num.UintZero() 1758 } 1759 takerFees[k].AddSum(v) 1760 } 1761 } 1762 return takerFees 1763 } 1764 1765 // calcTotalForWindowU returns the total relevant data from the given slice starting from the given dataIdx-1, going back <window_size> elements. 1766 func calcTotalForWindowU(party string, data []map[string]*num.Uint, windowSize int) (num.Decimal, bool) { 1767 found := false 1768 if len(data) == 0 { 1769 return num.DecimalZero(), found 1770 } 1771 total := num.UintZero() 1772 for i := 0; i < windowSize; i++ { 1773 ind := len(data) - i - 1 1774 if ind < 0 { 1775 return total.ToDecimal(), found 1776 } 1777 if v, ok := data[ind][party]; ok { 1778 found = true 1779 total.AddSum(v) 1780 } 1781 } 1782 return total.ToDecimal(), found 1783 } 1784 1785 // calcTotalForWindowD returns the total relevant data from the given slice starting from the given dataIdx-1, going back <window_size> elements. 1786 func calcTotalForWindowD(party string, data []map[string]num.Decimal, windowSize int) (num.Decimal, bool) { 1787 found := false 1788 if len(data) == 0 { 1789 return num.DecimalZero(), found 1790 } 1791 total := num.DecimalZero() 1792 for i := 0; i < windowSize; i++ { 1793 ind := len(data) - i - 1 1794 if ind < 0 { 1795 return total, found 1796 } 1797 if v, ok := data[ind][party]; ok { 1798 found = true 1799 total = total.Add(v) 1800 } 1801 } 1802 return total, found 1803 } 1804 1805 // calcTotalForWindowUint64 returns the total relevant data from the given slice starting from the given dataIdx-1, going back <window_size> elements. 1806 func calcTotalForWindowUint64(party string, data []map[string]uint64, windowSize int) (uint64, bool) { 1807 found := false 1808 if len(data) == 0 { 1809 return 0, found 1810 } 1811 1812 total := uint64(0) 1813 for i := 0; i < windowSize; i++ { 1814 ind := len(data) - i - 1 1815 if ind < 0 { 1816 return total, found 1817 } 1818 if v, ok := data[ind][party]; ok { 1819 found = true 1820 total += v 1821 } 1822 } 1823 return total, found 1824 } 1825 1826 // returns the sorted slice of keys for the given map. 1827 func sortedK[T any](m map[string]T) []string { 1828 keys := make([]string, 0, len(m)) 1829 for k := range m { 1830 keys = append(keys, k) 1831 } 1832 sort.Strings(keys) 1833 return keys 1834 } 1835 1836 // takes a set of all parties and exclude from it the given slice of parties. 1837 func excludePartiesInTeams(allParties map[string]struct{}, partiesInTeams []string) map[string]struct{} { 1838 for _, v := range partiesInTeams { 1839 delete(allParties, v) 1840 } 1841 return allParties 1842 }