diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 9ad44522..0302aa31 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -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 +) => { + 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, + } +} diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts index 19bd5640..39faadbf 100644 --- a/functions/src/update-metrics.ts +++ b/functions/src/update-metrics.ts @@ -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)