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  }