From 0185fe3b0b2027daf22cfb5678a0d64c4ae6caee Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 10 Oct 2022 11:21:49 -0500 Subject: [PATCH] Implement most of caching metrics per user per contract --- common/calculate-metrics.ts | 87 +++++++++++++++++++++++++++++++-- functions/src/update-metrics.ts | 70 +++++++++++++------------- 2 files changed, 116 insertions(+), 41 deletions(-) diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 9ad44522..874f6b46 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -1,7 +1,21 @@ -import { last, sortBy, sum, sumBy, uniq } from 'lodash' -import { calculatePayout } from './calculate' +import { + Dictionary, + groupBy, + keyBy, + 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 +49,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 +259,67 @@ 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') { + periodMetrics = keyBy(['day', 'week', 'month'] as const, (period) => { + return 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 4739dcc1..a7e3b13b 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() @@ -160,6 +162,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 ( @@ -190,6 +198,7 @@ export async function updateMetricsCore() { newProfit, didPortfolioChange, newFractionResolvedCorrectly, + metricsByContract, } }) @@ -205,48 +214,37 @@ 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 - .filter((u) => !isEmpty(u.subcollectionUpdates.fields)) - .map((u) => u.subcollectionUpdates), - 'set' + 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, portfolioHistoryUpdates, 'set') + log(`Updated metrics for ${users.length} users.`) try {