From db427611437814c2bd839b56749ec081cd953219 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 28 Apr 2022 13:27:36 -0400 Subject: [PATCH] Implement getFeed cloud function --- functions/src/get-feed.ts | 188 ++++++++++++++++++++++++++++++++++++++ functions/src/index.ts | 1 + functions/src/on-view.ts | 2 +- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 functions/src/get-feed.ts diff --git a/functions/src/get-feed.ts b/functions/src/get-feed.ts new file mode 100644 index 00000000..b241a902 --- /dev/null +++ b/functions/src/get-feed.ts @@ -0,0 +1,188 @@ +import * as _ from 'lodash' +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { getValue, getValues } from './utils' +import { Contract } from '../../common/contract' +import { logInterpolation } from '../../common/util/math' +import { DAY_MS } from '../../common/util/time' +import { + getProbability, + getOutcomeProbability, + getTopAnswer, +} from '../../common/calculate' +import { Bet } from '../../common/bet' +import { Comment } from '../../common/comment' + +const firestore = admin.firestore() + +const MAX_FEED_CONTRACTS = 60 + +export const getFeed = functions + .runWith({ minInstances: 1 }) + .https.onCall(async (_data, context) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + // Get contracts bet on or created in last week. + const contractsPromise = Promise.all([ + getValues( + firestore + .collection('contracts') + .where('isResolved', '==', false) + .where('volume7Days', '>', 0) + ), + + getValues( + firestore + .collection('contracts') + .where('isResolved', '==', false) + .where('createdTime', '>', Date.now() - DAY_MS * 7) + .where('volume7Days', '==', 0) + ), + ]).then(([activeContracts, inactiveContracts]) => { + const combined = [...activeContracts, ...inactiveContracts] + // Remove closed contracts. + return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now()) + }) + + const userCacheCollection = firestore.collection( + `private-users/${userId}/cached` + ) + const [recommendationScores, lastViewedTime] = await Promise.all([ + getValue<{ [contractId: string]: number }>( + userCacheCollection.doc('contractScores') + ), + getValue<{ [contractId: string]: number }>( + userCacheCollection.doc('lastViewTime') + ), + ]).then((dicts) => dicts.map((dict) => dict ?? {})) + + const contracts = await contractsPromise + + const averageRecScore = + 1 + + _.sumBy( + contracts.filter((c) => recommendationScores[c.id] !== undefined), + (c) => recommendationScores[c.id] + ) / + (contracts.length + 1) + + console.log({ recommendationScores, averageRecScore, lastViewedTime }) + + const scoredContracts = contracts.map((contract) => { + const score = scoreContract( + contract, + recommendationScores[contract.id] ?? averageRecScore, + lastViewedTime[contract.id] + ) + return [contract, score] as [Contract, number] + }) + + const sortedContracts = _.sortBy( + scoredContracts, + ([_, score]) => score + ).reverse() + + console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score)) + + const feedContracts = sortedContracts + .slice(0, MAX_FEED_CONTRACTS) + .map(([c]) => c) + + const feed = await Promise.all( + feedContracts.map((contract) => getRecentBetsAndComments(contract)) + ) + + console.log('feed', feed) + + return { status: 'success', feed } + }) + +function scoreContract( + contract: Contract, + recommendationScore: number, + viewTime: number | undefined +) { + const lastViewedScore = getLastViewedScore(viewTime) + const activityScore = getActivityScore(contract, viewTime) + return recommendationScore * lastViewedScore * activityScore +} + +function getActivityScore(contract: Contract, viewTime: number | undefined) { + const { createdTime, lastBetTime, lastCommentTime, outcomeType } = contract + const hasNewComments = + lastCommentTime && (!viewTime || lastCommentTime > viewTime) + const newCommentScore = hasNewComments ? 1 : 0.5 + + const timeSinceLastBet = Date.now() - (lastBetTime ?? createdTime) + const daysAgo = timeSinceLastBet / DAY_MS + const betTimeScore = 1 - logInterpolation(0, 3, daysAgo) + + let prob = 0.5 + if (outcomeType === 'BINARY') { + prob = getProbability(contract) + } else if (outcomeType === 'FREE_RESPONSE') { + const topAnswer = getTopAnswer(contract) + if (topAnswer) + prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id)) + } + const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25 + const probScore = 0.5 + frac * 0.5 + + const { volume24Hours, volume7Days, volume } = contract + const combinedVolume = + Math.log(volume24Hours + 1) + + Math.log(volume7Days + 1) + + Math.log(volume + 1) + const volumeScore = 0.5 + 0.5 * logInterpolation(7, 35, combinedVolume) + + const score = newCommentScore * betTimeScore * probScore * volumeScore + + // Map score to [0.5, 1] since no recent activty is not a deal breaker. + const mappedScore = 0.5 + 0.5 * score + const newMappedScore = 0.7 + 0.3 * score + + const isNew = Date.now() < contract.createdTime + DAY_MS + return isNew ? newMappedScore : mappedScore +} + +function getLastViewedScore(viewTime: number | undefined) { + if (viewTime === undefined) { + return 1 + } + + const daysAgo = (Date.now() - viewTime) / DAY_MS + + if (daysAgo < 0.5) { + const frac = logInterpolation(0, 0.5, daysAgo) + return 0.5 * frac + } + + const frac = logInterpolation(0.5, 14, daysAgo) + return 0.5 + 0.5 * frac +} + +async function getRecentBetsAndComments(contract: Contract) { + const contractDoc = firestore.collection('contracts').doc(contract.id) + + const [recentBets, recentComments] = await Promise.all([ + getValues( + contractDoc + .collection('bets') + .where('createdTime', '>', Date.now() - DAY_MS) + ), + + getValues( + contractDoc + .collection('comments') + .where('createdTime', '>', Date.now() - 3 * DAY_MS) + ), + ]) + + return { + contract, + recentBets, + recentComments, + } +} diff --git a/functions/src/index.ts b/functions/src/index.ts index 004a22b0..a3f81b36 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -25,3 +25,4 @@ export * from './backup-db' export * from './change-user-info' export * from './market-close-emails' export * from './add-liquidity' +export * from './get-feed' diff --git a/functions/src/on-view.ts b/functions/src/on-view.ts index 3939d4d2..54fe7332 100644 --- a/functions/src/on-view.ts +++ b/functions/src/on-view.ts @@ -19,6 +19,6 @@ export const onView = functions.firestore ) await firestore - .doc(`private-users/${userId}/cached/lastViewed`) + .doc(`private-users/${userId}/cached/lastViewTime`) .set({ [contractId]: timestamp }, { merge: true }) })