From 9d5490cf9a0422c57f5a07658e2d355ef4175843 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:01:04 -0600 Subject: [PATCH] Switch to new dpm mechanism (#26) * initial commit * antes * rename path, compute to contractPath, contractMetrics * merge * Include antes as bets; more calculations * fees on estimated winnings * mkt payout calculation * contract: remove startPool, add phantomShares * Merge branch 'main' into new-dpm * dpm migration script * my service account --- common/antes.ts | 61 +++++-- common/calculate.ts | 211 +++++++++++++--------- common/contract.ts | 3 +- common/new-bet.ts | 10 +- common/new-contract.ts | 14 +- common/payouts.ts | 4 +- common/sell-bet.ts | 42 +---- functions/src/create-contract.ts | 21 +++ functions/src/markets.ts | 6 +- functions/src/resolve-market.ts | 3 +- functions/src/scripts/migrate-to-dpm-2.ts | 71 ++++++++ web/components/bet-panel.tsx | 21 ++- web/components/bets-list.tsx | 8 +- web/components/contract-card.tsx | 16 +- web/components/contract-feed.tsx | 8 +- web/components/contract-overview.tsx | 8 +- web/components/contract-prob-graph.tsx | 6 +- web/components/contracts-list.tsx | 10 +- web/components/profile-menu.tsx | 1 + web/lib/firebase/contracts.ts | 26 ++- web/lib/firebase/init.ts | 4 +- web/pages/[username]/[contractSlug].tsx | 4 +- web/pages/create.tsx | 4 +- web/pages/make-predictions.tsx | 10 +- 24 files changed, 379 insertions(+), 193 deletions(-) create mode 100644 functions/src/scripts/migrate-to-dpm-2.ts diff --git a/common/antes.ts b/common/antes.ts index cfe2ede8..eb51b691 100644 --- a/common/antes.ts +++ b/common/antes.ts @@ -1,19 +1,58 @@ +import { Bet } from './bet' +import { getProbability } from './calculate' +import { Contract } from './contract' +import { User } from './user' + export const PHANTOM_ANTE = 200 -export const calcStartPool = (initialProbInt: number, ante?: number) => { +export const calcStartPool = (initialProbInt: number, ante = 0) => { const p = initialProbInt / 100.0 - const totalAnte = PHANTOM_ANTE + (ante || 0) + const totalAnte = PHANTOM_ANTE + ante - const poolYes = - p === 0.5 - ? p * totalAnte - : -(totalAnte * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p) + const sharesYes = Math.sqrt(p * totalAnte ** 2) + const sharesNo = Math.sqrt(totalAnte ** 2 - sharesYes ** 2) - const poolNo = totalAnte - poolYes + const poolYes = p * ante + const poolNo = (1 - p) * ante - const f = PHANTOM_ANTE / totalAnte - const startYes = f * poolYes - const startNo = f * poolNo + const phantomYes = Math.sqrt(p) * PHANTOM_ANTE + const phantomNo = Math.sqrt(1 - p) * PHANTOM_ANTE - return { startYes, startNo, poolYes, poolNo } + return { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } +} + +export function getAnteBets( + creator: User, + contract: Contract, + yesAnteId: string, + noAnteId: string +) { + const p = getProbability(contract.totalShares) + const ante = contract.totalBets.YES + contract.totalBets.NO + + const yesBet: Bet = { + id: yesAnteId, + userId: creator.id, + contractId: contract.id, + amount: p * ante, + shares: Math.sqrt(p) * ante, + outcome: 'YES', + probBefore: p, + probAfter: p, + createdTime: Date.now(), + } + + const noBet: Bet = { + id: noAnteId, + userId: creator.id, + contractId: contract.id, + amount: (1 - p) * ante, + shares: Math.sqrt(1 - p) * ante, + outcome: 'NO', + probBefore: p, + probAfter: p, + createdTime: Date.now(), + } + + return { yesBet, noBet } } diff --git a/common/calculate.ts b/common/calculate.ts index 5cf08088..b8a88012 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -2,37 +2,96 @@ import { Bet } from './bet' import { Contract } from './contract' import { FEES } from './fees' -export const blah = () => 999 - -export const 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 getProbability(totalShares: { YES: number; NO: number }) { + const { YES: y, NO: n } = totalShares + return y ** 2 / (y ** 2 + n ** 2) } export function getProbabilityAfterBet( - pool: { YES: number; NO: number }, + totalShares: { YES: number; NO: number }, outcome: 'YES' | 'NO', bet: number ) { - const [YES, NO] = [ - pool.YES + (outcome === 'YES' ? bet : 0), - pool.NO + (outcome === 'NO' ? bet : 0), - ] + const shares = calculateShares(totalShares, bet, outcome) + + const [YES, NO] = + outcome === 'YES' + ? [totalShares.YES + shares, totalShares.NO] + : [totalShares.YES, totalShares.NO + shares] + return getProbability({ YES, NO }) } export function calculateShares( - pool: { YES: number; NO: number }, + totalShares: { YES: number; NO: number }, bet: number, betChoice: 'YES' | 'NO' ) { - const [yesPool, noPool] = [pool.YES, pool.NO] + const [yesShares, noShares] = [totalShares.YES, totalShares.NO] + + const c = 2 * bet * Math.sqrt(yesShares ** 2 + noShares ** 2) return betChoice === 'YES' - ? bet + (bet * noPool ** 2) / (yesPool ** 2 + bet * yesPool) - : bet + (bet * yesPool ** 2) / (noPool ** 2 + bet * noPool) + ? Math.sqrt(bet ** 2 + yesShares ** 2 + c) - yesShares + : Math.sqrt(bet ** 2 + noShares ** 2 + c) - noShares +} + +export function calculateEstimatedWinnings( + totalShares: { YES: number; NO: number }, + shares: number, + betChoice: 'YES' | 'NO' +) { + const ind = betChoice === 'YES' ? 1 : 0 + + const yesShares = totalShares.YES + ind * shares + const noShares = totalShares.NO + (1 - ind) * shares + + const estPool = Math.sqrt(yesShares ** 2 + noShares ** 2) + const total = ind * yesShares + (1 - ind) * noShares + + return ((1 - FEES) * (shares * estPool)) / total +} + +export function calculateRawShareValue( + totalShares: { YES: number; NO: number }, + shares: number, + betChoice: 'YES' | 'NO' +) { + const [yesShares, noShares] = [totalShares.YES, totalShares.NO] + const currentValue = Math.sqrt(yesShares ** 2 + noShares ** 2) + + const postSaleValue = + betChoice === 'YES' + ? Math.sqrt(Math.max(0, yesShares - shares) ** 2 + noShares ** 2) + : Math.sqrt(yesShares ** 2 + Math.max(0, noShares - shares) ** 2) + + return currentValue - postSaleValue +} + +export function calculateMoneyRatio(contract: Contract) { + const { totalShares, pool } = contract + const [yesShares, noShares] = [totalShares.YES, totalShares.NO] + + const actual = pool.YES + pool.NO + const expected = Math.sqrt(yesShares ** 2 + noShares ** 2) + return actual / expected +} + +export function calculateShareValue(contract: Contract, bet: Bet) { + const shareValue = calculateRawShareValue( + contract.totalShares, + bet.shares, + bet.outcome + ) + const f = calculateMoneyRatio(contract) + + const myPool = contract.pool[bet.outcome] + const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) + return adjShareValue +} + +export function calculateSaleAmount(contract: Contract, bet: Bet) { + return (1 - FEES) * calculateShareValue(contract, bet) } export function calculatePayout( @@ -40,56 +99,74 @@ export function calculatePayout( bet: Bet, outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' ) { - const { amount, outcome: betOutcome, shares } = bet - - if (outcome === 'CANCEL') return amount + if (outcome === 'CANCEL') return calculateCancelPayout(contract, bet) if (outcome === 'MKT') return calculateMktPayout(contract, bet) + return calculateStandardPayout(contract, bet, outcome) +} + +export function calculateCancelPayout(contract: Contract, bet: Bet) { + const totalBets = contract.totalBets.YES + contract.totalBets.NO + const pool = contract.pool.YES + contract.pool.NO + + return (bet.amount / totalBets) * pool +} + +export function calculateStandardPayout( + contract: Contract, + bet: Bet, + outcome: 'YES' | 'NO' +) { + const { amount, outcome: betOutcome, shares } = bet if (betOutcome !== outcome) return 0 - const { totalShares, totalBets } = contract - + const { totalShares, totalBets, phantomShares } = contract if (totalShares[outcome] === 0) return 0 - const startPool = contract.startPool.YES + contract.startPool.NO - const truePool = contract.pool.YES + contract.pool.NO - startPool + const truePool = contract.pool.YES + contract.pool.NO if (totalBets[outcome] >= truePool) return (amount / totalBets[outcome]) * truePool - const total = totalShares[outcome] - totalBets[outcome] + const total = + totalShares[outcome] - phantomShares[outcome] - totalBets[outcome] const winningsPool = truePool - totalBets[outcome] return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool) } export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { - const { amount, outcome, shares } = bet - const { totalShares, totalBets } = contract + const { totalShares, pool, totalBets } = contract - const startPool = contract.startPool.YES + contract.startPool.NO - const truePool = amount + contract.pool.YES + contract.pool.NO - startPool + const ind = bet.outcome === 'YES' ? 1 : 0 + const { shares, amount } = bet - const totalBetsOutcome = totalBets[outcome] + amount - const totalSharesOutcome = totalShares[outcome] + shares + const newContract = { + ...contract, + totalShares: { + YES: totalShares.YES + ind * shares, + NO: totalShares.NO + (1 - ind) * shares, + }, + pool: { + YES: pool.YES + ind * amount, + NO: pool.NO + (1 - ind) * amount, + }, + totalBets: { + YES: totalBets.YES + ind * amount, + NO: totalBets.NO + (1 - ind) * amount, + }, + } - if (totalBetsOutcome >= truePool) - return (amount / totalBetsOutcome) * truePool - - const total = totalSharesOutcome - totalBetsOutcome - const winningsPool = truePool - totalBetsOutcome - - return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool) + return calculateStandardPayout(newContract, bet, bet.outcome) } function calculateMktPayout(contract: Contract, bet: Bet) { - const p = - contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) + const p = getProbability(contract.totalShares) + 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 truePool = contract.pool.YES + contract.pool.NO const betP = bet.outcome === 'YES' ? p : 1 - p @@ -100,8 +177,14 @@ function calculateMktPayout(contract: Contract, bet: Bet) { const winningsPool = truePool - weightedTotal const weightedShareTotal = - p * (contract.totalShares.YES - contract.totalBets.YES) + - (1 - p) * (contract.totalShares.NO - contract.totalBets.NO) + p * + (contract.totalShares.YES - + contract.phantomShares.YES - + contract.totalBets.YES) + + (1 - p) * + (contract.totalShares.NO - + contract.phantomShares.NO - + contract.totalBets.NO) return ( (1 - FEES) * @@ -116,6 +199,7 @@ export function resolvedPayout(contract: Contract, bet: Bet) { throw new Error('Contract was not resolved') } +// deprecated use MKT payout export function currentValue(contract: Contract, bet: Bet) { const prob = getProbability(contract.pool) const yesPayout = calculatePayout(contract, bet, 'YES') @@ -123,44 +207,3 @@ export function currentValue(contract: Contract, bet: Bet) { return prob * yesPayout + (1 - prob) * noPayout } - -export function calculateSaleAmount(contract: Contract, bet: Bet) { - const { shares, outcome } = bet - - const { YES: yesPool, NO: noPool } = contract.pool - const { YES: yesStart, NO: noStart } = contract.startPool - const { YES: yesShares, NO: noShares } = contract.totalShares - - const [y, n, s] = [yesPool, noPool, shares] - - const shareValue = - outcome === 'YES' - ? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b - (n ** 2 + - s * y + - y ** 2 - - Math.sqrt( - n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y) - )) / - (2 * y) - : (y ** 2 + - s * n + - n ** 2 - - Math.sqrt( - y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n) - )) / - (2 * n) - - const startPool = yesStart + noStart - const pool = yesPool + noPool - startPool - - const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) - const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares) - - const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart - - const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) - - const saleAmount = (1 - FEES) * adjShareValue - return saleAmount -} diff --git a/common/contract.ts b/common/contract.ts index 7c894ce5..385820df 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -11,7 +11,8 @@ export type Contract = { outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date' // outcomes: ['YES', 'NO'] - startPool: { YES: number; NO: number } + mechanism: 'dpm-2' + phantomShares: { YES: number; NO: number } pool: { YES: number; NO: number } totalShares: { YES: number; NO: number } totalBets: { YES: number; NO: number } diff --git a/common/new-bet.ts b/common/new-bet.ts index fe441aa4..61c72015 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -1,4 +1,5 @@ import { Bet } from './bet' +import { calculateShares, getProbability } from './calculate' import { Contract } from './contract' import { User } from './user' @@ -16,10 +17,7 @@ export const getNewBetInfo = ( ? { YES: yesPool + amount, NO: noPool } : { YES: yesPool, NO: noPool + amount } - const shares = - outcome === 'YES' - ? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool) - : amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool) + const shares = calculateShares(contract.totalShares, amount, outcome) const { YES: yesShares, NO: noShares } = contract.totalShares @@ -35,8 +33,8 @@ export const getNewBetInfo = ( ? { YES: yesBets + amount, NO: noBets } : { YES: yesBets, NO: noBets + amount } - const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) - const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) + const probBefore = getProbability(contract.totalShares) + const probAfter = getProbability(newTotalShares) const newBet: Bet = { id: newBetId, diff --git a/common/new-contract.ts b/common/new-contract.ts index b8abdba7..eef7b392 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -1,4 +1,5 @@ import { calcStartPool } from './antes' + import { Contract } from './contract' import { User } from './user' @@ -12,10 +13,8 @@ export function getNewContract( ante?: number, closeTime?: number ) { - const { startYes, startNo, poolYes, poolNo } = calcStartPool( - initialProb, - ante - ) + const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } = + calcStartPool(initialProb, ante) const contract: Contract = { id, @@ -29,10 +28,11 @@ export function getNewContract( question: question.trim(), description: description.trim(), - startPool: { YES: startYes, NO: startNo }, + mechanism: 'dpm-2', + phantomShares: { YES: phantomYes, NO: phantomNo }, pool: { YES: poolYes, NO: poolNo }, - totalShares: { YES: 0, NO: 0 }, - totalBets: { YES: 0, NO: 0 }, + totalShares: { YES: sharesYes, NO: sharesNo }, + totalBets: { YES: poolYes, NO: poolNo }, isResolved: false, createdTime: Date.now(), diff --git a/common/payouts.ts b/common/payouts.ts index e0225a27..37efb623 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -1,4 +1,5 @@ import { Bet } from './bet' +import { getProbability } from './calculate' import { Contract } from './contract' import { CREATOR_FEE, FEES } from './fees' @@ -58,8 +59,7 @@ export const getMktPayouts = ( contract: Contract, bets: Bet[] ) => { - const p = - contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) + const p = getProbability(contract.totalShares) console.log('Resolved MKT at p=', p, 'pool: $M', truePool) const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === 'YES') diff --git a/common/sell-bet.ts b/common/sell-bet.ts index 8aa2a709..eeeed355 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -1,6 +1,7 @@ import { Bet } from './bet' +import { calculateShareValue, getProbability } from './calculate' import { Contract } from './contract' -import { CREATOR_FEE, PLATFORM_FEE } from './fees' +import { CREATOR_FEE, FEES } from './fees' import { User } from './user' export const getSellBetInfo = ( @@ -12,40 +13,10 @@ export const getSellBetInfo = ( const { id: betId, amount, shares, outcome } = bet const { YES: yesPool, NO: noPool } = contract.pool - const { YES: yesStart, NO: noStart } = contract.startPool const { YES: yesShares, NO: noShares } = contract.totalShares const { YES: yesBets, NO: noBets } = contract.totalBets - const [y, n, s] = [yesPool, noPool, shares] - - const shareValue = - outcome === 'YES' - ? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b - (n ** 2 + - s * y + - y ** 2 - - Math.sqrt( - n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y) - )) / - (2 * y) - : (y ** 2 + - s * n + - n ** 2 - - Math.sqrt( - y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n) - )) / - (2 * n) - - const startPool = yesStart + noStart - const pool = yesPool + noPool - startPool - - const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) - - const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares) - - const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart - - const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) + const adjShareValue = calculateShareValue(contract, bet) const newPool = outcome === 'YES' @@ -62,10 +33,11 @@ export const getSellBetInfo = ( ? { YES: yesBets - amount, NO: noBets } : { YES: yesBets, NO: noBets - amount } - const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) + const probBefore = getProbability(contract.totalShares) + const probAfter = getProbability(newTotalShares) const creatorFee = CREATOR_FEE * adjShareValue - const saleAmount = (1 - CREATOR_FEE - PLATFORM_FEE) * adjShareValue + const saleAmount = (1 - FEES) * adjShareValue console.log( 'SELL M$', @@ -73,8 +45,6 @@ export const getSellBetInfo = ( outcome, 'for M$', saleAmount, - 'M$/share:', - f, 'creator fee: M$', creatorFee ) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index dbe3806b..779b38d4 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -6,6 +6,7 @@ import { Contract } from '../../common/contract' import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random-string' import { getNewContract } from '../../common/new-contract' +import { getAnteBets } from '../../common/antes' export const createContract = functions .runWith({ minInstances: 1 }) @@ -64,6 +65,26 @@ export const createContract = functions if (ante) await chargeUser(creator.id, ante) await contractRef.create(contract) + + if (ante) { + const yesBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + + const noBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + + const { yesBet, noBet } = getAnteBets( + creator, + contract, + yesBetDoc.id, + noBetDoc.id + ) + await yesBetDoc.set(yesBet) + await noBetDoc.set(noBet) + } + return { status: 'success', contract } } ) diff --git a/functions/src/markets.ts b/functions/src/markets.ts index aaa6db06..6d6380c1 100644 --- a/functions/src/markets.ts +++ b/functions/src/markets.ts @@ -33,7 +33,7 @@ const getContractInfo = ({ description, slug, pool, - startPool, + totalShares, volume7Days, volume24Hours, isResolved, @@ -47,8 +47,8 @@ const getContractInfo = ({ question, description, url: `https://manifold.markets/${creatorUsername}/${slug}`, - pool: pool.YES + pool.NO - startPool.YES - startPool.NO, - probability: getProbability(pool), + pool: pool.YES + pool.NO, + probability: getProbability(totalShares), volume7Days, volume24Hours, isResolved, diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 8fdbe470..b9d2931e 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -61,8 +61,7 @@ export const resolveMarket = functions const bets = betsSnap.docs.map((doc) => doc.data() as Bet) const openBets = bets.filter((b) => !b.isSold && !b.sale) - const startPool = contract.startPool.YES + contract.startPool.NO - const truePool = contract.pool.YES + contract.pool.NO - startPool + const truePool = contract.pool.YES + contract.pool.NO const payouts = outcome === 'CANCEL' diff --git a/functions/src/scripts/migrate-to-dpm-2.ts b/functions/src/scripts/migrate-to-dpm-2.ts new file mode 100644 index 00000000..80ce1cc1 --- /dev/null +++ b/functions/src/scripts/migrate-to-dpm-2.ts @@ -0,0 +1,71 @@ +import * as admin from 'firebase-admin' + +import { Contract } from '../../../common/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') +const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json') + +admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), +}) +const firestore = admin.firestore() + +async function recalculateContract(contractRef: DocRef, contract: Contract) { + const startPool = (contract as any).startPool as + | undefined + | { YES: number; NO: number } + + if (contract.phantomShares || !startPool) return + + const phantomAnte = startPool.YES + startPool.NO + const p = startPool.YES ** 2 / (startPool.YES ** 2 + startPool.NO ** 2) + + const phantomShares = { + YES: Math.sqrt(p) * phantomAnte, + NO: Math.sqrt(1 - p) * phantomAnte, + } + + const pool = { + YES: contract.pool.YES - startPool.YES, + NO: contract.pool.NO - startPool.NO, + } + + const totalShares = { + YES: contract.totalShares.YES + phantomShares.YES, + NO: contract.totalShares.NO + phantomShares.NO, + } + + const update: Partial = { + mechanism: 'dpm-2', + phantomShares, + pool, + totalShares, + } + + await contractRef.update(update) + + console.log('updated', contract.slug, 'with', update) + console.log() +} + +async function recalculateContractTotals() { + console.log('Migrating ante calculations to DPM-2') + + 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) + recalculateContractTotals().then(() => process.exit()) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index c4da7c54..1309e9b2 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -18,6 +18,7 @@ import { calculateShares, getProbabilityAfterBet, calculatePayoutAfterCorrectBet, + calculateEstimatedWinnings, } from '../../common/calculate' import { firebaseLogin } from '../lib/firebase/users' import { OutcomeLabel } from './outcome-label' @@ -84,18 +85,30 @@ export function BetPanel(props: { contract: Contract; className?: string }) { const betDisabled = isSubmitting || !betAmount || error - const initialProb = getProbability(contract.pool) + const initialProb = getProbability(contract.totalShares) + const resultProb = getProbabilityAfterBet( - contract.pool, + contract.totalShares, betChoice, betAmount ?? 0 ) - const shares = calculateShares(contract.pool, betAmount ?? 0, betChoice) - const estimatedWinnings = Math.floor(shares) + const shares = calculateShares( + contract.totalShares, + betAmount ?? 0, + betChoice + ) + + const estimatedWinnings = calculateEstimatedWinnings( + contract.totalShares, + shares, + betChoice + ) + const estimatedReturn = betAmount ? (estimatedWinnings - betAmount) / betAmount : 0 + const estimatedReturnPercent = (estimatedReturn * 100).toFixed() + '%' return ( diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 1eae2c53..8a9f816c 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -14,7 +14,11 @@ import { } from '../lib/util/format' import { Col } from './layout/col' import { Spacer } from './layout/spacer' -import { Contract, getContractFromId, path } from '../lib/firebase/contracts' +import { + Contract, + getContractFromId, + contractPath, +} from '../lib/firebase/contracts' import { Row } from './layout/row' import { UserLink } from './user-page' import { @@ -130,7 +134,7 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) { - + e.stopPropagation()} diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index a7c73b35..9fc08526 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -4,7 +4,11 @@ import { Row } from '../components/layout/row' import { formatMoney } from '../lib/util/format' import { UserLink } from './user-page' import { Linkify } from './linkify' -import { Contract, compute, path } from '../lib/firebase/contracts' +import { + Contract, + contractMetrics, + contractPath, +} from '../lib/firebase/contracts' import { Col } from './layout/col' import { parseTags } from '../lib/util/parse' import dayjs from 'dayjs' @@ -16,10 +20,10 @@ export function ContractCard(props: { }) { const { contract, showHotVolume } = props const { question, resolution } = contract - const { probPercent } = compute(contract) + const { probPercent } = contractMetrics(contract) return ( - +
  • @@ -99,7 +103,7 @@ export function AbbrContractDetails(props: { }) { const { contract, showHotVolume } = props const { volume24Hours } = contract - const { truePool } = compute(contract) + const { truePool } = contractMetrics(contract) return ( @@ -124,7 +128,7 @@ export function AbbrContractDetails(props: { export function ContractDetails(props: { contract: Contract }) { const { contract } = props const { question, description, closeTime } = contract - const { truePool, createdDate, resolvedDate } = compute(contract) + const { truePool, createdDate, resolvedDate } = contractMetrics(contract) const tags = parseTags(`${question} ${description}`).map((tag) => `#${tag}`) @@ -171,7 +175,7 @@ export function ContractDetails(props: { contract: Contract }) { // String version of the above, to send to the OpenGraph image generator export function contractTextDetails(contract: Contract) { const { question, description, closeTime } = contract - const { truePool, createdDate, resolvedDate } = compute(contract) + const { truePool, createdDate, resolvedDate } = contractMetrics(contract) const tags = parseTags(`${question} ${description}`).map((tag) => `#${tag}`) diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index 6c068116..9576b80e 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -17,9 +17,9 @@ import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { OutcomeLabel } from './outcome-label' import { - compute, + contractMetrics, Contract, - path, + contractPath, updateContract, } from '../lib/firebase/contracts' import { useUser } from '../hooks/use-user' @@ -205,7 +205,7 @@ export function ContractDescription(props: { function FeedQuestion(props: { contract: Contract }) { const { contract } = props - const { probPercent } = compute(contract) + const { probPercent } = contractMetrics(contract) return ( <> @@ -223,7 +223,7 @@ function FeedQuestion(props: { contract: Contract }) {
    {contract.question} diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx index 0bfb4909..0574aad9 100644 --- a/web/components/contract-overview.tsx +++ b/web/components/contract-overview.tsx @@ -1,8 +1,8 @@ import { - compute, + contractMetrics, Contract, deleteContract, - path, + contractPath, } from '../lib/firebase/contracts' import { Col } from './layout/col' import { Spacer } from './layout/spacer' @@ -22,7 +22,7 @@ export const ContractOverview = (props: { }) => { const { contract, className } = props const { resolution, creatorId, creatorName } = contract - const { probPercent, truePool } = compute(contract) + const { probPercent, truePool } = contractMetrics(contract) const user = useUser() const isCreator = user?.id === creatorId @@ -35,7 +35,7 @@ export const ContractOverview = (props: { ? `Resolved ${resolution}!` : `Resolved ${resolution} by ${creatorName}:` : `Currently ${probPercent} chance, place your bets here:` - const url = `https://manifold.markets${path(contract)}` + const url = `https://manifold.markets${contractPath(contract)}` const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}` return ( diff --git a/web/components/contract-prob-graph.tsx b/web/components/contract-prob-graph.tsx index beca505d..691a662e 100644 --- a/web/components/contract-prob-graph.tsx +++ b/web/components/contract-prob-graph.tsx @@ -1,19 +1,19 @@ import { DatumValue } from '@nivo/core' import { ResponsiveLine } from '@nivo/line' import dayjs from 'dayjs' +import { getProbability } from '../../common/calculate' import { useBets } from '../hooks/use-bets' import { useWindowSize } from '../hooks/use-window-size' import { Contract } from '../lib/firebase/contracts' export function ContractProbGraph(props: { contract: Contract }) { const { contract } = props - const { id, startPool, resolutionTime } = contract + const { id, phantomShares, resolutionTime } = contract let bets = useBets(id) if (bets === 'loading') bets = [] - const startProb = - startPool.YES ** 2 / (startPool.YES ** 2 + startPool.NO ** 2) + const startProb = getProbability(phantomShares) const times = [ contract.createdTime, diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 1a3765b0..c76fa72d 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -3,7 +3,11 @@ import Link from 'next/link' import clsx from 'clsx' import { useEffect, useState } from 'react' -import { compute, Contract, listContracts } from '../lib/firebase/contracts' +import { + contractMetrics, + Contract, + listContracts, +} from '../lib/firebase/contracts' import { User } from '../lib/firebase/users' import { Col } from './layout/col' import { SiteLink } from './site-link' @@ -189,7 +193,9 @@ export function SearchableGrid(props: { if (sort === 'newest' || sort === 'resolved' || sort === 'all') { matches.sort((a, b) => b.createdTime - a.createdTime) } else if (sort === 'most-traded') { - matches.sort((a, b) => compute(b).truePool - compute(a).truePool) + matches.sort( + (a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool + ) } else if (sort === 'creator' || sort === 'tag') { matches.sort((a, b) => b.volume7Days - a.volume7Days) } diff --git a/web/components/profile-menu.tsx b/web/components/profile-menu.tsx index 4f20a1a5..215865b7 100644 --- a/web/components/profile-menu.tsx +++ b/web/components/profile-menu.tsx @@ -59,6 +59,7 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', }, + ...(mobile ? [{ name: 'About', href: '/about' }] : []), { name: 'About', href: '/about', diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 2fd8e786..8556015a 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -18,24 +18,36 @@ import { import { app } from './init' import { getValues, listenForValues } from './utils' import { Contract } from '../../../common/contract' +import { getProbability } from '../../../common/calculate' export type { Contract } -export function path(contract: Contract) { +export function contractPath(contract: Contract) { // For now, derive username from creatorName return `/${contract.creatorUsername}/${contract.slug}` } -export function compute(contract: Contract) { - const { pool, startPool, createdTime, resolutionTime, isResolved } = contract - const truePool = pool.YES + pool.NO - startPool.YES - startPool.NO - const prob = pool.YES ** 2 / (pool.YES ** 2 + pool.NO ** 2) +export function contractMetrics(contract: Contract) { + const { + pool, + phantomShares, + totalShares, + createdTime, + resolutionTime, + isResolved, + } = contract + + const truePool = pool.YES + pool.NO + const prob = getProbability(totalShares) const probPercent = Math.round(prob * 100) + '%' - const startProb = - startPool.YES ** 2 / (startPool.YES ** 2 + startPool.NO ** 2) + + const startProb = getProbability(phantomShares) + const createdDate = dayjs(createdTime).format('MMM D') + const resolvedDate = isResolved ? dayjs(resolutionTime).format('MMM D') : undefined + return { truePool, probPercent, startProb, createdDate, resolvedDate } } diff --git a/web/lib/firebase/init.ts b/web/lib/firebase/init.ts index 98734946..ba10545a 100644 --- a/web/lib/firebase/init.ts +++ b/web/lib/firebase/init.ts @@ -2,8 +2,8 @@ import { getFirestore } from '@firebase/firestore' import { initializeApp } from 'firebase/app' // TODO: Reenable this when we have a way to set the Firebase db in dev -// export const isProd = process.env.NODE_ENV === 'production' -export const isProd = true +export const isProd = process.env.NODE_ENV === 'production' +// export const isProd = true const firebaseConfig = isProd ? { diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 6dc7629b..30092509 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -12,7 +12,7 @@ import { Title } from '../../components/title' import { Spacer } from '../../components/layout/spacer' import { User } from '../../lib/firebase/users' import { - compute, + contractMetrics, Contract, getContractFromSlug, } from '../../lib/firebase/contracts' @@ -58,7 +58,7 @@ export default function ContractPage(props: { !isResolved && (!contract.closeTime || contract.closeTime > Date.now()) const allowResolve = !isResolved && isCreator && !!user - const { probPercent } = compute(contract) + const { probPercent } = contractMetrics(contract) const description = resolution ? `Resolved ${resolution}. ${contract.description}` diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 1a4a19d2..7cb0f6e1 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -7,7 +7,7 @@ import Textarea from 'react-expanding-textarea' import { Spacer } from '../components/layout/spacer' import { Title } from '../components/title' import { useUser } from '../hooks/use-user' -import { Contract, path } from '../lib/firebase/contracts' +import { Contract, contractPath } from '../lib/firebase/contracts' import { Page } from '../components/page' import { AdvancedPanel } from '../components/advanced-panel' import { createContract } from '../lib/firebase/api-call' @@ -72,7 +72,7 @@ export default function NewContract() { return } - await router.push(path(result.contract as Contract)) + await router.push(contractPath(result.contract as Contract)) } // const descriptionPlaceholder = `e.g. This market will resolve to “Yes” if, by June 2, 2021, 11:59:59 PM ET, Paxlovid (also known under PF-07321332)...` diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx index c2a2fe5f..31a947d9 100644 --- a/web/pages/make-predictions.tsx +++ b/web/pages/make-predictions.tsx @@ -10,7 +10,11 @@ import { Page } from '../components/page' import { Title } from '../components/title' import { useUser } from '../hooks/use-user' import { createContract } from '../lib/firebase/api-call' -import { compute, Contract, path } from '../lib/firebase/contracts' +import { + contractMetrics, + Contract, + contractPath, +} from '../lib/firebase/contracts' type Prediction = { question: string @@ -20,12 +24,12 @@ type Prediction = { } function toPrediction(contract: Contract): Prediction { - const { startProb } = compute(contract) + const { startProb } = contractMetrics(contract) return { question: contract.question, description: contract.description, initialProb: startProb * 100, - createdUrl: path(contract), + createdUrl: contractPath(contract), } }