From 01e43abd17c630f62ba3f05731fac88aa23e092b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 Apr 2022 16:36:41 -0500 Subject: [PATCH 1/5] Add factors to weight new contracts higher, contracts with unseen comments, and contracts with prob closer to 50% --- common/calculate.ts | 21 ++++++++++- web/components/contract/contract-card.tsx | 14 +------- web/hooks/use-algo-feed.ts | 44 +++++++++++++++++------ 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/common/calculate.ts b/common/calculate.ts index d6834295..cc6451c5 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -18,7 +18,14 @@ import { getDpmProbabilityAfterSale, } from './calculate-dpm' import { calculateFixedPayout } from './calculate-fixed-payouts' -import { Binary, Contract, CPMM, DPM, FullContract } from './contract' +import { + Binary, + Contract, + CPMM, + DPM, + FreeResponseContract, + FullContract, +} from './contract' export function getProbability(contract: FullContract) { return contract.mechanism === 'cpmm-1' @@ -170,3 +177,15 @@ export function getContractBetNullMetrics() { profitPercent: 0, } } + +export function getTopAnswer(contract: FreeResponseContract) { + const { answers } = contract + const top = _.maxBy( + answers.map((answer) => ({ + answer, + prob: getOutcomeProbability(contract, answer.id), + })), + ({ prob }) => prob + ) + return top?.answer +} diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index f415a38c..41373679 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -23,7 +23,7 @@ import { BinaryContractOutcomeLabel, FreeResponseOutcomeLabel, } from '../outcome-label' -import { getOutcomeProbability } from '../../../common/calculate' +import { getOutcomeProbability, getTopAnswer } from '../../../common/calculate' import { AbbrContractDetails } from './contract-details' export function ContractCard(props: { @@ -122,18 +122,6 @@ export function BinaryResolutionOrChance(props: { ) } -function getTopAnswer(contract: FreeResponseContract) { - const { answers } = contract - const top = _.maxBy( - answers.map((answer) => ({ - answer, - prob: getOutcomeProbability(contract, answer.id), - })), - ({ prob }) => prob - ) - return top?.answer -} - export function FreeResponseResolutionOrChance(props: { contract: FreeResponseContract truncate: 'short' | 'long' | 'none' diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index 68958821..365f4baa 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -8,6 +8,12 @@ import { logInterpolation } from '../../common/util/math' import { getRecommendedContracts } from '../../common/recommended-contracts' import { useSeenContracts } from './use-seen-contracts' import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets' +import { DAY_MS } from '../../common/util/time' +import { + getProbability, + getOutcomeProbability, + getTopAnswer, +} from '../../common/calculate' const MAX_FEED_CONTRACTS = 75 @@ -120,11 +126,13 @@ function getContractsActivityScores( ) const scoredContracts = contracts.map((contract) => { + const { outcomeType } = contract + const seenTime = seenContracts[contract.id] const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime const hasNewComments = !seenTime || (lastCommentTime && lastCommentTime > seenTime) - const newCommentScore = hasNewComments ? 1 : 0.75 + const newCommentScore = hasNewComments ? 1 : 0.5 const commentCount = contractComments[contract.id]?.length ?? 0 const betCount = contractBets[contract.id]?.length ?? 0 @@ -132,25 +140,39 @@ function getContractsActivityScores( const activityCountScore = 0.5 + 0.5 * logInterpolation(0, 200, activtyCount) - const lastBetTime = contractMostRecentBet[contract.id]?.createdTime - const timeSinceLastBet = !lastBetTime - ? contract.createdTime - : Date.now() - lastBetTime - const daysAgo = timeSinceLastBet / oneDayMs + const lastBetTime = + contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime + const timeSinceLastBet = Date.now() - lastBetTime + const daysAgo = timeSinceLastBet / DAY_MS const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo) - const score = newCommentScore * activityCountScore * timeAgoScore + 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 score = + newCommentScore * activityCountScore * timeAgoScore * probScore // Map score to [0.5, 1] since no recent activty is not a deal breaker. const mappedScore = 0.5 + score / 2 - return [contract.id, mappedScore] as [string, number] + const newMappedScore = 0.75 + score / 4 + + const isNew = Date.now() < contract.createdTime + DAY_MS + const activityScore = isNew ? newMappedScore : mappedScore + + return [contract.id, activityScore] as [string, number] }) return _.fromPairs(scoredContracts) } -const oneDayMs = 24 * 60 * 60 * 1000 - function getSeenContractsScore( contract: Contract, seenContracts: { [contractId: string]: number } @@ -160,7 +182,7 @@ function getSeenContractsScore( return 1 } - const daysAgo = (Date.now() - lastSeen) / oneDayMs + const daysAgo = (Date.now() - lastSeen) / DAY_MS if (daysAgo < 0.5) { const frac = logInterpolation(0, 0.5, daysAgo) From e1b5b595e76414d5af90e46771813231aee62664 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 Apr 2022 16:59:34 -0500 Subject: [PATCH 2/5] Also get new contracts for feed with 0 volume. --- web/hooks/use-algo-feed.ts | 7 ++++++- web/hooks/use-contracts.ts | 16 +++++++++++++--- web/hooks/use-user-bets.ts | 12 +++++++----- web/lib/firebase/contracts.ts | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index 365f4baa..f27a1870 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -36,7 +36,12 @@ export const useAlgoFeed = ( const [algoFeed, setAlgoFeed] = useState([]) useEffect(() => { - if (initialContracts && initialBets && initialComments) { + if ( + initialContracts && + initialBets && + initialComments && + yourBetContractIds + ) { const eligibleContracts = initialContracts.filter( (c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now() ) diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 26c43ed1..c6d2be0e 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -6,6 +6,7 @@ import { listenForContracts, listenForHotContracts, listenForInactiveContracts, + listenForNewContracts, } from '../lib/firebase/contracts' import { listenForTaggedContracts } from '../lib/firebase/folds' @@ -20,13 +21,22 @@ export const useContracts = () => { } export const useActiveContracts = () => { - const [contracts, setContracts] = useState() + const [activeContracts, setActiveContracts] = useState< + Contract[] | undefined + >() + const [newContracts, setNewContracts] = useState() useEffect(() => { - return listenForActiveContracts(setContracts) + return listenForActiveContracts(setActiveContracts) }, []) - return contracts + useEffect(() => { + return listenForNewContracts(setNewContracts) + }, []) + + if (!activeContracts || !newContracts) return undefined + + return [...activeContracts, ...newContracts] } export const useInactiveContracts = () => { diff --git a/web/hooks/use-user-bets.ts b/web/hooks/use-user-bets.ts index 82606933..c11af097 100644 --- a/web/hooks/use-user-bets.ts +++ b/web/hooks/use-user-bets.ts @@ -54,13 +54,15 @@ export const useUserBetContracts = (userId: string | undefined) => { } export const useGetUserBetContractIds = (userId: string | undefined) => { - const [contractIds, setContractIds] = useState([]) + const [contractIds, setContractIds] = useState() useEffect(() => { - const key = `user-bet-contractIds-${userId}` - const userBetContractJson = localStorage.getItem(key) - if (userBetContractJson) { - setContractIds(JSON.parse(userBetContractJson)) + if (userId) { + const key = `user-bet-contractIds-${userId}` + const userBetContractJson = localStorage.getItem(key) + if (userBetContractJson) { + setContractIds(JSON.parse(userBetContractJson)) + } } }, [userId]) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index cc9118f9..1646bd09 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -22,6 +22,7 @@ import { getDpmProbability } from '../../../common/calculate-dpm' import { createRNG, shuffle } from '../../../common/util/random' import { getCpmmProbability } from '../../../common/calculate-cpmm' import { formatMoney, formatPercent } from '../../../common/util/format' +import { DAY_MS } from '../../../common/util/time' export type { Contract } export function contractPath(contract: Contract) { @@ -162,6 +163,19 @@ export function listenForInactiveContracts( return listenForValues(inactiveContractsQuery, setContracts) } +const newContractsQuery = query( + contractCollection, + where('isResolved', '==', false), + where('volume7Days', '==', 0), + where('createdTime', '>', Date.now() - 7 * DAY_MS) +) + +export function listenForNewContracts( + setContracts: (contracts: Contract[]) => void +) { + return listenForValues(newContractsQuery, setContracts) +} + export function listenForContract( contractId: string, setContract: (contract: Contract | null) => void From 8c8a9be6a118cfac841286bc183c44d41e9b80f5 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 Apr 2022 17:25:29 -0500 Subject: [PATCH 3/5] Show new label instead of 0 volume --- web/components/feed/feed-items.tsx | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index e2def4d4..2200bbbb 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -9,6 +9,7 @@ import { UserIcon, UsersIcon, XIcon, + SparklesIcon, } from '@heroicons/react/solid' import clsx from 'clsx' import Textarea from 'react-expanding-textarea' @@ -46,6 +47,7 @@ import { useSaveSeenContract } from '../../hooks/use-seen-contracts' import { User } from '../../../common/user' import { Modal } from '../layout/modal' import { trackClick } from '../../lib/firebase/tracking' +import { DAY_MS } from '../../../common/util/time' export function FeedItems(props: { contract: Contract @@ -307,10 +309,18 @@ export function FeedQuestion(props: { contractPath?: string }) { const { contract, showDescription } = props - const { creatorName, creatorUsername, question, resolution, outcomeType } = - contract + const { + creatorName, + creatorUsername, + question, + resolution, + outcomeType, + volume, + createdTime, + } = contract const { volumeLabel } = contractMetrics(contract) const isBinary = outcomeType === 'BINARY' + const isNew = createdTime > Date.now() - DAY_MS // const closeMessage = // contract.isResolved || !contract.closeTime ? null : ( @@ -336,10 +346,18 @@ export function FeedQuestion(props: { />{' '} asked {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} - - {volumeLabel} - {/* {closeMessage} */} - +
+ {isNew || volume === 0 ? ( + + + ) : ( + + {volumeLabel} + {/* {closeMessage} */} + + )} +
From 7847a9e78197435a665be2f5a1f4528d424dd043 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 21 Apr 2022 01:00:08 -0500 Subject: [PATCH 4/5] Track latency of feed and portfolio page. --- common/tracking.ts | 6 ++++++ firestore.rules | 4 ++++ web/components/bets-list.tsx | 10 ++++++++++ web/hooks/use-algo-feed.ts | 6 ++++++ web/hooks/use-time-since-first-render.ts | 13 +++++++++++++ web/lib/firebase/tracking.ts | 18 +++++++++++++++++- 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 web/hooks/use-time-since-first-render.ts diff --git a/common/tracking.ts b/common/tracking.ts index 29a1365c..bf06b6e3 100644 --- a/common/tracking.ts +++ b/common/tracking.ts @@ -10,3 +10,9 @@ export type ClickEvent = { contractId: string timestamp: number } + +export type LatencyEvent = { + type: 'feed' | 'portfolio' + latency: number + timestamp: number +} diff --git a/firestore.rules b/firestore.rules index 542bd5ec..09d65aac 100644 --- a/firestore.rules +++ b/firestore.rules @@ -30,6 +30,10 @@ service cloud.firestore { allow create: if userId == request.auth.uid; } + match /private-users/{userId}/latency/{loadTimeId} { + allow create: if userId == request.auth.uid; + } + match /contracts/{contractId} { allow read; allow update: if request.resource.data.diff(resource.data).affectedKeys() diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 35e0b3d9..63cbfd57 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -37,6 +37,8 @@ import { resolvedPayout, getContractBetNullMetrics, } from '../../common/calculate' +import { useTimeSinceFirstRender } from '../hooks/use-time-since-first-render' +import { trackLatency } from '../lib/firebase/tracking' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'closed' | 'resolved' | 'all' @@ -67,6 +69,14 @@ export function BetsList(props: { user: User }) { } }, [bets]) + const getTime = useTimeSinceFirstRender() + useEffect(() => { + if (bets && contracts) { + trackLatency('portfolio', getTime()) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [!!bets, !!contracts]) + if (!bets || !contracts) { return } diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index f27a1870..543f484b 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -14,6 +14,8 @@ import { getOutcomeProbability, getTopAnswer, } from '../../common/calculate' +import { useTimeSinceFirstRender } from './use-time-since-first-render' +import { trackLatency } from '../lib/firebase/tracking' const MAX_FEED_CONTRACTS = 75 @@ -35,6 +37,8 @@ export const useAlgoFeed = ( const [algoFeed, setAlgoFeed] = useState([]) + const getTime = useTimeSinceFirstRender() + useEffect(() => { if ( initialContracts && @@ -53,6 +57,7 @@ export const useAlgoFeed = ( seenContracts ) setAlgoFeed(contracts) + trackLatency('feed', getTime()) } }, [ initialBets, @@ -60,6 +65,7 @@ export const useAlgoFeed = ( initialContracts, seenContracts, yourBetContractIds, + getTime, ]) return algoFeed diff --git a/web/hooks/use-time-since-first-render.ts b/web/hooks/use-time-since-first-render.ts new file mode 100644 index 00000000..da132146 --- /dev/null +++ b/web/hooks/use-time-since-first-render.ts @@ -0,0 +1,13 @@ +import { useCallback, useEffect, useRef } from 'react' + +export function useTimeSinceFirstRender() { + const startTimeRef = useRef(0) + useEffect(() => { + startTimeRef.current = Date.now() + }, []) + + return useCallback(() => { + if (!startTimeRef.current) return 0 + return Date.now() - startTimeRef.current + }, []) +} diff --git a/web/lib/firebase/tracking.ts b/web/lib/firebase/tracking.ts index 4d609f68..034ad09d 100644 --- a/web/lib/firebase/tracking.ts +++ b/web/lib/firebase/tracking.ts @@ -2,7 +2,7 @@ import { doc, collection, setDoc } from 'firebase/firestore' import _ from 'lodash' import { db } from './init' -import { ClickEvent, View } from '../../../common/tracking' +import { ClickEvent, LatencyEvent, View } from '../../../common/tracking' import { listenForLogin, User } from './users' let user: User | null = null @@ -34,3 +34,19 @@ export async function trackClick(contractId: string) { return await setDoc(ref, clickEvent) } + +export async function trackLatency( + type: 'feed' | 'portfolio', + latency: number +) { + if (!user) return + const ref = doc(collection(db, 'private-users', user.id, 'latency')) + + const latencyEvent: LatencyEvent = { + type, + latency, + timestamp: Date.now(), + } + + return await setDoc(ref, latencyEvent) +} From 9ce82b1b6f02c76abd91e288a3ca4c3d4b7aaf3d Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 20 Apr 2022 23:03:16 -0700 Subject: [PATCH 5/5] Show "New" badge on contract cards too --- web/components/contract/contract-details.tsx | 11 ++++++----- web/components/feed/feed-items.tsx | 20 ++++---------------- web/components/new-contract-badge.tsx | 9 +++++++++ 3 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 web/components/new-contract-badge.tsx diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 83c3c25a..8cc27496 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -18,6 +18,7 @@ import { Avatar } from '../avatar' import { useState } from 'react' import { ContractInfoDialog } from './contract-info-dialog' import { Bet } from '../../../common/bet' +import NewContractBadge from '../new-contract-badge' export function AbbrContractDetails(props: { contract: Contract @@ -25,7 +26,8 @@ export function AbbrContractDetails(props: { showCloseTime?: boolean }) { const { contract, showHotVolume, showCloseTime } = props - const { volume24Hours, creatorName, creatorUsername, closeTime } = contract + const { volume, volume24Hours, creatorName, creatorUsername, closeTime } = + contract const { volumeLabel } = contractMetrics(contract) return ( @@ -54,11 +56,10 @@ export function AbbrContractDetails(props: { {(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '} {fromNow(closeTime || 0)} + ) : volume > 0 ? ( + {volumeLabel} ) : ( - - {/* */} - {volumeLabel} - + )} diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 2200bbbb..33fb0912 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -48,6 +48,7 @@ import { User } from '../../../common/user' import { Modal } from '../layout/modal' import { trackClick } from '../../lib/firebase/tracking' import { DAY_MS } from '../../../common/util/time' +import NewContractBadge from '../new-contract-badge' export function FeedItems(props: { contract: Contract @@ -313,7 +314,6 @@ export function FeedQuestion(props: { creatorName, creatorUsername, question, - resolution, outcomeType, volume, createdTime, @@ -322,15 +322,6 @@ export function FeedQuestion(props: { const isBinary = outcomeType === 'BINARY' const isNew = createdTime > Date.now() - DAY_MS - // const closeMessage = - // contract.isResolved || !contract.closeTime ? null : ( - // <> - // - // {contract.closeTime > Date.now() ? 'Closes' : 'Closed'} - // - // - // ) - return ( <> {' '} asked {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} -
+
{isNew || volume === 0 ? ( - - + ) : ( - + {volumeLabel} - {/* {closeMessage} */} )}
diff --git a/web/components/new-contract-badge.tsx b/web/components/new-contract-badge.tsx new file mode 100644 index 00000000..3cd47eab --- /dev/null +++ b/web/components/new-contract-badge.tsx @@ -0,0 +1,9 @@ +import { SparklesIcon } from '@heroicons/react/solid' + +export default function NewContractBadge() { + return ( + + + ) +}