Flag incorrectly resolved markets, warn about unreliable creators

This commit is contained in:
Pico2x 2022-09-27 19:36:38 -04:00
parent a12ed78813
commit d9ccdce8d4
8 changed files with 115 additions and 18 deletions

View File

@ -62,6 +62,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
featuredOnHomeRank?: number featuredOnHomeRank?: number
likedByUserIds?: string[] likedByUserIds?: string[]
likedByUserCount?: number likedByUserCount?: number
resolutionReports?: string[]
} & T } & T
export type BinaryContract = Contract & Binary export type BinaryContract = Contract & Binary

View File

@ -33,6 +33,8 @@ export type User = {
allTime: number allTime: number
} }
correctResolutionPercentageCached: number
nextLoanCached: number nextLoanCached: number
followerCountCached: number followerCountCached: number

View File

@ -69,6 +69,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
followerCountCached: 0, followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES, followedCategories: DEFAULT_CATEGORIES,
shouldShowWelcome: true, shouldShowWelcome: true,
correctResolutionPercentageCached: 1,
} }
await firestore.collection('users').doc(auth.uid).create(user) await firestore.collection('users').doc(auth.uid).create(user)

View File

@ -116,6 +116,25 @@ export async function updateMetricsCore() {
lastPortfolio.investmentValue !== newPortfolio.investmentValue lastPortfolio.investmentValue !== newPortfolio.investmentValue
const newProfit = calculateNewProfit(portfolioHistory, newPortfolio) const newProfit = calculateNewProfit(portfolioHistory, newPortfolio)
const contractRatios = userContracts
.map((contract) => {
if (
!contract.resolutionReports ||
contract.resolutionReports?.length === 0
) {
return 0
}
const contractRatio =
contract.resolutionReports.length / (contract.uniqueBettorCount ?? 1)
return contractRatio
})
.filter((ratio) => ratio > 0)
const badResolutions = contractRatios.filter(
(ratio) => ratio > BAD_RESOLUTION_THRESHOLD
)
const newCorrectResolutionPercentage =
(userContracts.length - badResolutions.length) / userContracts.length
return { return {
user, user,
@ -123,6 +142,7 @@ export async function updateMetricsCore() {
newPortfolio, newPortfolio,
newProfit, newProfit,
didPortfolioChange, didPortfolioChange,
newCorrectResolutionPercentage,
} }
}) })
@ -144,6 +164,7 @@ export async function updateMetricsCore() {
newPortfolio, newPortfolio,
newProfit, newProfit,
didPortfolioChange, didPortfolioChange,
newCorrectResolutionPercentage,
}) => { }) => {
const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0 const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0
return { return {
@ -153,6 +174,7 @@ export async function updateMetricsCore() {
creatorVolumeCached: newCreatorVolume, creatorVolumeCached: newCreatorVolume,
profitCached: newProfit, profitCached: newProfit,
nextLoanCached, nextLoanCached,
correctResolutionPercentageCached: newCorrectResolutionPercentage,
}, },
}, },
@ -224,3 +246,5 @@ const topUserScores = (scores: { [userId: string]: number }) => {
} }
type GroupContractDoc = { contractId: string; createdTime: number } type GroupContractDoc = { contractId: string; createdTime: number }
const BAD_RESOLUTION_THRESHOLD = 0.1

View File

