diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 164551bd..4862703f 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -26,7 +26,7 @@ export const resolveMarket = functions const { outcome, contractId } = data - if (!['YES', 'NO', 'CANCEL'].includes(outcome)) + if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome)) return { status: 'error', message: 'Invalid outcome' } const contractDoc = firestore.doc(`contracts/${contractId}`) @@ -155,7 +155,6 @@ const getStandardPayouts = ( const shareDifferenceSum = _.sumBy(winningBets, (b) => b.shares - b.amount) const winningsPool = truePool - betSum - const fees = PLATFORM_FEE + CREATOR_FEE const winnerPayouts = winningBets.map((bet) => ({ userId: bet.userId, @@ -173,11 +172,53 @@ const getStandardPayouts = ( const getMktPayouts = (truePool: number, contract: Contract, bets: Bet[]) => { const p = contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) - console.log('Resolved MKT at p=', p) + console.log('Resolved MKT at p=', p, 'pool: $M', truePool) + + const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') + + const weightedBetTotal = + p * _.sumBy(yesBets, (b) => b.amount) + + (1 - p) * _.sumBy(noBets, (b) => b.amount) + + if (weightedBetTotal >= truePool) { + return bets.map((bet) => ({ + userId: bet.userId, + payout: + (((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) / + weightedBetTotal) * + truePool, + })) + } + + const winningsPool = truePool - weightedBetTotal + + const weightedShareTotal = + p * _.sumBy(yesBets, (b) => b.shares - b.amount) + + (1 - p) * _.sumBy(noBets, (b) => b.shares - b.amount) + + const yesPayouts = yesBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - fees) * + (p * bet.amount + + ((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool), + })) + + const noPayouts = noBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - fees) * + ((1 - p) * bet.amount + + (((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) * + winningsPool), + })) + + const creatorPayout = CREATOR_FEE * truePool return [ - ...getStandardPayouts('YES', p * truePool, contract, bets), - ...getStandardPayouts('NO', (1 - p) * truePool, contract, bets), + ...yesPayouts, + ...noPayouts, + { userId: contract.creatorId, payout: creatorPayout }, ] } @@ -192,3 +233,5 @@ const payUser = ([userId, payout]: [string, number]) => { transaction.update(userDoc, { balance: newUserBalance }) }) } + +const fees = PLATFORM_FEE + CREATOR_FEE diff --git a/functions/src/scripts/recalculate.ts b/functions/src/scripts/recalculate.ts new file mode 100644 index 00000000..103152c9 --- /dev/null +++ b/functions/src/scripts/recalculate.ts @@ -0,0 +1,61 @@ +import * as admin from 'firebase-admin' +import * as _ from 'lodash' +import { Bet } from '../types/bet' +import { Contract } from '../types/contract' + +type DocRef = admin.firestore.DocumentReference + +// Generate your own private key, and set the path below: +// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk +const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json') + +admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), +}) +const firestore = admin.firestore() + +async function recalculateContract(contractRef: DocRef, contract: Contract) { + const bets = await contractRef + .collection('bets') + .get() + .then((snap) => snap.docs.map((bet) => bet.data() as Bet)) + + const openBets = bets.filter((b) => !b.isSold && !b.sale) + + const totalShares = { + YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)), + NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)), + } + + const totalBets = { + YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.amount : 0)), + NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.amount : 0)), + } + + await contractRef.update({ totalShares, totalBets }) + + console.log( + 'calculating totals for "', + contract.question, + '" total bets:', + totalBets + ) + console.log() +} + +async function migrateContracts() { + console.log('Recalculating contract info') + + const snapshot = await firestore.collection('contracts').get() + const contracts = snapshot.docs.map((doc) => doc.data() as Contract) + + console.log('Loaded', contracts.length, 'contracts') + + for (const contract of contracts) { + const contractRef = firestore.doc(`contracts/${contract.id}`) + + await recalculateContract(contractRef, contract) + } +} + +if (require.main === module) migrateContracts().then(() => process.exit()) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 18c74563..29d51346 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -191,7 +191,7 @@ export function MyBetsSummary(props: { const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount) const betsPayout = resolution - ? _.sumBy(bets, (bet) => resolvedPayout(contract, bet)) + ? _.sumBy(excludeSales, (bet) => resolvedPayout(contract, bet)) : 0 const yesWinnings = _.sumBy(excludeSales, (bet) => @@ -357,11 +357,12 @@ function SellButton(props: { contract: Contract; bet: Bet }) { ) } -function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' }) { +function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' }) { const { outcome } = props if (outcome === 'YES') return if (outcome === 'NO') return + if (outcome === 'MKT') return return } @@ -376,3 +377,7 @@ function NoLabel() { function CancelLabel() { return N/A } + +function MarketLabel() { + return MKT +} diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx index aa8dfe6c..ec2fc4cb 100644 --- a/web/components/contract-overview.tsx +++ b/web/components/contract-overview.tsx @@ -98,6 +98,7 @@ export const ContractOverview = (props: { const resolutionColor = { YES: 'text-primary', NO: 'text-red-400', + MKT: 'text-blue-400', CANCEL: 'text-yellow-400', '': '', // Empty if unresolved }[contract.resolution || ''] diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 07c052b0..dc83bafb 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -44,6 +44,7 @@ function ContractCard(props: { contract: Contract }) { const resolutionColor = { YES: 'text-primary', NO: 'text-red-400', + MKT: 'text-blue-400', CANCEL: 'text-yellow-400', '': '', // Empty if unresolved }[contract.resolution || ''] @@ -51,6 +52,7 @@ function ContractCard(props: { contract: Contract }) { const resolutionText = { YES: 'YES', NO: 'NO', + MKT: 'MKT', CANCEL: 'N/A', '': '', }[contract.resolution || ''] diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index 7f9f74c6..d5ccbfd5 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -20,7 +20,9 @@ export function ResolutionPanel(props: { }) { const { contract, className } = props - const [outcome, setOutcome] = useState<'YES' | 'NO' | 'CANCEL' | undefined>() + const [outcome, setOutcome] = useState< + 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined + >() const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState(undefined) @@ -48,6 +50,8 @@ export function ResolutionPanel(props: { ? 'bg-red-400 hover:bg-red-500' : outcome === 'CANCEL' ? 'bg-yellow-400 hover:bg-yellow-500' + : outcome === 'MKT' + ? 'bg-blue-400 hover:bg-blue-500' : 'btn-disabled' return ( @@ -74,6 +78,11 @@ export function ResolutionPanel(props: { <>Winnings will be paid out to NO bettors. You earn 1% of the pool. ) : outcome === 'CANCEL' ? ( <>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. + ) : ( <>Resolving this market will immediately pay out traders. )} diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index 391db469..528b199d 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx' import React from 'react' +import { Col } from './layout/col' import { Row } from './layout/row' export function YesNoSelector(props: { @@ -29,48 +30,60 @@ export function YesNoSelector(props: { } export function YesNoCancelSelector(props: { - selected: 'YES' | 'NO' | 'CANCEL' | undefined - onSelect: (selected: 'YES' | 'NO' | 'CANCEL') => void + selected: 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined + onSelect: (selected: 'YES' | 'NO' | 'MKT' | 'CANCEL') => void className?: string btnClassName?: string }) { const { selected, onSelect, className } = props - const btnClassName = clsx('px-6', props.btnClassName) + const btnClassName = clsx('px-6 flex-1', props.btnClassName) return ( - - + + + - + + - - + + + + + + ) } function Button(props: { className?: string onClick?: () => void - color: 'green' | 'red' | 'yellow' | 'gray' + color: 'green' | 'red' | 'blue' | 'yellow' | 'gray' children?: any }) { const { className, onClick, children, color } = props @@ -83,6 +96,7 @@ function Button(props: { color === 'green' && 'btn-primary', color === 'red' && 'bg-red-400 hover:bg-red-500', color === 'yellow' && 'bg-yellow-400 hover:bg-yellow-500', + color === 'blue' && 'bg-blue-400 hover:bg-blue-500', color === 'gray' && 'text-gray-700 bg-gray-300 hover:bg-gray-400', className )} diff --git a/web/lib/calculate.ts b/web/lib/calculate.ts index 642473df..b265f228 100644 --- a/web/lib/calculate.ts +++ b/web/lib/calculate.ts @@ -37,11 +37,13 @@ export function calculateShares( export function calculatePayout( contract: Contract, bet: Bet, - outcome: 'YES' | 'NO' | 'CANCEL' + outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' ) { const { amount, outcome: betOutcome, shares } = bet if (outcome === 'CANCEL') return amount + if (outcome === 'MKT') return calculateMktPayout(contract, bet) + if (betOutcome !== outcome) return 0 const { totalShares, totalBets } = contract @@ -60,6 +62,34 @@ export function calculatePayout( return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool) } +function calculateMktPayout(contract: Contract, bet: Bet) { + const p = + contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) + const weightedTotal = + p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO + + const startPool = contract.startPool.YES + contract.startPool.NO + const truePool = contract.pool.YES + contract.pool.NO - startPool + + const betP = bet.outcome === 'YES' ? p : 1 - p + + if (weightedTotal >= truePool) { + return ((betP * bet.amount) / weightedTotal) * truePool + } + + const winningsPool = truePool - weightedTotal + + const weightedShareTotal = + p * (contract.totalShares.YES - contract.totalBets.YES) + + (1 - p) * (contract.totalShares.NO - contract.totalBets.NO) + + return ( + (1 - fees) * + (betP * bet.amount + + ((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool) + ) +} + export function resolvedPayout(contract: Contract, bet: Bet) { if (contract.resolution) return calculatePayout(contract, bet, contract.resolution)