diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index bc47a6b3..9e773848 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -43,7 +43,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( .collection(`contracts/${contractId}/bets`) .doc() - const { newBet, newPot, newDpmWeights, newBalance } = getNewBetInfo( + const { newBet, newPool, newDpmWeights, newBalance } = getNewBetInfo( user, outcome, amount, @@ -52,7 +52,10 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( ) transaction.create(newBetDoc, newBet) - transaction.update(contractDoc, { pot: newPot, dpmWeights: newDpmWeights }) + transaction.update(contractDoc, { + pool: newPool, + dpmWeights: newDpmWeights, + }) transaction.update(userDoc, { balance: newBalance }) return { status: 'success' } @@ -69,35 +72,37 @@ const getNewBetInfo = ( contract: Contract, newBetId: string ) => { - const { YES: yesPot, NO: noPot } = contract.pot + const { YES: yesPool, NO: noPool } = contract.pool - const newPot = + const newPool = outcome === 'YES' - ? { YES: yesPot + amount, NO: noPot } - : { YES: yesPot, NO: noPot + amount } + ? { YES: yesPool + amount, NO: noPool } + : { YES: yesPool, NO: noPool + amount } const dpmWeight = outcome === 'YES' - ? (amount * noPot ** 2) / (yesPot ** 2 + amount * yesPot) - : (amount * yesPot ** 2) / (noPot ** 2 + amount * noPot) + ? (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool) + : (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool) - const { YES: yesWeight, NO: noWeight } = contract.dpmWeights - || { YES: 0, NO: 0 } // only nesc for old contracts + const { YES: yesWeight, NO: noWeight } = contract.dpmWeights || { + YES: 0, + NO: 0, + } // only nesc for old contracts const newDpmWeights = outcome === 'YES' ? { YES: yesWeight + dpmWeight, NO: noWeight } : { YES: yesWeight, NO: noWeight + dpmWeight } - const probBefore = yesPot ** 2 / (yesPot ** 2 + noPot ** 2) + const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) const probAverage = (amount + - noPot * Math.atan(yesPot / noPot) - - noPot * Math.atan((amount + yesPot) / noPot)) / + noPool * Math.atan(yesPool / noPool) - + noPool * Math.atan((amount + yesPool) / noPool)) / amount - const probAfter = newPot.YES ** 2 / (newPot.YES ** 2 + newPot.NO ** 2) + const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) const newBet: Bet = { id: newBetId, @@ -114,5 +119,5 @@ const getNewBetInfo = ( const newBalance = user.balance - amount - return { newBet, newPot, newDpmWeights, newBalance } + return { newBet, newPool, newDpmWeights, newBalance } } diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 973d3367..e20ca354 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -11,90 +11,98 @@ export const CREATOR_FEE = 0.01 // 1% export const resolveMarket = functions .runWith({ minInstances: 1 }) - .https - .onCall(async (data: { - outcome: string - contractId: string - }, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } + .https.onCall( + async ( + data: { + outcome: string + contractId: string + }, + context + ) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } - const { outcome, contractId } = data + const { outcome, contractId } = data - if (!['YES', 'NO', 'CANCEL'].includes(outcome)) - return { status: 'error', message: 'Invalid outcome' } + if (!['YES', 'NO', '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 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) - return { status: 'error', message: 'User not creator of contract' } + if (contract.creatorId !== userId) + return { status: 'error', message: 'User not creator of contract' } - if (contract.resolution) - return { status: 'error', message: 'Contract already resolved' } + if (contract.resolution) + return { status: 'error', message: 'Contract already resolved' } - await contractDoc.update({ - isResolved: true, - resolution: outcome, - resolutionTime: Date.now() - }) + await contractDoc.update({ + isResolved: true, + resolution: outcome, + resolutionTime: Date.now(), + }) - console.log('contract ', contractId, 'resolved to:', outcome) + console.log('contract ', contractId, 'resolved to:', outcome) - const betsSnap = await firestore.collection(`contracts/${contractId}/bets`).get() - const bets = betsSnap.docs.map(doc => doc.data() as Bet) + const betsSnap = await firestore + .collection(`contracts/${contractId}/bets`) + .get() + const bets = betsSnap.docs.map((doc) => doc.data() as Bet) - const payouts = outcome === 'CANCEL' - ? bets.map(bet => ({ - userId: bet.userId, - payout: bet.amount - })) + const payouts = + outcome === 'CANCEL' + ? bets.map((bet) => ({ + userId: bet.userId, + payout: bet.amount, + })) + : getPayouts(outcome, contract, bets) - : getPayouts(outcome, contract, bets) + console.log('payouts:', payouts) - console.log('payouts:', payouts) + const groups = _.groupBy(payouts, (payout) => payout.userId) + const userPayouts = _.mapValues(groups, (group) => + _.sumBy(group, (g) => g.payout) + ) - const groups = _.groupBy(payouts, payout => payout.userId) - const userPayouts = _.mapValues(groups, group => _.sumBy(group, g => g.payout)) + const payoutPromises = Object.entries(userPayouts).map(payUser) - const payoutPromises = Object - .entries(userPayouts) - .map(payUser) - - return await Promise.all(payoutPromises) - .catch(e => ({ status: 'error', message: e })) - .then(() => ({ status: 'success' })) - }) + return await Promise.all(payoutPromises) + .catch((e) => ({ status: 'error', message: e })) + .then(() => ({ status: 'success' })) + } + ) const firestore = admin.firestore() const getPayouts = (outcome: string, contract: Contract, bets: Bet[]) => { - const [yesBets, noBets] = _.partition(bets, bet => bet.outcome === 'YES') + const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') - const [pot, winningBets] = outcome === 'YES' - ? [contract.pot.NO - contract.seedAmounts.NO, yesBets] - : [contract.pot.YES - contract.seedAmounts.YES, noBets] + const [pool, winningBets] = + outcome === 'YES' + ? [contract.pool.NO - contract.startPool.NO, yesBets] + : [contract.pool.YES - contract.startPool.YES, noBets] - const finalPot = (1 - PLATFORM_FEE - CREATOR_FEE) * pot - const creatorPayout = CREATOR_FEE * pot - console.log('final pot:', finalPot, 'creator fee:', creatorPayout) + const finalPool = (1 - PLATFORM_FEE - CREATOR_FEE) * pool + const creatorPayout = CREATOR_FEE * pool + console.log('final pool:', finalPool, 'creator fee:', creatorPayout) - const sumWeights = _.sumBy(winningBets, bet => bet.dpmWeight) + const sumWeights = _.sumBy(winningBets, (bet) => bet.dpmWeight) - const winnerPayouts = winningBets.map(bet => ({ + const winnerPayouts = winningBets.map((bet) => ({ userId: bet.userId, - payout: bet.amount + (bet.dpmWeight / sumWeights * finalPot) + payout: bet.amount + (bet.dpmWeight / sumWeights) * finalPool, })) - return winnerPayouts - .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee + return winnerPayouts.concat([ + { userId: contract.creatorId, payout: creatorPayout }, + ]) // add creator fee } const payUser = ([userId, payout]: [string, number]) => { - return firestore.runTransaction(async transaction => { + return firestore.runTransaction(async (transaction) => { const userDoc = firestore.doc(`users/${userId}`) const userSnap = await transaction.get(userDoc) if (!userSnap.exists) return @@ -104,4 +112,3 @@ const payUser = ([userId, payout]: [string, number]) => { transaction.update(userDoc, { balance: newUserBalance }) }) } - diff --git a/functions/src/types/contract.ts b/functions/src/types/contract.ts index f20da2b7..44cd57df 100644 --- a/functions/src/types/contract.ts +++ b/functions/src/types/contract.ts @@ -1,4 +1,3 @@ - export type Contract = { id: string // Chosen by creator; must be unique creatorId: string @@ -6,11 +5,11 @@ export type Contract = { question: string description: string // More info about what the contract is about - outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date' // outcomes: ['YES', 'NO'] - seedAmounts: { YES: number; NO: number } - pot: { YES: number; NO: number } + + startPool: { YES: number; NO: number } + pool: { YES: number; NO: number } dpmWeights: { YES: number; NO: number } createdTime: number // Milliseconds since epoch @@ -18,6 +17,6 @@ export type Contract = { closeTime?: number // When no more trading is allowed isResolved: boolean - resolutionTime?: 10293849 // When the contract creator resolved the market; 0 if unresolved + resolutionTime?: number // When the contract creator resolved the market resolution?: 'YES' | 'NO' | 'CANCEL' // Chosen by creator; must be one of outcomes -} \ No newline at end of file +} diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 793a9a2c..49513214 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -77,13 +77,13 @@ export function BetPanel(props: { contract: Contract; className?: string }) { const betDisabled = isSubmitting || !betAmount || error - const initialProb = getProbability(contract.pot) + const initialProb = getProbability(contract.pool) const resultProb = getProbabilityAfterBet( - contract.pot, + contract.pool, betChoice, betAmount ?? 0 ) - const dpmWeight = getDpmWeight(contract.pot, betAmount ?? 0, betChoice) + const dpmWeight = getDpmWeight(contract.pool, betAmount ?? 0, betChoice) const estimatedWinnings = Math.floor((betAmount ?? 0) + dpmWeight) const estimatedReturn = betAmount diff --git a/web/components/contract-prob-graph.tsx b/web/components/contract-prob-graph.tsx index 9a448496..beca505d 100644 --- a/web/components/contract-prob-graph.tsx +++ b/web/components/contract-prob-graph.tsx @@ -7,19 +7,19 @@ import { Contract } from '../lib/firebase/contracts' export function ContractProbGraph(props: { contract: Contract }) { const { contract } = props - const { id, seedAmounts, resolutionTime } = contract + const { id, startPool, resolutionTime } = contract let bets = useBets(id) if (bets === 'loading') bets = [] - const seedProb = - seedAmounts.YES ** 2 / (seedAmounts.YES ** 2 + seedAmounts.NO ** 2) + const startProb = + startPool.YES ** 2 / (startPool.YES ** 2 + startPool.NO ** 2) const times = [ contract.createdTime, ...bets.map((bet) => bet.createdTime), ].map((time) => new Date(time)) - const probs = [seedProb, ...bets.map((bet) => bet.probAfter)] + const probs = [startProb, ...bets.map((bet) => bet.probAfter)] const latestTime = dayjs(resolutionTime ? resolutionTime : Date.now()) diff --git a/web/lib/calculation/contract.ts b/web/lib/calculation/contract.ts index 6a13227e..69464beb 100644 --- a/web/lib/calculation/contract.ts +++ b/web/lib/calculation/contract.ts @@ -3,35 +3,35 @@ import { Contract } from '../firebase/contracts' const fees = 0.02 -export function getProbability(pot: { YES: number; NO: number }) { - const [yesPot, noPot] = [pot.YES, pot.NO] - const numerator = Math.pow(yesPot, 2) - const denominator = Math.pow(yesPot, 2) + Math.pow(noPot, 2) +export function getProbability(pool: { YES: number; NO: number }) { + const [yesPool, noPool] = [pool.YES, pool.NO] + const numerator = Math.pow(yesPool, 2) + const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 2) return numerator / denominator } export function getProbabilityAfterBet( - pot: { YES: number; NO: number }, + pool: { YES: number; NO: number }, outcome: 'YES' | 'NO', bet: number ) { const [YES, NO] = [ - pot.YES + (outcome === 'YES' ? bet : 0), - pot.NO + (outcome === 'NO' ? bet : 0), + pool.YES + (outcome === 'YES' ? bet : 0), + pool.NO + (outcome === 'NO' ? bet : 0), ] return getProbability({ YES, NO }) } export function getDpmWeight( - pot: { YES: number; NO: number }, + pool: { YES: number; NO: number }, bet: number, betChoice: 'YES' | 'NO' ) { - const [yesPot, noPot] = [pot.YES, pot.NO] + const [yesPool, noPool] = [pool.YES, pool.NO] return betChoice === 'YES' - ? (bet * Math.pow(noPot, 2)) / (Math.pow(yesPot, 2) + bet * yesPot) - : (bet * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + bet * noPot) + ? (bet * Math.pow(noPool, 2)) / (Math.pow(yesPool, 2) + bet * yesPool) + : (bet * Math.pow(yesPool, 2)) / (Math.pow(noPool, 2) + bet * noPool) } export function calculatePayout( @@ -44,18 +44,18 @@ export function calculatePayout( if (outcome === 'CANCEL') return amount if (betOutcome !== outcome) return 0 - let { dpmWeights, pot, seedAmounts } = contract + let { dpmWeights, pool, startPool } = contract // Fake data if not set. if (!dpmWeights) dpmWeights = { YES: 100, NO: 100 } // Fake data if not set. - if (!pot) pot = { YES: 100, NO: 100 } + if (!pool) pool = { YES: 100, NO: 100 } const otherOutcome = outcome === 'YES' ? 'NO' : 'YES' - const potSize = pot[otherOutcome] - seedAmounts[otherOutcome] + const poolSize = pool[otherOutcome] - startPool[otherOutcome] - return (1 - fees) * (dpmWeight / dpmWeights[outcome]) * potSize + amount + return (1 - fees) * (dpmWeight / dpmWeights[outcome]) * poolSize + amount } export function resolvedPayout(contract: Contract, bet: Bet) { if (contract.resolution) @@ -64,7 +64,7 @@ export function resolvedPayout(contract: Contract, bet: Bet) { } export function currentValue(contract: Contract, bet: Bet) { - const prob = getProbability(contract.pot) + const prob = getProbability(contract.pool) const yesPayout = calculatePayout(contract, bet, 'YES') const noPayout = calculatePayout(contract, bet, 'NO') diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 0d96091e..63147616 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -24,8 +24,9 @@ export type Contract = { description: string // More info about what the contract is about outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date' // outcomes: ['YES', 'NO'] - seedAmounts: { YES: number; NO: number } // seedBets: [number, number] - pot: { YES: number; NO: number } + + startPool: { YES: number; NO: number } + pool: { YES: number; NO: number } dpmWeights: { YES: number; NO: number } createdTime: number // Milliseconds since epoch @@ -33,7 +34,7 @@ export type Contract = { closeTime?: number // When no more trading is allowed isResolved: boolean - resolutionTime?: number // When the contract creator resolved the market; 0 if unresolved + resolutionTime?: number // When the contract creator resolved the market resolution?: 'YES' | 'NO' | 'CANCEL' // Chosen by creator; must be one of outcomes } @@ -45,9 +46,9 @@ export function path(contract: Contract) { } export function compute(contract: Contract) { - const { pot, seedAmounts, createdTime, resolutionTime, isResolved } = contract - const volume = pot.YES + pot.NO - seedAmounts.YES - seedAmounts.NO - const prob = pot.YES ** 2 / (pot.YES ** 2 + pot.NO ** 2) + const { pool, startPool, createdTime, resolutionTime, isResolved } = contract + const volume = pool.YES + pool.NO - startPool.YES - startPool.NO + const prob = pool.YES ** 2 / (pool.YES ** 2 + pool.NO ** 2) const probPercent = Math.round(prob * 100) + '%' const createdDate = dayjs(createdTime).format('MMM D') const resolvedDate = isResolved diff --git a/web/lib/service/create-contract.ts b/web/lib/service/create-contract.ts index e33c1a3b..2ad5b115 100644 --- a/web/lib/service/create-contract.ts +++ b/web/lib/service/create-contract.ts @@ -16,7 +16,7 @@ export async function createContract( const contractId = preexistingContract ? slug + '-' + randomString() : slug - const { seedYes, seedNo } = calcSeedBets(initialProb) + const { startYes, startNo } = calcStartPool(initialProb) const contract: Contract = { id: contractId, @@ -28,8 +28,8 @@ export async function createContract( question: question.trim(), description: description.trim(), - seedAmounts: { YES: seedYes, NO: seedNo }, - pot: { YES: seedYes, NO: seedNo }, + startPool: { YES: startYes, NO: startNo }, + pool: { YES: startYes, NO: startNo }, dpmWeights: { YES: 0, NO: 0 }, isResolved: false, @@ -43,15 +43,15 @@ export async function createContract( return contract } -export function calcSeedBets(initialProb: number, initialCapital = 100) { +export function calcStartPool(initialProb: number, initialCapital = 100) { const p = initialProb / 100.0 - const seedYes = + const startYes = p === 0.5 ? p * initialCapital : -(initialCapital * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p) - const seedNo = initialCapital - seedYes + const startNo = initialCapital - startYes - return { seedYes, seedNo } + return { startYes, startNo } }