diff --git a/common/calculate.ts b/common/calculate.ts index b5c6818b..2e69690e 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -174,7 +174,10 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { } function calculateMktPayout(contract: Contract, bet: Bet) { - const p = getProbability(contract.totalShares) + const p = + contract.resolutionProbability !== undefined + ? contract.resolutionProbability + : getProbability(contract.totalShares) const weightedTotal = p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO diff --git a/common/contract.ts b/common/contract.ts index d220da38..ed642cfa 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -27,6 +27,7 @@ export type Contract = { isResolved: boolean resolutionTime?: number // When the contract creator resolved the market resolution?: outcome // Chosen by creator; must be one of outcomes + resolutionProbability?: number volume24Hours: number volume7Days: number diff --git a/common/payouts.ts b/common/payouts.ts index 851861df..c4010ba2 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -59,8 +59,16 @@ export const getStandardPayouts = ( ]) // add creator fee } -export const getMktPayouts = (contract: Contract, bets: Bet[]) => { - const p = getProbability(contract.totalShares) +export const getMktPayouts = ( + contract: Contract, + bets: Bet[], + resolutionProbability?: number +) => { + const p = + resolutionProbability === undefined + ? getProbability(contract.totalShares) + : resolutionProbability + const poolTotal = contract.pool.YES + contract.pool.NO console.log('Resolved MKT at p=', p, 'pool: $M', poolTotal) @@ -116,14 +124,15 @@ export const getMktPayouts = (contract: Contract, bets: Bet[]) => { export const getPayouts = ( outcome: outcome, contract: Contract, - bets: Bet[] + bets: Bet[], + resolutionProbability?: number ) => { switch (outcome) { case 'YES': case 'NO': return getStandardPayouts(outcome, contract, bets) case 'MKT': - return getMktPayouts(contract, bets) + return getMktPayouts(contract, bets, resolutionProbability) case 'CANCEL': return getCancelPayouts(contract, bets) } diff --git a/web/lib/util/format.ts b/common/util/format.ts similarity index 100% rename from web/lib/util/format.ts rename to common/util/format.ts diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 8650d981..63d0c9d2 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,5 +1,7 @@ +import { getProbability } from '../../common/calculate' import { Contract } from '../../common/contract' import { User } from '../../common/user' +import { formatPercent } from '../../common/util/format' import { sendTemplateEmail } from './send-email' import { getPrivateUser, getUser } from './utils' @@ -18,7 +20,8 @@ export const sendMarketResolutionEmail = async ( payout: number, creator: User, contract: Contract, - resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT' + resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT', + resolutionProbability?: number ) => { const privateUser = await getPrivateUser(userId) if ( @@ -31,6 +34,14 @@ export const sendMarketResolutionEmail = async ( const user = await getUser(userId) if (!user) return + const prob = resolutionProbability ?? getProbability(contract.totalShares) + + const toDisplayResolution = { + YES: 'YES', + NO: 'NO', + CANCEL: 'N/A', + MKT: formatPercent(prob), + } const outcome = toDisplayResolution[resolution] const subject = `Resolved ${outcome}: ${contract.question}` @@ -56,5 +67,3 @@ export const sendMarketResolutionEmail = async ( templateData ) } - -const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' } diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index df10feed..6816a8d7 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -16,17 +16,24 @@ export const resolveMarket = functions data: { outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' contractId: string + probabilityInt?: number }, context ) => { const userId = context?.auth?.uid if (!userId) return { status: 'error', message: 'Not authorized' } - const { outcome, contractId } = data + const { outcome, contractId, probabilityInt } = data if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome)) return { status: 'error', message: 'Invalid outcome' } + if ( + probabilityInt !== undefined && + (probabilityInt < 1 || probabilityInt > 99 || !isFinite(probabilityInt)) + ) + return { status: 'error', message: 'Invalid probability' } + const contractDoc = firestore.doc(`contracts/${contractId}`) const contractSnap = await contractDoc.get() if (!contractSnap.exists) @@ -42,10 +49,16 @@ export const resolveMarket = functions const creator = await getUser(contract.creatorId) if (!creator) return { status: 'error', message: 'Creator not found' } + const resolutionProbability = + probabilityInt !== undefined ? probabilityInt / 100 : undefined + await contractDoc.update({ isResolved: true, resolution: outcome, resolutionTime: Date.now(), + ...(resolutionProbability === undefined + ? {} + : { resolutionProbability }), }) console.log('contract ', contractId, 'resolved to:', outcome) @@ -57,7 +70,12 @@ export const resolveMarket = functions const bets = betsSnap.docs.map((doc) => doc.data() as Bet) const openBets = bets.filter((b) => !b.isSold && !b.sale) - const payouts = getPayouts(outcome, contract, openBets) + const payouts = getPayouts( + outcome, + contract, + openBets, + resolutionProbability + ) console.log('payouts:', payouts) @@ -79,7 +97,8 @@ export const resolveMarket = functions userPayouts, creator, contract, - outcome + outcome, + resolutionProbability ) return result @@ -91,7 +110,8 @@ const sendResolutionEmails = async ( userPayouts: { [userId: string]: number }, creator: User, contract: Contract, - outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' + outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT', + resolutionProbability?: number ) => { const nonWinners = _.difference( _.uniq(openBets.map(({ userId }) => userId)), @@ -103,7 +123,14 @@ const sendResolutionEmails = async ( ] await Promise.all( emailPayouts.map(([userId, payout]) => - sendMarketResolutionEmail(userId, payout, creator, contract, outcome) + sendMarketResolutionEmail( + userId, + payout, + creator, + contract, + outcome, + resolutionProbability + ) ) ) } diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 9968a132..f01ae418 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' import { useUser } from '../hooks/use-user' -import { formatMoney } from '../lib/util/format' +import { formatMoney } from '../../common/util/format' import { AddFundsButton } from './add-funds-button' import { Col } from './layout/col' import { Row } from './layout/row' diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 12a094fb..5fd9b1f4 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -11,7 +11,7 @@ import { formatMoney, formatPercent, formatWithCommas, -} from '../lib/util/format' +} from '../../common/util/format' import { Title } from './title' import { getProbability, diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 85a4fe71..5af4bb0d 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -11,7 +11,7 @@ import { formatMoney, formatPercent, formatWithCommas, -} from '../lib/util/format' +} from '../../common/util/format' import { Col } from './layout/col' import { Spacer } from './layout/spacer' import { diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 9f91d272..e682c12a 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' import Link from 'next/link' import { Row } from '../components/layout/row' -import { formatMoney } from '../lib/util/format' +import { formatMoney } from '../../common/util/format' import { UserLink } from './user-page' import { Contract, @@ -74,7 +74,7 @@ export function ResolutionOrChance(props: { const resolutionText = { YES: 'YES', NO: 'NO', - MKT: 'MKT', + MKT: probPercent, CANCEL: 'N/A', '': '', }[resolution || ''] diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index c8618691..c5ae3ee5 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -27,7 +27,7 @@ import { Linkify } from './linkify' import { Row } from './layout/row' import { createComment } from '../lib/firebase/comments' import { useComments } from '../hooks/use-comments' -import { formatMoney } from '../lib/util/format' +import { formatMoney } from '../../common/util/format' import { ResolutionOrChance } from './contract-card' import { SiteLink } from './site-link' import { Col } from './layout/col' diff --git a/web/components/create-fold-button.tsx b/web/components/create-fold-button.tsx index c68f1223..794ed8ab 100644 --- a/web/components/create-fold-button.tsx +++ b/web/components/create-fold-button.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { parseWordsAsTags } from '../../common/util/parse' import { createFold } from '../lib/firebase/api-call' import { foldPath } from '../lib/firebase/folds' -import { toCamelCase } from '../lib/util/format' +import { toCamelCase } from '../../common/util/format' import { ConfirmationButton } from './confirmation-button' import { Col } from './layout/col' import { Spacer } from './layout/spacer' diff --git a/web/components/edit-fold-button.tsx b/web/components/edit-fold-button.tsx index 64ffc2fe..3019f3a1 100644 --- a/web/components/edit-fold-button.tsx +++ b/web/components/edit-fold-button.tsx @@ -6,7 +6,7 @@ import { PencilIcon } from '@heroicons/react/outline' import { Fold } from '../../common/fold' import { parseWordsAsTags } from '../../common/util/parse' import { deleteFold, updateFold } from '../lib/firebase/folds' -import { toCamelCase } from '../lib/util/format' +import { toCamelCase } from '../../common/util/format' import { Spacer } from './layout/spacer' import { TagsList } from './tags-list' import { useRouter } from 'next/router' diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 1eb8ac4f..ec65667d 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -5,7 +5,7 @@ export function OutcomeLabel(props: { if (outcome === 'YES') return if (outcome === 'NO') return - if (outcome === 'MKT') return + if (outcome === 'MKT') return return } @@ -24,3 +24,7 @@ export function CancelLabel() { export function MarketLabel() { return MKT } + +export function ProbLabel() { + return PROB +} diff --git a/web/components/probability-selector.tsx b/web/components/probability-selector.tsx new file mode 100644 index 00000000..2fc03787 --- /dev/null +++ b/web/components/probability-selector.tsx @@ -0,0 +1,36 @@ +import { Row } from './layout/row' + +export function ProbabilitySelector(props: { + probabilityInt: number + setProbabilityInt: (p: number) => void + isSubmitting?: boolean +}) { + const { probabilityInt, setProbabilityInt, isSubmitting } = props + + return ( + + + setProbabilityInt(parseInt(e.target.value))} + /> + + ) +} diff --git a/web/components/profile-menu.tsx b/web/components/profile-menu.tsx index 94f2c3f6..327b389b 100644 --- a/web/components/profile-menu.tsx +++ b/web/components/profile-menu.tsx @@ -1,5 +1,5 @@ import { firebaseLogout, User } from '../lib/firebase/users' -import { formatMoney } from '../lib/util/format' +import { formatMoney } from '../../common/util/format' import { Avatar } from './avatar' import { Col } from './layout/col' import { MenuButton } from './menu' diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index 64e643a3..1a409f7e 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -9,6 +9,8 @@ import { YesNoCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ConfirmationButton as ConfirmationButton } from './confirmation-button' import { resolveMarket } from '../lib/firebase/api-call' +import { ProbabilitySelector } from './probability-selector' +import { getProbability } from '../../common/calculate' export function ResolutionPanel(props: { creator: User @@ -26,6 +28,8 @@ export function ResolutionPanel(props: { 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined >() + const [prob, setProb] = useState(getProbability(contract.totalShares) * 100) + const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState(undefined) @@ -35,6 +39,7 @@ export function ResolutionPanel(props: { const result = await resolveMarket({ outcome, contractId: contract.id, + probabilityInt: prob, }).then((r) => r.data as any) console.log('resolved', outcome, 'result:', result) @@ -82,8 +87,8 @@ export function ResolutionPanel(props: { <>The pool will be returned to traders with no fees. ) : outcome === 'MKT' ? ( <> - Traders will be paid out at the current implied probability. You - earn 1% of the pool. + Traders will be paid out at the probability you specify. You earn 1% + of the pool. ) : ( <>Resolving this market will immediately pay out traders. @@ -113,7 +118,20 @@ export function ResolutionPanel(props: { }} onSubmit={resolve} > -

Are you sure you want to resolve this market?

+ {outcome === 'MKT' ? ( + <> +

+ What probability would you like to resolve the market to? +

+ + + + ) : ( +

Are you sure you want to resolve this market?

+ )} ) diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index 8073ffe4..91cf6358 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' import React from 'react' -import { formatMoney } from '../lib/util/format' +import { formatMoney } from '../../common/util/format' import { Col } from './layout/col' import { Row } from './layout/row' @@ -78,7 +78,7 @@ export function YesNoCancelSelector(props: { onClick={() => onSelect('MKT')} className={clsx(btnClassName, 'btn-sm')} > - MKT + PROB