From fc954b6b4b512160a323d8b0af680c448891e956 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 15 Feb 2022 21:37:34 -0600 Subject: [PATCH] Resolve multi market --- common/calculate.ts | 49 +++++++++++++++++++---------- common/payouts.ts | 16 +++++----- functions/src/emails.ts | 21 ++++++++----- functions/src/resolve-market.ts | 31 +++++++++++------- web/components/answers-panel.tsx | 29 +++++++++++++++-- web/components/resolution-panel.tsx | 4 ++- web/lib/firebase/api-call.ts | 9 +++++- 7 files changed, 111 insertions(+), 48 deletions(-) diff --git a/common/calculate.ts b/common/calculate.ts index d030d14c..b995bf4d 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -179,29 +179,24 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { } function calculateMktPayout(contract: Contract, bet: Bet) { - const { resolutionProbability, totalShares, pool, phantomShares } = contract + if (contract.outcomeType === 'BINARY') + return calculateBinaryMktPayout(contract, bet) + + const { totalShares, pool } = contract as any as Contract<'MULTI'> const totalPool = _.sum(Object.values(pool)) - const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2) + const sharesSquareSum = _.sumBy( + Object.values(totalShares), + (shares) => shares ** 2 + ) - let weightedShareTotal = _.sumBy(Object.keys(totalShares), (outcome) => { - const shareTotals = totalShares as { [outcome: string]: number } - - // Avoid O(n^2) by reusing squareSum for prob. - const prob = shareTotals[outcome] ** 2 / squareSum - const shares = - shareTotals[outcome] - - (phantomShares ? phantomShares[outcome as 'YES' | 'NO'] : 0) + const weightedShareTotal = _.sumBy(Object.keys(totalShares), (outcome) => { + // Avoid O(n^2) by reusing sharesSquareSum for prob. + const shares = totalShares[outcome] + const prob = shares ** 2 / sharesSquareSum return prob * shares }) - // Compute binary case if resolutionProbability provided. - if (resolutionProbability !== undefined) { - weightedShareTotal = - resolutionProbability * (totalShares.YES - phantomShares.YES) + - (1 - resolutionProbability) * (totalShares.NO - phantomShares.NO) - } - const { outcome, amount, shares } = bet const betP = getOutcomeProbability(totalShares, outcome) @@ -210,6 +205,26 @@ function calculateMktPayout(contract: Contract, bet: Bet) { return deductFees(amount, winnings) } +function calculateBinaryMktPayout(contract: Contract, bet: Bet) { + const p = + contract.resolutionProbability !== undefined + ? contract.resolutionProbability + : getProbability(contract.totalShares) + + const pool = contract.pool.YES + contract.pool.NO + + const weightedShareTotal = + p * (contract.totalShares.YES - contract.phantomShares.YES) + + (1 - p) * (contract.totalShares.NO - contract.phantomShares.NO) + + const { outcome, amount, shares } = bet + + const betP = outcome === 'YES' ? p : 1 - p + const winnings = ((betP * shares) / weightedShareTotal) * pool + + return deductFees(amount, winnings) +} + export function resolvedPayout(contract: Contract, bet: Bet) { if (contract.resolution) return calculatePayout(contract, bet, contract.resolution) diff --git a/common/payouts.ts b/common/payouts.ts index a372f6bc..e68f58c8 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -7,7 +7,7 @@ import { CREATOR_FEE, FEES } from './fees' export const getCancelPayouts = (contract: Contract, bets: Bet[]) => { const { pool } = contract - const poolTotal = pool.YES + pool.NO + const poolTotal = _.sum(Object.values(pool)) console.log('resolved N/A, pool M$', poolTotal) const betSum = _.sumBy(bets, (b) => b.amount) @@ -19,18 +19,17 @@ export const getCancelPayouts = (contract: Contract, bets: Bet[]) => { } export const getStandardPayouts = ( - outcome: 'YES' | 'NO', + outcome: string, contract: Contract, bets: Bet[] ) => { - const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') - const winningBets = outcome === 'YES' ? yesBets : noBets + const winningBets = bets.filter((bet) => bet.outcome === outcome) - const pool = contract.pool.YES + contract.pool.NO + const poolTotal = _.sum(Object.values(contract.pool)) const totalShares = _.sumBy(winningBets, (b) => b.shares) const payouts = winningBets.map(({ userId, amount, shares }) => { - const winnings = (shares / totalShares) * pool + const winnings = (shares / totalShares) * poolTotal const profit = winnings - amount // profit can be negative if using phantom shares @@ -45,7 +44,7 @@ export const getStandardPayouts = ( 'resolved', outcome, 'pool', - pool, + poolTotal, 'profits', profits, 'creator fee', @@ -114,5 +113,8 @@ export const getPayouts = ( return getMktPayouts(contract, bets, resolutionProbability) case 'CANCEL': return getCancelPayouts(contract, bets) + default: + // Multi outcome. + return getStandardPayouts(outcome, contract, bets) } } diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 15f1e8b9..ea4541ba 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -15,12 +15,23 @@ type market_resolved_template = { url: string } +const toDisplayResolution = (outcome: string, prob: number) => { + const display = { + YES: 'YES', + NO: 'NO', + CANCEL: 'N/A', + MKT: formatPercent(prob), + }[outcome] + + return display === undefined ? `#${outcome}` : display +} + export const sendMarketResolutionEmail = async ( userId: string, payout: number, creator: User, contract: Contract, - resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT', + resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string, resolutionProbability?: number ) => { const privateUser = await getPrivateUser(userId) @@ -36,13 +47,7 @@ export const sendMarketResolutionEmail = async ( const prob = resolutionProbability ?? getProbability(contract.totalShares) - const toDisplayResolution = { - YES: 'YES', - NO: 'NO', - CANCEL: 'N/A', - MKT: formatPercent(prob), - } - const outcome = toDisplayResolution[resolution] + const outcome = toDisplayResolution(resolution, prob) const subject = `Resolved ${outcome}: ${contract.question}` diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 5f297595..561af9f8 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -25,10 +25,25 @@ export const resolveMarket = functions const { outcome, contractId, probabilityInt } = data - if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome)) - return { status: 'error', message: 'Invalid outcome' } + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await contractDoc.get() + if (!contractSnap.exists) + return { status: 'error', message: 'Invalid contract' } + const contract = contractSnap.data() as Contract + const { creatorId, outcomeType } = contract + + if (outcomeType === 'BINARY') { + if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome)) + return { status: 'error', message: 'Invalid outcome' } + } else if (outcomeType === 'MULTI') { + if (isNaN(+outcome)) + return { status: 'error', message: 'Invalid outcome' } + } else { + return { status: 'error', message: 'Invalid contract outcomeType' } + } if ( + outcomeType === 'BINARY' && probabilityInt !== undefined && (probabilityInt < 0 || probabilityInt > 100 || @@ -36,19 +51,13 @@ export const resolveMarket = functions ) return { status: 'error', message: 'Invalid probability' } - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await contractDoc.get() - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract - - if (contract.creatorId !== userId) + if (creatorId !== userId) return { status: 'error', message: 'User not creator of contract' } if (contract.resolution) return { status: 'error', message: 'Contract already resolved' } - const creator = await getUser(contract.creatorId) + const creator = await getUser(creatorId) if (!creator) return { status: 'error', message: 'Creator not found' } const resolutionProbability = @@ -112,7 +121,7 @@ const sendResolutionEmails = async ( userPayouts: { [userId: string]: number }, creator: User, contract: Contract, - outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT', + outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string, resolutionProbability?: number ) => { const nonWinners = _.difference( diff --git a/web/components/answers-panel.tsx b/web/components/answers-panel.tsx index 0f36f588..59651a17 100644 --- a/web/components/answers-panel.tsx +++ b/web/components/answers-panel.tsx @@ -8,7 +8,7 @@ import { Answer } from '../../common/answer' import { Contract } from '../../common/contract' import { AmountInput } from './amount-input' import { Col } from './layout/col' -import { createAnswer, placeBet } from '../lib/firebase/api-call' +import { createAnswer, placeBet, resolveMarket } from '../lib/firebase/api-call' import { Row } from './layout/row' import { Avatar } from './avatar' import { SiteLink } from './site-link' @@ -391,6 +391,27 @@ function AnswerResolvePanel(props: { clearAnswerChoice, } = props + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(undefined) + + const onResolve = async () => { + if (answer === undefined) return + + setIsSubmitting(true) + + const result = await resolveMarket({ + outcome: answer, + contractId: contract.id, + }).then((r) => r.data as any) + + console.log('resolved', `#${answer}`, 'result:', result) + + if (result?.status !== 'success') { + setError(result?.error || 'Error resolving market') + } + setIsSubmitting(false) + } + const resolutionButtonClass = resolveOption === 'CANCEL' ? 'bg-yellow-400 hover:bg-yellow-500' @@ -426,13 +447,15 @@ function AnswerResolvePanel(props: { )} {}} - isSubmitting={false} + onResolve={onResolve} + isSubmitting={isSubmitting} openModelButtonClass={resolutionButtonClass} submitButtonClass={resolutionButtonClass} /> + + {!!error &&
{error}
} ) } diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index 304ee9d2..a0f83028 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -20,7 +20,7 @@ export function ResolutionPanel(props: { }) { useEffect(() => { // warm up cloud function - resolveMarket({}).catch() + resolveMarket({} as any).catch() }, []) const { contract, className } = props @@ -35,6 +35,8 @@ export function ResolutionPanel(props: { const [error, setError] = useState(undefined) const resolve = async () => { + if (!outcome) return + setIsSubmitting(true) const result = await resolveMarket({ diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 7adef1f5..efef68f8 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -28,7 +28,14 @@ export const createAnswer = cloudFunction< } >('createAnswer') -export const resolveMarket = cloudFunction('resolveMarket') +export const resolveMarket = cloudFunction< + { + outcome: string + contractId: string + probabilityInt?: number + }, + { status: 'error' | 'success'; message?: string } +>('resolveMarket') export const sellBet = cloudFunction('sellBet')