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:
James Grugett 2022-10-10 13:05:17 -05:00 committed by GitHub
parent b8ef272784
commit f6fd703005
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 129 additions and 50 deletions

View File

@ -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,
}
}

View File

@ -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)