@ -68,10 +68,7 @@ export function ConfirmationButton(props: {
</Row> </Row>
</Col> </Col>
</Modal> </Modal>
<div <div className={openModalBtn.className} onClick={() => updateOpen(true)}>
className={clsx('btn', openModalBtn.className)}
onClick={() => updateOpen(true)}
>
{openModalBtn.icon} {openModalBtn.icon}
{openModalBtn.label} {openModalBtn.label}
</div> </div>
@ -91,7 +88,7 @@ export function ResolveConfirmationButton(props: {
<ConfirmationButton <ConfirmationButton
openModalBtn={{ openModalBtn={{
className: clsx( className: clsx(
'border-none self-start', 'btn border-none self-start',
openModalButtonClass, openModalButtonClass,
isSubmitting && 'btn-disabled loading' isSubmitting && 'btn-disabled loading'
), ),

View File

@ -35,6 +35,7 @@ import { getMappedValue } from 'common/pseudo-numeric'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { ProbChange } from './prob-change-table' import { ProbChange } from './prob-change-table'
import { ContractReportResolution } from './contract-report-resolution'
export function ContractCard(props: { export function ContractCard(props: {
contract: Contract contract: Contract
@ -213,17 +214,20 @@ export function BinaryResolutionOrChance(props: {
return ( return (
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}> <Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
{resolution ? ( {resolution ? (
<> <Row className="flex items-start">
<div <div>
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')} <div
> className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
Resolved >
Resolved
</div>
<BinaryContractOutcomeLabel
contract={contract}
resolution={resolution}
/>
</div> </div>
<BinaryContractOutcomeLabel <ContractReportResolution contract={contract} />
contract={contract} </Row>
resolution={resolution}
/>
</>
) : ( ) : (
<> <>
{probAfter && probChanged ? ( {probAfter && probChanged ? (

View File

@ -14,7 +14,7 @@ import { useState } from 'react'
import NewContractBadge from '../new-contract-badge' import NewContractBadge from '../new-contract-badge'
import { MiniUserFollowButton } from '../follow-button' import { MiniUserFollowButton } from '../follow-button'
import { DAY_MS } from 'common/util/time' import { DAY_MS } from 'common/util/time'
import { useUser } from 'web/hooks/use-user' import { useUser, useUserById } from 'web/hooks/use-user'
import { exhibitExts } from 'common/util/parse' import { exhibitExts } from 'common/util/parse'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
@ -28,7 +28,7 @@ import { UserLink } from 'web/components/user-link'
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge' import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
import { Tooltip } from 'web/components/tooltip' import { Tooltip } from 'web/components/tooltip'
import { ExtraContractActionsRow } from './extra-contract-actions-row' import { ExtraContractActionsRow } from './extra-contract-actions-row'
import { PlusCircleIcon } from '@heroicons/react/solid' import { ExclamationIcon, PlusCircleIcon } from '@heroicons/react/solid'
import { GroupLink } from 'common/group' import { GroupLink } from 'common/group'
import { Subtitle } from '../subtitle' import { Subtitle } from '../subtitle'
import { useIsMobile } from 'web/hooks/use-is-mobile' import { useIsMobile } from 'web/hooks/use-is-mobile'
@ -142,6 +142,8 @@ export function MarketSubheader(props: {
const { creatorName, creatorUsername, creatorId, creatorAvatarUrl } = contract const { creatorName, creatorUsername, creatorId, creatorAvatarUrl } = contract
const { resolvedDate } = contractMetrics(contract) const { resolvedDate } = contractMetrics(contract)
const user = useUser() const user = useUser()
const correctResolutionPercentage =
useUserById(creatorId)?.correctResolutionPercentageCached
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isMobile = useIsMobile() const isMobile = useIsMobile()
return ( return (
@ -153,13 +155,14 @@ export function MarketSubheader(props: {
size={9} size={9}
className="mr-1.5" className="mr-1.5"
/> />
{!disabled && ( {!disabled && (
<div className="absolute mt-3 ml-[11px]"> <div className="absolute mt-3 ml-[11px]">
<MiniUserFollowButton userId={creatorId} /> <MiniUserFollowButton userId={creatorId} />
</div> </div>
)} )}
<Col className="text-greyscale-6 ml-2 flex-1 flex-wrap text-sm"> <Col className="text-greyscale-6 ml-2 flex-1 flex-wrap text-sm">
<Row className="w-full justify-between "> <Row className="w-full space-x-1 ">
{disabled ? ( {disabled ? (
creatorName creatorName
) : ( ) : (
@ -170,6 +173,12 @@ export function MarketSubheader(props: {
short={isMobile} short={isMobile}
/> />
)} )}
{correctResolutionPercentage != null &&
correctResolutionPercentage < BAD_CREATOR_THRESHOLD && (
<Tooltip text="This creator has a track record of creating contracts that are resolved incorrectly.">
<ExclamationIcon className="h-6 w-6 text-yellow-500" />
</Tooltip>
)}
</Row> </Row>
<Row className="text-2xs text-greyscale-4 gap-2 sm:text-xs"> <Row className="text-2xs text-greyscale-4 gap-2 sm:text-xs">
<CloseOrResolveTime <CloseOrResolveTime
@ -445,3 +454,5 @@ function EditableCloseDate(props: {
</> </>
) )
} }
const BAD_CREATOR_THRESHOLD = 0.8

View File

@ -0,0 +1,57 @@
import { Contract } from 'common/contract'
import { useUser } from 'web/hooks/use-user'
import clsx from 'clsx'
import { updateContract } from 'web/lib/firebase/contracts'
import { Tooltip } from '../tooltip'
import { ConfirmationButton } from '../confirmation-button'
import { Row } from '../layout/row'
import { FlagIcon } from '@heroicons/react/solid'
export function ContractReportResolution(props: { contract: Contract }) {
const { contract } = props
const user = useUser()
if (!user) {
return <></>
}
const userReported = contract.resolutionReports?.includes(user.id)
const onSubmit = async () => {
if (!user) {
return true
}
await updateContract(contract.id, {
resolutionReports: [...(contract.resolutionReports || []), user.id],
})
return true
}
const flagClass = clsx(
'mx-2 flex flex-col items-center gap-1 w-6 h-6 text-gray-500 rounded-md bg-gray-100 px-2 py-1 hover:bg-gray-300',
userReported && 'text-red-500'
)
return (
<Tooltip text="Flag this market as incorrectly resolved">
<ConfirmationButton
openModalBtn={{
label: '',
icon: <FlagIcon className="h-5 w-5" />,
className: flagClass,
}}
cancelBtn={{
label: 'Cancel',
className: 'border-none btn-sm btn-ghost self-center',
}}
submitBtn={{
label: 'Submit',
className: 'btn-secondary',
}}
onSubmitWithSuccess={onSubmit}
>
<Row className="items-center text-xl">
Flag this market as incorrectly resolved
</Row>
</ConfirmationButton>
</Tooltip>
)
}