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 { calculatePayout } from './calculate'
import { Dictionary, groupBy, last, sortBy, sum, sumBy, uniq } from 'lodash'
import { calculatePayout, getContractBetMetrics } from './calculate'
import { Bet, LimitBet } from './bet'
import { Contract, CPMMContract, DPMContract } from './contract'
import {
Contract,
CPMMBinaryContract,
CPMMContract,
DPMContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { DAY_MS } from './util/time'
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
@ -35,8 +40,7 @@ export const computeInvestmentValueCustomProb = (
const betP = outcome === 'YES' ? p : 1 - p
const payout = betP * shares
const value = payout - (bet.loanAmount ?? 0)
const value = betP * shares
if (isNaN(value)) return 0
return value
})
@ -246,3 +250,71 @@ export const calculateNewProfit = (
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 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 { getValues, log, logMemory, writeAsync } from './utils'
@ -15,6 +15,7 @@ import {
calculateNewPortfolioMetrics,
calculateNewProfit,
calculateProbChanges,
calculateMetricsByContract,
computeElasticity,
computeVolume,
} from '../../common/calculate-metrics'
@ -23,6 +24,7 @@ import { Group } from '../../common/group'
import { batchedWaitAll } from '../../common/util/promise'
import { newEndpointNoAuth } from './api'
import { getFunctionUrl } from '../../common/api'
import { filterDefined } from '../../common/util/array'
const firestore = admin.firestore()
export const scheduleUpdateMetrics = functions.pubsub
@ -159,6 +161,12 @@ export async function updateMetricsCore() {
lastPortfolio.investmentValue !== newPortfolio.investmentValue
const newProfit = calculateNewProfit(portfolioHistory, newPortfolio)
const metricsByContract = calculateMetricsByContract(
currentBets,
contractsById
)
const contractRatios = userContracts
.map((contract) => {
if (
@ -189,6 +197,7 @@ export async function updateMetricsCore() {
newProfit,
didPortfolioChange,
newFractionResolvedCorrectly,
metricsByContract,
}
})
@ -204,63 +213,61 @@ export async function updateMetricsCore() {
const nextLoanByUser = keyBy(userPayouts, (payout) => payout.user.id)
const userUpdates = userMetrics.map(
({
user,
newCreatorVolume,
newPortfolio,
newProfit,
didPortfolioChange,
newFractionResolvedCorrectly,
}) => {
({ user, newCreatorVolume, newProfit, newFractionResolvedCorrectly }) => {
const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0
return {
fieldUpdates: {
doc: firestore.collection('users').doc(user.id),
fields: {
creatorVolumeCached: newCreatorVolume,
profitCached: newProfit,
nextLoanCached,
fractionResolvedCorrectly: newFractionResolvedCorrectly,
},
},
subcollectionUpdates: {
doc: firestore
.collection('users')
.doc(user.id)
.collection('portfolioHistory')
.doc(),
fields: didPortfolioChange ? newPortfolio : {},
doc: firestore.collection('users').doc(user.id),
fields: {
creatorVolumeCached: newCreatorVolume,
profitCached: newProfit,
nextLoanCached,
fractionResolvedCorrectly: newFractionResolvedCorrectly,
},
}
}
)
await writeAsync(
firestore,
userUpdates.map((u) => u.fieldUpdates)
await writeAsync(firestore, userUpdates)
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(
firestore,
userUpdates
.filter((u) => !isEmpty(u.subcollectionUpdates.fields))
.map((u) => u.subcollectionUpdates),
'set'
await writeAsync(firestore, portfolioHistoryUpdates, 'set')
const contractMetricsUpdates = userMetrics.flatMap(
({ user, metricsByContract }) => {
const collection = firestore
.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.`)
try {
const groupUpdates = groups.map((group, index) => {
const groupContractIds = contractsByGroup[index] as GroupContractDoc[]
const groupContracts = groupContractIds
.map((e) => contractsById[e.contractId])
.filter((e) => e !== undefined) as Contract[]
const bets = groupContracts.map((e) => {
if (e != null && e.id in betsByContract) {
return betsByContract[e.id] ?? []
} else {
return []
}
})
const groupContracts = filterDefined(
groupContractIds.map((e) => contractsById[e.contractId])
)
const bets = groupContracts.map((e) => betsByContract[e.id] ?? [])
const creatorScores = scoreCreators(groupContracts)
const traderScores = scoreTraders(groupContracts, bets)