2022-04-09 23:10:58 +00:00
|
|
|
import _ from 'lodash'
|
|
|
|
import { useState, useEffect, useMemo } from 'react'
|
|
|
|
import { Bet } from '../../common/bet'
|
|
|
|
import { Comment } from '../../common/comment'
|
|
|
|
import { Contract } from '../../common/contract'
|
|
|
|
import { User } from '../../common/user'
|
|
|
|
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'
|
2022-04-20 21:36:41 +00:00
|
|
|
import { DAY_MS } from '../../common/util/time'
|
|
|
|
import {
|
|
|
|
getProbability,
|
|
|
|
getOutcomeProbability,
|
|
|
|
getTopAnswer,
|
|
|
|
} from '../../common/calculate'
|
2022-04-21 06:00:08 +00:00
|
|
|
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
|
|
|
import { trackLatency } from '../lib/firebase/tracking'
|
2022-04-09 23:10:58 +00:00
|
|
|
|
|
|
|
const MAX_FEED_CONTRACTS = 75
|
|
|
|
|
|
|
|
export const useAlgoFeed = (
|
|
|
|
user: User | null | undefined,
|
|
|
|
contracts: Contract[] | undefined,
|
|
|
|
recentBets: Bet[] | undefined,
|
|
|
|
recentComments: Comment[] | undefined
|
|
|
|
) => {
|
|
|
|
const initialContracts = useMemo(() => contracts, [!!contracts])
|
|
|
|
const initialBets = useMemo(() => recentBets, [!!recentBets])
|
|
|
|
const initialComments = useMemo(() => recentComments, [!!recentComments])
|
|
|
|
|
|
|
|
const yourBetContractIds = useGetUserBetContractIds(user?.id)
|
|
|
|
// Update user bet contracts in local storage.
|
|
|
|
useUserBetContracts(user?.id)
|
|
|
|
|
|
|
|
const seenContracts = useSeenContracts()
|
|
|
|
|
|
|
|
const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
|
|
|
|
|
2022-04-21 06:00:08 +00:00
|
|
|
const getTime = useTimeSinceFirstRender()
|
|
|
|
|
2022-04-09 23:10:58 +00:00
|
|
|
useEffect(() => {
|
2022-04-20 21:59:34 +00:00
|
|
|
if (
|
|
|
|
initialContracts &&
|
|
|
|
initialBets &&
|
|
|
|
initialComments &&
|
|
|
|
yourBetContractIds
|
|
|
|
) {
|
2022-04-09 23:10:58 +00:00
|
|
|
const eligibleContracts = initialContracts.filter(
|
|
|
|
(c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
|
|
|
|
)
|
|
|
|
const contracts = getAlgoFeed(
|
|
|
|
eligibleContracts,
|
|
|
|
initialBets,
|
|
|
|
initialComments,
|
|
|
|
yourBetContractIds,
|
|
|
|
seenContracts
|
|
|
|
)
|
|
|
|
setAlgoFeed(contracts)
|
2022-04-21 06:00:08 +00:00
|
|
|
trackLatency('feed', getTime())
|
2022-04-09 23:10:58 +00:00
|
|
|
}
|
|
|
|
}, [
|
|
|
|
initialBets,
|
|
|
|
initialComments,
|
|
|
|
initialContracts,
|
|
|
|
seenContracts,
|
|
|
|
yourBetContractIds,
|
2022-04-21 06:00:08 +00:00
|
|
|
getTime,
|
2022-04-09 23:10:58 +00:00
|
|
|
])
|
|
|
|
|
|
|
|
return algoFeed
|
|
|
|
}
|
|
|
|
|
|
|
|
const getAlgoFeed = (
|
|
|
|
contracts: Contract[],
|
|
|
|
recentBets: Bet[],
|
|
|
|
recentComments: Comment[],
|
|
|
|
yourBetContractIds: string[],
|
|
|
|
seenContracts: { [contractId: string]: number }
|
|
|
|
) => {
|
|
|
|
const contractsById = _.keyBy(contracts, (c) => c.id)
|
|
|
|
|
|
|
|
const recommended = getRecommendedContracts(contractsById, yourBetContractIds)
|
|
|
|
const confidence = logInterpolation(0, 100, yourBetContractIds.length)
|
|
|
|
const recommendedScores = _.fromPairs(
|
|
|
|
recommended.map((c, index) => {
|
|
|
|
const score = 1 - index / recommended.length
|
|
|
|
const withConfidence = score * confidence + (1 - confidence)
|
|
|
|
return [c.id, withConfidence] as [string, number]
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
const seenScores = _.fromPairs(
|
|
|
|
contracts.map(
|
|
|
|
(c) => [c.id, getSeenContractsScore(c, seenContracts)] as [string, number]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
const activityScores = getContractsActivityScores(
|
|
|
|
contracts,
|
|
|
|
recentComments,
|
|
|
|
recentBets,
|
|
|
|
seenContracts
|
|
|
|
)
|
|
|
|
|
|
|
|
const combinedScores = contracts.map((contract) => {
|
|
|
|
const score =
|
|
|
|
(recommendedScores[contract.id] ?? 0) *
|
|
|
|
(seenScores[contract.id] ?? 0) *
|
|
|
|
(activityScores[contract.id] ?? 0)
|
|
|
|
return { contract, score }
|
|
|
|
})
|
|
|
|
|
|
|
|
const sorted = _.sortBy(combinedScores, (c) => -c.score)
|
|
|
|
return sorted.map((c) => c.contract).slice(0, MAX_FEED_CONTRACTS)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getContractsActivityScores(
|
|
|
|
contracts: Contract[],
|
|
|
|
recentComments: Comment[],
|
|
|
|
recentBets: Bet[],
|
|
|
|
seenContracts: { [contractId: string]: number }
|
|
|
|
) {
|
|
|
|
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
|
|
|
const contractMostRecentBet = _.mapValues(
|
|
|
|
contractBets,
|
|
|
|
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
|
|
|
|
)
|
|
|
|
|
|
|
|
const contractComments = _.groupBy(
|
|
|
|
recentComments,
|
|
|
|
(comment) => comment.contractId
|
|
|
|
)
|
|
|
|
const contractMostRecentComment = _.mapValues(
|
|
|
|
contractComments,
|
|
|
|
(comments) => _.maxBy(comments, (c) => c.createdTime) as Comment
|
|
|
|
)
|
|
|
|
|
|
|
|
const scoredContracts = contracts.map((contract) => {
|
2022-04-20 21:36:41 +00:00
|
|
|
const { outcomeType } = contract
|
|
|
|
|
2022-04-09 23:10:58 +00:00
|
|
|
const seenTime = seenContracts[contract.id]
|
|
|
|
const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
|
|
|
|
const hasNewComments =
|
|
|
|
!seenTime || (lastCommentTime && lastCommentTime > seenTime)
|
2022-04-20 21:36:41 +00:00
|
|
|
const newCommentScore = hasNewComments ? 1 : 0.5
|
2022-04-09 23:10:58 +00:00
|
|
|
|
|
|
|
const commentCount = contractComments[contract.id]?.length ?? 0
|
|
|
|
const betCount = contractBets[contract.id]?.length ?? 0
|
|
|
|
const activtyCount = betCount + commentCount * 5
|
|
|
|
const activityCountScore =
|
|
|
|
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
|
|
|
|
|
2022-04-27 20:25:54 +00:00
|
|
|
const { volume7Days, volume } = contract
|
|
|
|
const combinedVolume = Math.log(volume7Days + 1) + Math.log(volume + 1)
|
|
|
|
const volumeScore = 0.5 + 0.5 * logInterpolation(4, 25, combinedVolume)
|
|
|
|
|
2022-04-20 21:36:41 +00:00
|
|
|
const lastBetTime =
|
|
|
|
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
|
|
|
|
const timeSinceLastBet = Date.now() - lastBetTime
|
|
|
|
const daysAgo = timeSinceLastBet / DAY_MS
|
2022-04-09 23:10:58 +00:00
|
|
|
const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo)
|
|
|
|
|
2022-04-20 21:36:41 +00:00
|
|
|
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 =
|
2022-04-27 20:25:54 +00:00
|
|
|
newCommentScore *
|
|
|
|
activityCountScore *
|
|
|
|
volumeScore *
|
|
|
|
timeAgoScore *
|
|
|
|
probScore
|
2022-04-09 23:10:58 +00:00
|
|
|
|
|
|
|
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
|
|
|
|
const mappedScore = 0.5 + score / 2
|
2022-04-20 21:36:41 +00:00
|
|
|
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]
|
2022-04-09 23:10:58 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return _.fromPairs(scoredContracts)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getSeenContractsScore(
|
|
|
|
contract: Contract,
|
|
|
|
seenContracts: { [contractId: string]: number }
|
|
|
|
) {
|
|
|
|
const lastSeen = seenContracts[contract.id]
|
|
|
|
if (lastSeen === undefined) {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
2022-04-20 21:36:41 +00:00
|
|
|
const daysAgo = (Date.now() - lastSeen) / DAY_MS
|
2022-04-09 23:10:58 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|