Store each user's contract bet metrics (#1017)
* Implement most of caching metrics per user per contract * Small group updates refactor * Write contract-metrics subcollection * Fix type error
This commit is contained in:
		
							parent
							
								
									b8ef272784
								
							
						
					
					
						commit
						f6fd703005
					
				|  | @ -1,7 +1,12 @@ | ||||||
| import { last, sortBy, sum, sumBy, uniq } from 'lodash' | import { Dictionary, groupBy, last, sortBy, sum, sumBy, uniq } from 'lodash' | ||||||
| import { calculatePayout } from './calculate' | import { calculatePayout, getContractBetMetrics } from './calculate' | ||||||
| import { Bet, LimitBet } from './bet' | import { Bet, LimitBet } from './bet' | ||||||
| import { Contract, CPMMContract, DPMContract } from './contract' | import { | ||||||
|  |   Contract, | ||||||
|  |   CPMMBinaryContract, | ||||||
|  |   CPMMContract, | ||||||
|  |   DPMContract, | ||||||
|  | } from './contract' | ||||||
| import { PortfolioMetrics, User } from './user' | import { PortfolioMetrics, User } from './user' | ||||||
| import { DAY_MS } from './util/time' | import { DAY_MS } from './util/time' | ||||||
| import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet' | import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet' | ||||||
|  | @ -35,8 +40,7 @@ export const computeInvestmentValueCustomProb = ( | ||||||
| 
 | 
 | ||||||
|     const betP = outcome === 'YES' ? p : 1 - p |     const betP = outcome === 'YES' ? p : 1 - p | ||||||
| 
 | 
 | ||||||
|     const payout = betP * shares |     const value = betP * shares | ||||||
|     const value = payout - (bet.loanAmount ?? 0) |  | ||||||
|     if (isNaN(value)) return 0 |     if (isNaN(value)) return 0 | ||||||
|     return value |     return value | ||||||
|   }) |   }) | ||||||
|  | @ -246,3 +250,71 @@ export const calculateNewProfit = ( | ||||||
| 
 | 
 | ||||||
|   return newProfit |   return newProfit | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export const calculateMetricsByContract = ( | ||||||
|  |   bets: Bet[], | ||||||
|  |   contractsById: Dictionary<Contract> | ||||||
|  | ) => { | ||||||
|  |   const betsByContract = groupBy(bets, (bet) => bet.contractId) | ||||||
|  |   const unresolvedContracts = Object.keys(betsByContract) | ||||||
|  |     .map((cid) => contractsById[cid]) | ||||||
|  |     .filter((c) => c && !c.isResolved) | ||||||
|  | 
 | ||||||
|  |   return unresolvedContracts.map((c) => { | ||||||
|  |     const bets = betsByContract[c.id] ?? [] | ||||||
|  |     const current = getContractBetMetrics(c, bets) | ||||||
|  | 
 | ||||||
|  |     let periodMetrics | ||||||
|  |     if (c.mechanism === 'cpmm-1' && c.outcomeType === 'BINARY') { | ||||||
|  |       const periods = ['day', 'week', 'month'] as const | ||||||
|  |       periodMetrics = Object.fromEntries( | ||||||
|  |         periods.map((period) => [ | ||||||
|  |           period, | ||||||
|  |           calculatePeriodProfit(c, bets, period), | ||||||
|  |         ]) | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       contractId: c.id, | ||||||
|  |       ...current, | ||||||
|  |       from: periodMetrics, | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const calculatePeriodProfit = ( | ||||||
|  |   contract: CPMMBinaryContract, | ||||||
|  |   bets: Bet[], | ||||||
|  |   period: 'day' | 'week' | 'month' | ||||||
|  | ) => { | ||||||
|  |   const days = period === 'day' ? 1 : period === 'week' ? 7 : 30 | ||||||
|  |   const fromTime = Date.now() - days * DAY_MS | ||||||
|  |   const previousBets = bets.filter((b) => b.createdTime < fromTime) | ||||||
|  | 
 | ||||||
|  |   const prevProb = contract.prob - contract.probChanges[period] | ||||||
|  |   const prob = contract.resolutionProbability | ||||||
|  |     ? contract.resolutionProbability | ||||||
|  |     : contract.prob | ||||||
|  | 
 | ||||||
|  |   const previousBetsValue = computeInvestmentValueCustomProb( | ||||||
|  |     previousBets, | ||||||
|  |     contract, | ||||||
|  |     prevProb | ||||||
|  |   ) | ||||||
|  |   const currentBetsValue = computeInvestmentValueCustomProb( | ||||||
|  |     previousBets, | ||||||
|  |     contract, | ||||||
|  |     prob | ||||||
|  |   ) | ||||||
|  |   const profit = currentBetsValue - previousBetsValue | ||||||
|  |   const profitPercent = | ||||||
|  |     previousBetsValue === 0 ? 0 : 100 * (profit / previousBetsValue) | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     profit, | ||||||
|  |     profitPercent, | ||||||
|  |     prevValue: previousBetsValue, | ||||||
|  |     value: currentBetsValue, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import * as functions from 'firebase-functions' | import * as functions from 'firebase-functions' | ||||||
| import * as admin from 'firebase-admin' | import * as admin from 'firebase-admin' | ||||||
| import { groupBy, isEmpty, keyBy, last, sortBy } from 'lodash' | import { groupBy, keyBy, last, sortBy } from 'lodash' | ||||||
| import fetch from 'node-fetch' | import fetch from 'node-fetch' | ||||||
| 
 | 
 | ||||||
| import { getValues, log, logMemory, writeAsync } from './utils' | import { getValues, log, logMemory, writeAsync } from './utils' | ||||||
|  | @ -15,6 +15,7 @@ import { | ||||||
|   calculateNewPortfolioMetrics, |   calculateNewPortfolioMetrics, | ||||||
|   calculateNewProfit, |   calculateNewProfit, | ||||||
|   calculateProbChanges, |   calculateProbChanges, | ||||||
|  |   calculateMetricsByContract, | ||||||
|   computeElasticity, |   computeElasticity, | ||||||
|   computeVolume, |   computeVolume, | ||||||
| } from '../../common/calculate-metrics' | } from '../../common/calculate-metrics' | ||||||
|  | @ -23,6 +24,7 @@ import { Group } from '../../common/group' | ||||||
| import { batchedWaitAll } from '../../common/util/promise' | import { batchedWaitAll } from '../../common/util/promise' | ||||||
| import { newEndpointNoAuth } from './api' | import { newEndpointNoAuth } from './api' | ||||||
| import { getFunctionUrl } from '../../common/api' | import { getFunctionUrl } from '../../common/api' | ||||||
|  | import { filterDefined } from '../../common/util/array' | ||||||
| 
 | 
 | ||||||
| const firestore = admin.firestore() | const firestore = admin.firestore() | ||||||
| export const scheduleUpdateMetrics = functions.pubsub | export const scheduleUpdateMetrics = functions.pubsub | ||||||
|  | @ -159,6 +161,12 @@ export async function updateMetricsCore() { | ||||||
|       lastPortfolio.investmentValue !== newPortfolio.investmentValue |       lastPortfolio.investmentValue !== newPortfolio.investmentValue | ||||||
| 
 | 
 | ||||||
|     const newProfit = calculateNewProfit(portfolioHistory, newPortfolio) |     const newProfit = calculateNewProfit(portfolioHistory, newPortfolio) | ||||||
|  | 
 | ||||||
|  |     const metricsByContract = calculateMetricsByContract( | ||||||
|  |       currentBets, | ||||||
|  |       contractsById | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|     const contractRatios = userContracts |     const contractRatios = userContracts | ||||||
|       .map((contract) => { |       .map((contract) => { | ||||||
|         if ( |         if ( | ||||||
|  | @ -189,6 +197,7 @@ export async function updateMetricsCore() { | ||||||
|       newProfit, |       newProfit, | ||||||
|       didPortfolioChange, |       didPortfolioChange, | ||||||
|       newFractionResolvedCorrectly, |       newFractionResolvedCorrectly, | ||||||
|  |       metricsByContract, | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  | @ -204,63 +213,61 @@ export async function updateMetricsCore() { | ||||||
|   const nextLoanByUser = keyBy(userPayouts, (payout) => payout.user.id) |   const nextLoanByUser = keyBy(userPayouts, (payout) => payout.user.id) | ||||||
| 
 | 
 | ||||||
|   const userUpdates = userMetrics.map( |   const userUpdates = userMetrics.map( | ||||||
|     ({ |     ({ user, newCreatorVolume, newProfit, newFractionResolvedCorrectly }) => { | ||||||
|       user, |  | ||||||
|       newCreatorVolume, |  | ||||||
|       newPortfolio, |  | ||||||
|       newProfit, |  | ||||||
|       didPortfolioChange, |  | ||||||
|       newFractionResolvedCorrectly, |  | ||||||
|     }) => { |  | ||||||
|       const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0 |       const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0 | ||||||
|       return { |       return { | ||||||
|         fieldUpdates: { |         doc: firestore.collection('users').doc(user.id), | ||||||
|           doc: firestore.collection('users').doc(user.id), |         fields: { | ||||||
|           fields: { |           creatorVolumeCached: newCreatorVolume, | ||||||
|             creatorVolumeCached: newCreatorVolume, |           profitCached: newProfit, | ||||||
|             profitCached: newProfit, |           nextLoanCached, | ||||||
|             nextLoanCached, |           fractionResolvedCorrectly: newFractionResolvedCorrectly, | ||||||
|             fractionResolvedCorrectly: newFractionResolvedCorrectly, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
| 
 |  | ||||||
|         subcollectionUpdates: { |  | ||||||
|           doc: firestore |  | ||||||
|             .collection('users') |  | ||||||
|             .doc(user.id) |  | ||||||
|             .collection('portfolioHistory') |  | ||||||
|             .doc(), |  | ||||||
|           fields: didPortfolioChange ? newPortfolio : {}, |  | ||||||
|         }, |         }, | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|   await writeAsync( |   await writeAsync(firestore, userUpdates) | ||||||
|     firestore, | 
 | ||||||
|     userUpdates.map((u) => u.fieldUpdates) |   const portfolioHistoryUpdates = filterDefined( | ||||||
|  |     userMetrics.map(({ user, newPortfolio, didPortfolioChange }) => { | ||||||
|  |       return didPortfolioChange | ||||||
|  |         ? { | ||||||
|  |             doc: firestore | ||||||
|  |               .collection('users') | ||||||
|  |               .doc(user.id) | ||||||
|  |               .collection('portfolioHistory') | ||||||
|  |               .doc(), | ||||||
|  |             fields: newPortfolio, | ||||||
|  |           } | ||||||
|  |         : null | ||||||
|  |     }) | ||||||
|   ) |   ) | ||||||
|   await writeAsync( |   await writeAsync(firestore, portfolioHistoryUpdates, 'set') | ||||||
|     firestore, | 
 | ||||||
|     userUpdates |   const contractMetricsUpdates = userMetrics.flatMap( | ||||||
|       .filter((u) => !isEmpty(u.subcollectionUpdates.fields)) |     ({ user, metricsByContract }) => { | ||||||
|       .map((u) => u.subcollectionUpdates), |       const collection = firestore | ||||||
|     'set' |         .collection('users') | ||||||
|  |         .doc(user.id) | ||||||
|  |         .collection('contract-metrics') | ||||||
|  |       return metricsByContract.map((metrics) => ({ | ||||||
|  |         doc: collection.doc(metrics.contractId), | ||||||
|  |         fields: metrics, | ||||||
|  |       })) | ||||||
|  |     } | ||||||
|   ) |   ) | ||||||
|  | 
 | ||||||
|  |   await writeAsync(firestore, contractMetricsUpdates, 'set') | ||||||
|  | 
 | ||||||
|   log(`Updated metrics for ${users.length} users.`) |   log(`Updated metrics for ${users.length} users.`) | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     const groupUpdates = groups.map((group, index) => { |     const groupUpdates = groups.map((group, index) => { | ||||||
|       const groupContractIds = contractsByGroup[index] as GroupContractDoc[] |       const groupContractIds = contractsByGroup[index] as GroupContractDoc[] | ||||||
|       const groupContracts = groupContractIds |       const groupContracts = filterDefined( | ||||||
|         .map((e) => contractsById[e.contractId]) |         groupContractIds.map((e) => contractsById[e.contractId]) | ||||||
|         .filter((e) => e !== undefined) as Contract[] |       ) | ||||||
|       const bets = groupContracts.map((e) => { |       const bets = groupContracts.map((e) => betsByContract[e.id] ?? []) | ||||||
|         if (e != null && e.id in betsByContract) { |  | ||||||
|           return betsByContract[e.id] ?? [] |  | ||||||
|         } else { |  | ||||||
|           return [] |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
| 
 | 
 | ||||||
|       const creatorScores = scoreCreators(groupContracts) |       const creatorScores = scoreCreators(groupContracts) | ||||||
|       const traderScores = scoreTraders(groupContracts, bets) |       const traderScores = scoreTraders(groupContracts, bets) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user