Implement resolving to multiple answers, weighted by probability
This commit is contained in:
parent
42f88766b3
commit
fd846254a0
|
@ -32,7 +32,8 @@ export type Contract = {
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
resolutionTime?: number // When the contract creator resolved the market
|
resolutionTime?: number // When the contract creator resolved the market
|
||||||
resolution?: string
|
resolution?: string
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||||
|
resolutions?: { [outcome: string]: number } // Used for outcomeType FREE_RESPONSE resolved to MKT
|
||||||
closeEmailsSent?: number
|
closeEmailsSent?: number
|
||||||
|
|
||||||
volume24Hours: number
|
volume24Hours: number
|
||||||
|
|
|
@ -118,3 +118,46 @@ export const getPayouts = (
|
||||||
return getStandardPayouts(outcome, contract, bets)
|
return getStandardPayouts(outcome, contract, bets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPayoutsMultiOutcome = (
|
||||||
|
resolutions: { [outcome: string]: number },
|
||||||
|
contract: Contract,
|
||||||
|
bets: Bet[]
|
||||||
|
) => {
|
||||||
|
const poolTotal = _.sum(Object.values(contract.pool))
|
||||||
|
const winningBets = bets.filter((bet) => resolutions[bet.outcome])
|
||||||
|
|
||||||
|
const betsByOutcome = _.groupBy(winningBets, (bet) => bet.outcome)
|
||||||
|
const sharesByOutcome = _.mapValues(betsByOutcome, (bets) =>
|
||||||
|
_.sumBy(bets, (bet) => bet.shares)
|
||||||
|
)
|
||||||
|
|
||||||
|
const probTotal = _.sum(Object.values(resolutions))
|
||||||
|
|
||||||
|
const payouts = winningBets.map(({ userId, outcome, amount, shares }) => {
|
||||||
|
const prob = resolutions[outcome] / probTotal
|
||||||
|
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
|
||||||
|
const profit = winnings - amount
|
||||||
|
|
||||||
|
const payout = amount + (1 - FEES) * Math.max(0, profit)
|
||||||
|
return { userId, profit, payout }
|
||||||
|
})
|
||||||
|
|
||||||
|
const profits = _.sumBy(payouts, (po) => po.profit)
|
||||||
|
const creatorPayout = CREATOR_FEE * profits
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'resolved',
|
||||||
|
resolutions,
|
||||||
|
'pool',
|
||||||
|
poolTotal,
|
||||||
|
'profits',
|
||||||
|
profits,
|
||||||
|
'creator fee',
|
||||||
|
creatorPayout
|
||||||
|
)
|
||||||
|
|
||||||
|
return payouts
|
||||||
|
.map(({ userId, payout }) => ({ userId, payout }))
|
||||||
|
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,13 @@ type market_resolved_template = {
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const toDisplayResolution = (outcome: string, prob: number) => {
|
const toDisplayResolution = (
|
||||||
|
outcome: string,
|
||||||
|
prob: number,
|
||||||
|
resolutions?: { [outcome: string]: number }
|
||||||
|
) => {
|
||||||
|
if (outcome === 'MKT' && resolutions) return 'MULTI'
|
||||||
|
|
||||||
const display = {
|
const display = {
|
||||||
YES: 'YES',
|
YES: 'YES',
|
||||||
NO: 'NO',
|
NO: 'NO',
|
||||||
|
@ -33,8 +39,9 @@ export const sendMarketResolutionEmail = async (
|
||||||
payout: number,
|
payout: number,
|
||||||
creator: User,
|
creator: User,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string,
|
resolution: string,
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number,
|
||||||
|
resolutions?: { [outcome: string]: number }
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
if (
|
if (
|
||||||
|
@ -49,7 +56,7 @@ export const sendMarketResolutionEmail = async (
|
||||||
|
|
||||||
const prob = resolutionProbability ?? getProbability(contract.totalShares)
|
const prob = resolutionProbability ?? getProbability(contract.totalShares)
|
||||||
|
|
||||||
const outcome = toDisplayResolution(resolution, prob)
|
const outcome = toDisplayResolution(resolution, prob, resolutions)
|
||||||
|
|
||||||
const subject = `Resolved ${outcome}: ${contract.question}`
|
const subject = `Resolved ${outcome}: ${contract.question}`
|
||||||
|
|
||||||
|
|
|
@ -7,23 +7,24 @@ import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, payUser } from './utils'
|
import { getUser, payUser } from './utils'
|
||||||
import { sendMarketResolutionEmail } from './emails'
|
import { sendMarketResolutionEmail } from './emails'
|
||||||
import { getPayouts } from '../../common/payouts'
|
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
|
||||||
|
|
||||||
export const resolveMarket = functions
|
export const resolveMarket = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
.https.onCall(
|
.https.onCall(
|
||||||
async (
|
async (
|
||||||
data: {
|
data: {
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
outcome: string
|
||||||
contractId: string
|
contractId: string
|
||||||
probabilityInt?: number
|
probabilityInt?: number
|
||||||
|
resolutions?: { [outcome: string]: number }
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const userId = context?.auth?.uid
|
const userId = context?.auth?.uid
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
const { outcome, contractId, probabilityInt } = data
|
const { outcome, contractId, probabilityInt, resolutions } = data
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await contractDoc.get()
|
const contractSnap = await contractDoc.get()
|
||||||
|
@ -36,7 +37,11 @@ export const resolveMarket = functions
|
||||||
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
if (outcome !== 'CANCEL' && isNaN(+outcome))
|
if (
|
||||||
|
isNaN(+outcome) &&
|
||||||
|
!(outcome === 'MKT' && resolutions) &&
|
||||||
|
outcome !== 'CANCEL'
|
||||||
|
)
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
} else {
|
} else {
|
||||||
return { status: 'error', message: 'Invalid contract outcomeType' }
|
return { status: 'error', message: 'Invalid contract outcomeType' }
|
||||||
|
@ -70,6 +75,7 @@ export const resolveMarket = functions
|
||||||
...(resolutionProbability === undefined
|
...(resolutionProbability === undefined
|
||||||
? {}
|
? {}
|
||||||
: { resolutionProbability }),
|
: { resolutionProbability }),
|
||||||
|
...(resolutions === undefined ? {} : { resolutions }),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('contract ', contractId, 'resolved to:', outcome)
|
console.log('contract ', contractId, 'resolved to:', outcome)
|
||||||
|
@ -81,12 +87,10 @@ export const resolveMarket = functions
|
||||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
|
||||||
const payouts = getPayouts(
|
const payouts =
|
||||||
outcome,
|
outcomeType === 'FREE_RESPONSE' && resolutions
|
||||||
contract,
|
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
||||||
openBets,
|
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
||||||
resolutionProbability
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('payouts:', payouts)
|
console.log('payouts:', payouts)
|
||||||
|
|
||||||
|
@ -109,7 +113,8 @@ export const resolveMarket = functions
|
||||||
creator,
|
creator,
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
resolutionProbability
|
resolutionProbability,
|
||||||
|
resolutions
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -121,8 +126,9 @@ const sendResolutionEmails = async (
|
||||||
userPayouts: { [userId: string]: number },
|
userPayouts: { [userId: string]: number },
|
||||||
creator: User,
|
creator: User,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string,
|
outcome: string,
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number,
|
||||||
|
resolutions?: { [outcome: string]: number }
|
||||||
) => {
|
) => {
|
||||||
const nonWinners = _.difference(
|
const nonWinners = _.difference(
|
||||||
_.uniq(openBets.map(({ userId }) => userId)),
|
_.uniq(openBets.map(({ userId }) => userId)),
|
||||||
|
@ -140,7 +146,8 @@ const sendResolutionEmails = async (
|
||||||
creator,
|
creator,
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
resolutionProbability
|
resolutionProbability,
|
||||||
|
resolutions
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
import { XIcon } from '@heroicons/react/solid'
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
|
@ -34,18 +34,22 @@ import { Bet } from '../../common/bet'
|
||||||
import { useAnswers } from '../hooks/use-answers'
|
import { useAnswers } from '../hooks/use-answers'
|
||||||
import { ResolveConfirmationButton } from './confirmation-button'
|
import { ResolveConfirmationButton } from './confirmation-button'
|
||||||
import { tradingAllowed } from '../lib/firebase/contracts'
|
import { tradingAllowed } from '../lib/firebase/contracts'
|
||||||
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
|
||||||
export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { creatorId, resolution } = contract
|
const { creatorId, resolution, resolutions } = contract
|
||||||
|
|
||||||
const answers = useAnswers(contract.id) ?? props.answers
|
const answers = useAnswers(contract.id) ?? props.answers
|
||||||
const [chosenAnswer, otherAnswers] = _.partition(
|
const [winningAnswers, otherAnswers] = _.partition(
|
||||||
answers.filter((answer) => answer.id !== '0'),
|
answers.filter((answer) => answer.id !== '0'),
|
||||||
(answer) => answer.id === resolution
|
(answer) =>
|
||||||
|
answer.id === resolution || (resolutions && resolutions[answer.id])
|
||||||
)
|
)
|
||||||
const sortedAnswers = [
|
const sortedAnswers = [
|
||||||
...chosenAnswer,
|
..._.sortBy(winningAnswers, (answer) =>
|
||||||
|
resolutions ? -1 * resolutions[answer.id] : 0
|
||||||
|
),
|
||||||
..._.sortBy(
|
..._.sortBy(
|
||||||
otherAnswers,
|
otherAnswers,
|
||||||
(answer) => -1 * getOutcomeProbability(contract.totalShares, answer.id)
|
(answer) => -1 * getOutcomeProbability(contract.totalShares, answer.id)
|
||||||
|
@ -55,13 +59,46 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const [resolveOption, setResolveOption] = useState<
|
const [resolveOption, setResolveOption] = useState<
|
||||||
'CHOOSE' | 'CANCEL' | undefined
|
'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
>()
|
>()
|
||||||
const [answerChoice, setAnswerChoice] = useState<string | undefined>()
|
const [chosenAnswers, setChosenAnswers] = useState<{
|
||||||
|
[answerId: string]: number
|
||||||
|
}>({})
|
||||||
|
|
||||||
useEffect(() => {
|
const chosenTotal = _.sum(Object.values(chosenAnswers))
|
||||||
if (resolveOption !== 'CHOOSE' && answerChoice) setAnswerChoice(undefined)
|
|
||||||
}, [answerChoice, resolveOption])
|
const onChoose = (answerId: string, prob: number) => {
|
||||||
|
if (resolveOption === 'CHOOSE') {
|
||||||
|
setChosenAnswers({ [answerId]: prob })
|
||||||
|
} else {
|
||||||
|
setChosenAnswers((chosenAnswers) => {
|
||||||
|
return {
|
||||||
|
...chosenAnswers,
|
||||||
|
[answerId]: prob,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeselect = (answerId: string) => {
|
||||||
|
setChosenAnswers((chosenAnswers) => {
|
||||||
|
const newChosenAnswers = { ...chosenAnswers }
|
||||||
|
delete newChosenAnswers[answerId]
|
||||||
|
return newChosenAnswers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setChosenAnswers({})
|
||||||
|
}, [resolveOption])
|
||||||
|
|
||||||
|
const showChoice = resolution
|
||||||
|
? undefined
|
||||||
|
: resolveOption === 'CHOOSE'
|
||||||
|
? 'radio'
|
||||||
|
: resolveOption === 'CHOOSE_MULTIPLE'
|
||||||
|
? 'checkbox'
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-3">
|
<Col className="gap-3">
|
||||||
|
@ -70,9 +107,11 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
key={answer.id}
|
key={answer.id}
|
||||||
answer={answer}
|
answer={answer}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
showChoice={!resolution && resolveOption === 'CHOOSE'}
|
showChoice={showChoice}
|
||||||
isChosen={answer.id === answerChoice}
|
chosenProb={chosenAnswers[answer.id]}
|
||||||
onChoose={() => setAnswerChoice(answer.id)}
|
totalChosenProb={chosenTotal}
|
||||||
|
onChoose={onChoose}
|
||||||
|
onDeselect={onDeselect}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
@ -85,14 +124,16 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tradingAllowed(contract) && <CreateAnswerInput contract={contract} />}
|
{tradingAllowed(contract) && !resolveOption && (
|
||||||
|
<CreateAnswerInput contract={contract} />
|
||||||
|
)}
|
||||||
|
|
||||||
{user?.id === creatorId && !resolution && (
|
{user?.id === creatorId && !resolution && (
|
||||||
<AnswerResolvePanel
|
<AnswerResolvePanel
|
||||||
contract={contract}
|
contract={contract}
|
||||||
resolveOption={resolveOption}
|
resolveOption={resolveOption}
|
||||||
setResolveOption={setResolveOption}
|
setResolveOption={setResolveOption}
|
||||||
answer={answerChoice}
|
chosenAnswers={chosenAnswers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -102,18 +143,31 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
function AnswerItem(props: {
|
function AnswerItem(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: Contract
|
contract: Contract
|
||||||
showChoice: boolean
|
showChoice: 'radio' | 'checkbox' | undefined
|
||||||
isChosen: boolean
|
chosenProb: number | undefined
|
||||||
onChoose: () => void
|
totalChosenProb?: number
|
||||||
|
onChoose: (answerId: string, prob: number) => void
|
||||||
|
onDeselect: (answerId: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { answer, contract, showChoice, isChosen, onChoose } = props
|
const {
|
||||||
const { resolution, totalShares } = contract
|
answer,
|
||||||
|
contract,
|
||||||
|
showChoice,
|
||||||
|
chosenProb,
|
||||||
|
totalChosenProb,
|
||||||
|
onChoose,
|
||||||
|
onDeselect,
|
||||||
|
} = props
|
||||||
|
const { resolution, resolutions, totalShares } = contract
|
||||||
const { username, avatarUrl, name, createdTime, number, text } = answer
|
const { username, avatarUrl, name, createdTime, number, text } = answer
|
||||||
|
const isChosen = chosenProb !== undefined
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
const createdDate = dayjs(createdTime).format('MMM D')
|
||||||
const prob = getOutcomeProbability(totalShares, answer.id)
|
const prob = getOutcomeProbability(totalShares, answer.id)
|
||||||
|
const roundedProb = Math.round(prob * 100)
|
||||||
const probPercent = formatPercent(prob)
|
const probPercent = formatPercent(prob)
|
||||||
const wasResolvedTo = resolution === answer.id
|
const wasResolvedTo =
|
||||||
|
resolution === answer.id || (resolutions && resolutions[answer.id])
|
||||||
|
|
||||||
const [isBetting, setIsBetting] = useState(false)
|
const [isBetting, setIsBetting] = useState(false)
|
||||||
|
|
||||||
|
@ -122,10 +176,14 @@ function AnswerItem(props: {
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'p-4 sm:flex-row rounded gap-4',
|
'p-4 sm:flex-row rounded gap-4',
|
||||||
wasResolvedTo
|
wasResolvedTo
|
||||||
? 'bg-green-50 mb-8'
|
? resolution === 'MKT'
|
||||||
: isChosen
|
? 'bg-blue-50 mb-2'
|
||||||
|
: 'bg-green-50 mb-8'
|
||||||
|
: chosenProb === undefined
|
||||||
|
? 'bg-gray-50'
|
||||||
|
: showChoice === 'radio'
|
||||||
? 'bg-green-50'
|
? 'bg-green-50'
|
||||||
: 'bg-gray-50'
|
: 'bg-blue-50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Col className="gap-3 flex-1">
|
<Col className="gap-3 flex-1">
|
||||||
|
@ -158,8 +216,24 @@ function AnswerItem(props: {
|
||||||
closePanel={() => setIsBetting(false)}
|
closePanel={() => setIsBetting(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Row className="self-end sm:self-start items-center gap-4">
|
<Row className="self-end sm:self-start items-center gap-4 justify-end">
|
||||||
{!wasResolvedTo && (
|
{!wasResolvedTo &&
|
||||||
|
(showChoice === 'checkbox' ? (
|
||||||
|
<input
|
||||||
|
className="input input-bordered text-2xl justify-self-end w-24"
|
||||||
|
type="number"
|
||||||
|
placeholder={`${roundedProb}`}
|
||||||
|
maxLength={9}
|
||||||
|
value={chosenProb ? Math.round(chosenProb) : ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target
|
||||||
|
const numberValue = value
|
||||||
|
? parseInt(value.replace(/[^\d]/, ''))
|
||||||
|
: 0
|
||||||
|
if (!isNaN(numberValue)) onChoose(answer.id, numberValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'text-2xl',
|
'text-2xl',
|
||||||
|
@ -168,20 +242,45 @@ function AnswerItem(props: {
|
||||||
>
|
>
|
||||||
{probPercent}
|
{probPercent}
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
{showChoice ? (
|
{showChoice ? (
|
||||||
<div className="form-control py-1">
|
<div className="form-control py-1">
|
||||||
<label className="cursor-pointer label gap-2">
|
<label className="cursor-pointer label gap-3">
|
||||||
<span className="label-text">Choose this answer</span>
|
<span className="">Choose this answer</span>
|
||||||
|
{showChoice === 'radio' && (
|
||||||
<input
|
<input
|
||||||
className={clsx('radio', isChosen && '!bg-green-500')}
|
className={clsx('radio', chosenProb && '!bg-green-500')}
|
||||||
type="radio"
|
type="radio"
|
||||||
name="opt"
|
name="opt"
|
||||||
checked={isChosen}
|
checked={isChosen}
|
||||||
onChange={onChoose}
|
onChange={() => onChoose(answer.id, 1)}
|
||||||
value={answer.id}
|
value={answer.id}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{showChoice === 'checkbox' && (
|
||||||
|
<input
|
||||||
|
className={clsx('checkbox', chosenProb && '!bg-blue-500')}
|
||||||
|
type="checkbox"
|
||||||
|
name="opt"
|
||||||
|
checked={isChosen}
|
||||||
|
onChange={() => {
|
||||||
|
if (isChosen) onDeselect(answer.id)
|
||||||
|
else {
|
||||||
|
onChoose(answer.id, 100 * prob)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={answer.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
{showChoice === 'checkbox' && (
|
||||||
|
<div className="ml-1">
|
||||||
|
{chosenProb && totalChosenProb
|
||||||
|
? Math.round((100 * chosenProb) / totalChosenProb)
|
||||||
|
: 0}
|
||||||
|
% share
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -195,7 +294,17 @@ function AnswerItem(props: {
|
||||||
)}
|
)}
|
||||||
{wasResolvedTo && (
|
{wasResolvedTo && (
|
||||||
<Col className="items-end">
|
<Col className="items-end">
|
||||||
<div className="text-green-700 text-xl">Chosen</div>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'text-xl',
|
||||||
|
resolution === 'MKT' ? 'text-blue-700' : 'text-green-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Chosen{' '}
|
||||||
|
{resolutions
|
||||||
|
? `${Math.round(resolutions[answer.id])}%`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
<div className="text-2xl text-gray-500">{probPercent}</div>
|
<div className="text-2xl text-gray-500">{probPercent}</div>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
@ -482,29 +591,48 @@ function CreateAnswerInput(props: { contract: Contract }) {
|
||||||
|
|
||||||
function AnswerResolvePanel(props: {
|
function AnswerResolvePanel(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
resolveOption: 'CHOOSE' | 'CANCEL' | undefined
|
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
setResolveOption: (option: 'CHOOSE' | 'CANCEL' | undefined) => void
|
setResolveOption: (
|
||||||
answer: string | undefined
|
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
|
) => void
|
||||||
|
chosenAnswers: { [answerId: string]: number }
|
||||||
}) {
|
}) {
|
||||||
const { contract, resolveOption, setResolveOption, answer } = props
|
const { contract, resolveOption, setResolveOption, chosenAnswers } = props
|
||||||
|
const answers = Object.keys(chosenAnswers)
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [error, setError] = useState<string | undefined>(undefined)
|
const [error, setError] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const onResolve = async () => {
|
const onResolve = async () => {
|
||||||
if (resolveOption === 'CHOOSE' && answer === undefined) return
|
if (resolveOption === 'CHOOSE' && answers.length !== 1) return
|
||||||
|
if (resolveOption === 'CHOOSE_MULTIPLE' && answers.length < 2) return
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await resolveMarket({
|
const totalProb = _.sum(Object.values(chosenAnswers))
|
||||||
outcome: resolveOption === 'CHOOSE' ? (answer as string) : 'CANCEL',
|
const normalizedProbs = _.mapValues(
|
||||||
contractId: contract.id,
|
chosenAnswers,
|
||||||
}).then((r) => r.data as any)
|
(prob) => (100 * prob) / totalProb
|
||||||
|
)
|
||||||
|
|
||||||
console.log('resolved', `#${answer}`, 'result:', result)
|
const resolutionProps = removeUndefinedProps({
|
||||||
|
outcome:
|
||||||
|
resolveOption === 'CHOOSE'
|
||||||
|
? answers[0]
|
||||||
|
: resolveOption === 'CHOOSE_MULTIPLE'
|
||||||
|
? 'MKT'
|
||||||
|
: 'CANCEL',
|
||||||
|
resolutions:
|
||||||
|
resolveOption === 'CHOOSE_MULTIPLE' ? normalizedProbs : undefined,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await resolveMarket(resolutionProps).then((r) => r.data)
|
||||||
|
|
||||||
|
console.log('resolved', resolutionProps, 'result:', result)
|
||||||
|
|
||||||
if (result?.status !== 'success') {
|
if (result?.status !== 'success') {
|
||||||
setError(result?.error || 'Error resolving market')
|
setError(result?.message || 'Error resolving market')
|
||||||
}
|
}
|
||||||
setResolveOption(undefined)
|
setResolveOption(undefined)
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
@ -513,8 +641,12 @@ function AnswerResolvePanel(props: {
|
||||||
const resolutionButtonClass =
|
const resolutionButtonClass =
|
||||||
resolveOption === 'CANCEL'
|
resolveOption === 'CANCEL'
|
||||||
? 'bg-yellow-400 hover:bg-yellow-500'
|
? 'bg-yellow-400 hover:bg-yellow-500'
|
||||||
: resolveOption === 'CHOOSE' && answer
|
: resolveOption === 'CHOOSE' && answers.length
|
||||||
? 'btn-primary'
|
? 'btn-primary'
|
||||||
|
: resolveOption === 'CHOOSE_MULTIPLE' &&
|
||||||
|
answers.length > 1 &&
|
||||||
|
answers.every((answer) => chosenAnswers[answer] > 0)
|
||||||
|
? 'bg-blue-400 hover:bg-blue-500'
|
||||||
: 'btn-disabled'
|
: 'btn-disabled'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -85,7 +85,7 @@ export function ResolutionOrChance(props: {
|
||||||
{
|
{
|
||||||
YES: 'YES',
|
YES: 'YES',
|
||||||
NO: 'NO',
|
NO: 'NO',
|
||||||
MKT: getBinaryProbPercent(contract),
|
MKT: isBinary ? getBinaryProbPercent(contract) : 'MULTI',
|
||||||
CANCEL: 'N/A',
|
CANCEL: 'N/A',
|
||||||
'': '',
|
'': '',
|
||||||
}[resolution || ''] ?? `#${resolution}`
|
}[resolution || ''] ?? `#${resolution}`
|
||||||
|
|
|
@ -43,12 +43,12 @@ export function ResolutionPanel(props: {
|
||||||
outcome,
|
outcome,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
probabilityInt: prob,
|
probabilityInt: prob,
|
||||||
}).then((r) => r.data as any)
|
}).then((r) => r.data)
|
||||||
|
|
||||||
console.log('resolved', outcome, 'result:', result)
|
console.log('resolved', outcome, 'result:', result)
|
||||||
|
|
||||||
if (result?.status !== 'success') {
|
if (result?.status !== 'success') {
|
||||||
setError(result?.error || 'Error resolving market')
|
setError(result?.message || 'Error resolving market')
|
||||||
}
|
}
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,8 +91,8 @@ export function YesNoCancelSelector(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChooseCancelSelector(props: {
|
export function ChooseCancelSelector(props: {
|
||||||
selected: 'CHOOSE' | 'CANCEL' | undefined
|
selected: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
onSelect: (selected: 'CHOOSE' | 'CANCEL') => void
|
onSelect: (selected: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL') => void
|
||||||
className?: string
|
className?: string
|
||||||
btnClassName?: string
|
btnClassName?: string
|
||||||
}) {
|
}) {
|
||||||
|
@ -110,6 +110,14 @@ export function ChooseCancelSelector(props: {
|
||||||
Choose an answer
|
Choose an answer
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color={selected === 'CHOOSE_MULTIPLE' ? 'blue' : 'gray'}
|
||||||
|
onClick={() => onSelect('CHOOSE_MULTIPLE')}
|
||||||
|
className={clsx('whitespace-nowrap', btnClassName)}
|
||||||
|
>
|
||||||
|
Choose multiple
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color={selected === 'CANCEL' ? 'yellow' : 'gray'}
|
color={selected === 'CANCEL' ? 'yellow' : 'gray'}
|
||||||
onClick={() => onSelect('CANCEL')}
|
onClick={() => onSelect('CANCEL')}
|
||||||
|
|
|
@ -33,6 +33,7 @@ export const resolveMarket = cloudFunction<
|
||||||
outcome: string
|
outcome: string
|
||||||
contractId: string
|
contractId: string
|
||||||
probabilityInt?: number
|
probabilityInt?: number
|
||||||
|
resolutions?: { [outcome: string]: number }
|
||||||
},
|
},
|
||||||
{ status: 'error' | 'success'; message?: string }
|
{ status: 'error' | 'success'; message?: string }
|
||||||
>('resolveMarket')
|
>('resolveMarket')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user