From c183e00d47b4a647a1a40fbdb0f98917778f8677 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:27:51 -0500 Subject: [PATCH] Cfmm (#64) * cpmm initial commit: common logic, cloud functions * remove unnecessary property * contract type * rename 'calculate.ts' => 'calculate-dpm.ts' * rename dpm calculations * use focus hook * mechanism-agnostic calculations * bet panel: use new calculations * use new calculations * delete markets cloud function * use correct contract type in scripts / functions * calculate fixed payouts; bets list calculations * new bet: use calculateCpmmPurchase * getOutcomeProbabilityAfterBet * use deductFixedFees * fix auto-refactor * fix antes * separate logic to payouts-dpm, payouts-fixed * liquidity provision tracking * remove comment * liquidity label * create liquidity provision even if no ante bet * liquidity fee * use all bets for getFixedCancelPayouts * updateUserBalance: allow negative balances * store initialProbability in contracts * turn on liquidity fee; turn off creator fee * Include time param in tweet url, so image preview is re-fetched * share redemption * cpmm ContractBetsTable display * formatMoney: handle minus zero * filter out redemption bets * track fees on contract and bets; change fee schedule for cpmm markets; only pay out creator fees at resolution * small fixes * small fixes * Redeem shares pays back loans first * Fix initial point on graph * calculateCpmmPurchase: deduct creator fee * Filter out redemption bets from feed * set env to dev for user-testing purposes * creator fees messaging * new cfmm: k = y^(1-p) * n^p * addCpmmLiquidity * correct price function * enable fees * handle overflow * liquidity provision tracking * raise fees * Fix merge error * fix dpm free response payout for single outcome * Fix DPM payout calculation * Remove hardcoding as dev Co-authored-by: James Grugett --- common/antes.ts | 45 ++- common/bet.ts | 8 +- common/calculate-cpmm.ts | 217 +++++++++++++ common/calculate-dpm.ts | 293 +++++++++++++++++ common/calculate-fixed-payouts.ts | 41 +++ common/calculate.ts | 303 +++++------------- common/contract.ts | 60 +++- common/fees.ts | 21 +- common/liquidity-provision.ts | 13 + common/new-bet.ts | 80 ++++- common/new-contract.ts | 57 +++- common/payouts-dpm.ts | 189 +++++++++++ common/payouts-fixed.ts | 123 +++++++ common/payouts.ts | 280 +++++++--------- common/scoring.ts | 14 +- common/sell-bet.ts | 85 ++++- common/util/format.ts | 3 +- common/util/object.ts | 16 + functions/src/create-answer.ts | 9 +- functions/src/create-contract.ts | 32 +- functions/src/emails.ts | 80 +++-- functions/src/index.ts | 1 - functions/src/markets.ts | 61 ---- functions/src/on-create-comment.ts | 5 +- functions/src/place-bet.ts | 168 ++++++---- functions/src/redeem-shares.ts | 90 ++++++ functions/src/resolve-market.ts | 46 +-- .../src/scripts/correct-bet-probability.ts | 15 +- functions/src/scripts/migrate-to-dpm-2.ts | 13 +- .../src/scripts/pay-out-contract-again.ts | 22 +- functions/src/sell-bet.ts | 47 +-- functions/src/update-user-metrics.ts | 4 +- functions/src/utils.ts | 5 + web/components/answers/answer-bet-panel.tsx | 26 +- web/components/answers/answer-item.tsx | 8 +- .../answers/answer-resolve-panel.tsx | 4 +- web/components/answers/answers-graph.tsx | 23 +- web/components/answers/answers-panel.tsx | 13 +- .../answers/create-answer-panel.tsx | 20 +- web/components/bet-panel.tsx | 65 ++-- web/components/bet-row.tsx | 5 +- web/components/bets-list.tsx | 63 ++-- web/components/contract-card.tsx | 14 +- web/components/contract-overview.tsx | 6 +- web/components/contract-prob-graph.tsx | 30 +- web/components/contracts-list.tsx | 7 +- web/components/feed/activity-items.ts | 45 ++- web/components/feed/feed-items.tsx | 29 +- web/components/resolution-panel.tsx | 20 +- web/hooks/use-focus.ts | 11 + web/lib/firebase/contracts.ts | 27 +- web/pages/[username]/[contractSlug].tsx | 1 - web/pages/api/v0/_types.ts | 8 +- web/pages/create.tsx | 5 +- web/pages/make-predictions.tsx | 13 +- 55 files changed, 2024 insertions(+), 865 deletions(-) create mode 100644 common/calculate-cpmm.ts create mode 100644 common/calculate-dpm.ts create mode 100644 common/calculate-fixed-payouts.ts create mode 100644 common/liquidity-provision.ts create mode 100644 common/payouts-dpm.ts create mode 100644 common/payouts-fixed.ts delete mode 100644 functions/src/markets.ts create mode 100644 functions/src/redeem-shares.ts create mode 100644 web/hooks/use-focus.ts diff --git a/common/antes.ts b/common/antes.ts index d36ef584..6092ab09 100644 --- a/common/antes.ts +++ b/common/antes.ts @@ -1,34 +1,44 @@ import { Bet } from './bet' -import { getProbability } from './calculate' -import { Contract } from './contract' +import { getDpmProbability } from './calculate-dpm' +import { Binary, CPMM, DPM, FreeResponse, FullContract } from './contract' import { User } from './user' +import { LiquidityProvision } from './liquidity-provision' +import { noFees } from './fees' export const PHANTOM_ANTE = 0.001 export const MINIMUM_ANTE = 10 -export const calcStartPool = (initialProbInt: number, ante = 0) => { - const p = initialProbInt / 100.0 - const totalAnte = PHANTOM_ANTE + ante +export function getCpmmInitialLiquidity( + creator: User, + contract: FullContract, + anteId: string, + amount: number +) { + const { createdTime, p } = contract - const sharesYes = Math.sqrt(p * totalAnte ** 2) - const sharesNo = Math.sqrt(totalAnte ** 2 - sharesYes ** 2) + const lp: LiquidityProvision = { + id: anteId, + userId: creator.id, + contractId: contract.id, + createdTime, + isAnte: true, - const poolYes = p * ante - const poolNo = (1 - p) * ante + amount: amount, + liquidity: amount, + p: p, + pool: { YES: 0, NO: 0 }, + } - const phantomYes = Math.sqrt(p) * PHANTOM_ANTE - const phantomNo = Math.sqrt(1 - p) * PHANTOM_ANTE - - return { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } + return lp } export function getAnteBets( creator: User, - contract: Contract, + contract: FullContract, yesAnteId: string, noAnteId: string ) { - const p = getProbability(contract.totalShares) + const p = getDpmProbability(contract.totalShares) const ante = contract.totalBets.YES + contract.totalBets.NO const { createdTime } = contract @@ -44,6 +54,7 @@ export function getAnteBets( probAfter: p, createdTime, isAnte: true, + fees: noFees, } const noBet: Bet = { @@ -57,6 +68,7 @@ export function getAnteBets( probAfter: p, createdTime, isAnte: true, + fees: noFees, } return { yesBet, noBet } @@ -64,7 +76,7 @@ export function getAnteBets( export function getFreeAnswerAnte( creator: User, - contract: Contract, + contract: FullContract, anteBetId: string ) { const { totalBets, totalShares } = contract @@ -84,6 +96,7 @@ export function getFreeAnswerAnte( probAfter: 1, createdTime, isAnte: true, + fees: noFees, } return anteBet diff --git a/common/bet.ts b/common/bet.ts index a3e8e714..3ef71ed6 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -1,3 +1,5 @@ +import { Fees } from './fees' + export type Bet = { id: string userId: string @@ -6,7 +8,7 @@ export type Bet = { amount: number // bet size; negative if SELL bet loanAmount?: number outcome: string - shares: number // dynamic parimutuel pool weight; negative if SELL bet + shares: number // dynamic parimutuel pool weight or fixed ; negative if SELL bet probBefore: number probAfter: number @@ -17,8 +19,12 @@ export type Bet = { // TODO: add sale time? } + fees: Fees + isSold?: boolean // true if this BUY bet has been sold isAnte?: boolean + isLiquidityProvision?: boolean + isRedemption?: boolean createdTime: number } diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts new file mode 100644 index 00000000..45de7392 --- /dev/null +++ b/common/calculate-cpmm.ts @@ -0,0 +1,217 @@ +import * as _ from 'lodash' + +import { Bet } from './bet' +import { Binary, CPMM, FullContract } from './contract' +import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees' + +export function getCpmmProbability( + pool: { [outcome: string]: number }, + p: number +) { + const { YES, NO } = pool + return (p * NO) / ((1 - p) * YES + p * NO) +} + +export function getCpmmProbabilityAfterBetBeforeFees( + contract: FullContract, + outcome: string, + bet: number +) { + const { pool, p } = contract + const shares = calculateCpmmShares(pool, p, bet, outcome) + const { YES: y, NO: n } = pool + + const [newY, newN] = + outcome === 'YES' + ? [y - shares + bet, n + bet] + : [y + bet, n - shares + bet] + + return getCpmmProbability({ YES: newY, NO: newN }, p) +} + +export function getCpmmOutcomeProbabilityAfterBet( + contract: FullContract, + outcome: string, + bet: number +) { + const { newPool } = calculateCpmmPurchase(contract, bet, outcome) + const p = getCpmmProbability(newPool, contract.p) + return outcome === 'NO' ? 1 - p : p +} + +// before liquidity fee +function calculateCpmmShares( + pool: { + [outcome: string]: number + }, + p: number, + bet: number, + betChoice: string +) { + const { YES: y, NO: n } = pool + const k = y ** p * n ** (1 - p) + + return betChoice === 'YES' + ? // https://www.wolframalpha.com/input?i=%28y%2Bb-s%29%5E%28p%29*%28n%2Bb%29%5E%281-p%29+%3D+k%2C+solve+s + y + bet - (k * (bet + n) ** (p - 1)) ** (1 / p) + : n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p)) +} + +export function getCpmmLiquidityFee( + contract: FullContract, + bet: number, + outcome: string +) { + const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet) + const betP = outcome === 'YES' ? 1 - prob : prob + + const liquidityFee = LIQUIDITY_FEE * betP * bet + const platformFee = PLATFORM_FEE * betP * bet + const creatorFee = CREATOR_FEE * betP * bet + const fees: Fees = { liquidityFee, platformFee, creatorFee } + + const totalFees = liquidityFee + platformFee + creatorFee + const remainingBet = bet - totalFees + + return { remainingBet, fees } +} + +export function calculateCpmmSharesAfterFee( + contract: FullContract, + bet: number, + outcome: string +) { + const { pool, p } = contract + const { remainingBet } = getCpmmLiquidityFee(contract, bet, outcome) + + return calculateCpmmShares(pool, p, remainingBet, outcome) +} + +export function calculateCpmmPurchase( + contract: FullContract, + bet: number, + outcome: string +) { + const { pool, p } = contract + const { remainingBet, fees } = getCpmmLiquidityFee(contract, bet, outcome) + // const remainingBet = bet + // const fees = noFees + + const shares = calculateCpmmShares(pool, p, remainingBet, outcome) + const { YES: y, NO: n } = pool + + const { liquidityFee: fee } = fees + + const [newY, newN] = + outcome === 'YES' + ? [y - shares + remainingBet + fee, n + remainingBet + fee] + : [y + remainingBet + fee, n - shares + remainingBet + fee] + + const postBetPool = { YES: newY, NO: newN } + + const { newPool, newP } = addCpmmLiquidity(postBetPool, p, fee) + + return { shares, newPool, newP, fees } +} + +export function calculateCpmmShareValue( + contract: FullContract, + shares: number, + outcome: string +) { + const { pool } = contract + const { YES: y, NO: n } = pool + + // TODO: calculate using new function + const poolChange = outcome === 'YES' ? shares + y - n : shares + n - y + const k = y * n + const shareValue = 0.5 * (shares + y + n - Math.sqrt(4 * k + poolChange ** 2)) + return shareValue +} + +export function calculateCpmmSale( + contract: FullContract, + bet: Bet +) { + const { shares, outcome } = bet + + const rawSaleValue = calculateCpmmShareValue(contract, shares, outcome) + + const { fees, remainingBet: saleValue } = getCpmmLiquidityFee( + contract, + rawSaleValue, + outcome === 'YES' ? 'NO' : 'YES' + ) + + const { pool } = contract + const { YES: y, NO: n } = pool + + const { liquidityFee: fee } = fees + + const [newY, newN] = + outcome === 'YES' + ? [y + shares - saleValue + fee, n - saleValue + fee] + : [y - saleValue + fee, n + shares - saleValue + fee] + + const newPool = { YES: newY, NO: newN } + + return { saleValue, newPool, fees } +} + +export function getCpmmProbabilityAfterSale( + contract: FullContract, + bet: Bet +) { + const { newPool } = calculateCpmmSale(contract, bet) + return getCpmmProbability(newPool, contract.p) +} + +export function getCpmmLiquidity( + pool: { [outcome: string]: number }, + p: number +) { + const { YES, NO } = pool + return YES ** p * NO ** (1 - p) +} + +export function addCpmmLiquidity( + pool: { [outcome: string]: number }, + p: number, + amount: number +) { + const prob = getCpmmProbability(pool, p) + + //https://www.wolframalpha.com/input?i=p%28n%2Bb%29%2F%28%281-p%29%28y%2Bb%29%2Bp%28n%2Bb%29%29%3Dq%2C+solve+p + const { YES: y, NO: n } = pool + const numerator = prob * (amount + y) + const denominator = amount - n * (prob - 1) + prob * y + const newP = numerator / denominator + + const newPool = { YES: y + amount, NO: n + amount } + + const oldLiquidity = getCpmmLiquidity(pool, newP) + const newLiquidity = getCpmmLiquidity(newPool, newP) + const liquidity = newLiquidity - oldLiquidity + + return { newPool, liquidity, newP } +} + +// export function removeCpmmLiquidity( +// contract: FullContract, +// liquidity: number +// ) { +// const { YES, NO } = contract.pool +// const poolLiquidity = getCpmmLiquidity({ YES, NO }) +// const p = getCpmmProbability({ YES, NO }, contract.p) + +// const f = liquidity / poolLiquidity +// const [payoutYes, payoutNo] = [f * YES, f * NO] + +// const betAmount = Math.abs(payoutYes - payoutNo) +// const betOutcome = p >= 0.5 ? 'NO' : 'YES' // opposite side as adding liquidity +// const payout = Math.min(payoutYes, payoutNo) + +// const newPool = { YES: YES - payoutYes, NO: NO - payoutNo } + +// return { newPool, payout, betAmount, betOutcome } +// } diff --git a/common/calculate-dpm.ts b/common/calculate-dpm.ts new file mode 100644 index 00000000..76b464e5 --- /dev/null +++ b/common/calculate-dpm.ts @@ -0,0 +1,293 @@ +import * as _ from 'lodash' +import { Bet } from './bet' +import { Binary, DPM, FullContract } from './contract' +import { DPM_FEES } from './fees' + +export function getDpmProbability(totalShares: { [outcome: string]: number }) { + // For binary contracts only. + return getDpmOutcomeProbability(totalShares, 'YES') +} + +export function getDpmOutcomeProbability( + totalShares: { + [outcome: string]: number + }, + outcome: string +) { + const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2) + const shares = totalShares[outcome] ?? 0 + return shares ** 2 / squareSum +} + +export function getDpmOutcomeProbabilityAfterBet( + totalShares: { + [outcome: string]: number + }, + outcome: string, + bet: number +) { + const shares = calculateDpmShares(totalShares, bet, outcome) + + const prevShares = totalShares[outcome] ?? 0 + const newTotalShares = { ...totalShares, [outcome]: prevShares + shares } + + return getDpmOutcomeProbability(newTotalShares, outcome) +} + +export function getDpmProbabilityAfterSale( + totalShares: { + [outcome: string]: number + }, + outcome: string, + shares: number +) { + const prevShares = totalShares[outcome] ?? 0 + const newTotalShares = { ...totalShares, [outcome]: prevShares - shares } + + const predictionOutcome = outcome === 'NO' ? 'YES' : outcome + return getDpmOutcomeProbability(newTotalShares, predictionOutcome) +} + +export function calculateDpmShares( + totalShares: { + [outcome: string]: number + }, + bet: number, + betChoice: string +) { + const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2) + const shares = totalShares[betChoice] ?? 0 + + const c = 2 * bet * Math.sqrt(squareSum) + + return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares +} + +export function calculateDpmRawShareValue( + totalShares: { + [outcome: string]: number + }, + shares: number, + betChoice: string +) { + const currentValue = Math.sqrt( + _.sumBy(Object.values(totalShares), (shares) => shares ** 2) + ) + + const postSaleValue = Math.sqrt( + _.sumBy(Object.keys(totalShares), (outcome) => + outcome === betChoice + ? Math.max(0, totalShares[outcome] - shares) ** 2 + : totalShares[outcome] ** 2 + ) + ) + + return currentValue - postSaleValue +} + +export function calculateDpmMoneyRatio( + contract: FullContract, + bet: Bet, + shareValue: number +) { + const { totalShares, totalBets, pool } = contract + const { outcome, amount } = bet + + const p = getDpmOutcomeProbability(totalShares, outcome) + + const actual = _.sum(Object.values(pool)) - shareValue + + const betAmount = p * amount + + const expected = + _.sumBy( + Object.keys(totalBets), + (outcome) => + getDpmOutcomeProbability(totalShares, outcome) * + (totalBets as { [outcome: string]: number })[outcome] + ) - betAmount + + if (actual <= 0 || expected <= 0) return 0 + + return actual / expected +} + +export function calculateDpmShareValue( + contract: FullContract, + bet: Bet +) { + const { pool, totalShares } = contract + const { shares, outcome } = bet + + const shareValue = calculateDpmRawShareValue(totalShares, shares, outcome) + const f = calculateDpmMoneyRatio(contract, bet, shareValue) + + const myPool = pool[outcome] + const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) + return adjShareValue +} + +export function calculateDpmSaleAmount( + contract: FullContract, + bet: Bet +) { + const { amount } = bet + const winnings = calculateDpmShareValue(contract, bet) + return deductDpmFees(amount, winnings) +} + +export function calculateDpmPayout( + contract: FullContract, + bet: Bet, + outcome: string +) { + if (outcome === 'CANCEL') return calculateDpmCancelPayout(contract, bet) + if (outcome === 'MKT') return calculateMktDpmPayout(contract, bet) + + return calculateStandardDpmPayout(contract, bet, outcome) +} + +export function calculateDpmCancelPayout( + contract: FullContract, + bet: Bet +) { + const { totalBets, pool } = contract + const betTotal = _.sum(Object.values(totalBets)) + const poolTotal = _.sum(Object.values(pool)) + + return (bet.amount / betTotal) * poolTotal +} + +export function calculateStandardDpmPayout( + contract: FullContract, + bet: Bet, + outcome: string +) { + const { amount, outcome: betOutcome, shares } = bet + if (betOutcome !== outcome) return 0 + + const { totalShares, phantomShares, pool } = contract + if (!totalShares[outcome]) return 0 + + const poolTotal = _.sum(Object.values(pool)) + + const total = + totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0) + + const winnings = (shares / total) * poolTotal + // profit can be negative if using phantom shares + return amount + (1 - DPM_FEES) * Math.max(0, winnings - amount) +} + +export function calculateDpmPayoutAfterCorrectBet( + contract: FullContract, + bet: Bet +) { + const { totalShares, pool, totalBets } = contract + const { shares, amount, outcome } = bet + + const prevShares = totalShares[outcome] ?? 0 + const prevPool = pool[outcome] ?? 0 + const prevTotalBet = totalBets[outcome] ?? 0 + + const newContract = { + ...contract, + totalShares: { + ...totalShares, + [outcome]: prevShares + shares, + }, + pool: { + ...pool, + [outcome]: prevPool + amount, + }, + totalBets: { + ...totalBets, + [outcome]: prevTotalBet + amount, + }, + } + + return calculateStandardDpmPayout(newContract, bet, outcome) +} + +function calculateMktDpmPayout(contract: FullContract, bet: Bet) { + if (contract.outcomeType === 'BINARY') + return calculateBinaryMktDpmPayout(contract, bet) + + const { totalShares, pool } = contract + + const totalPool = _.sum(Object.values(pool)) + const sharesSquareSum = _.sumBy( + Object.values(totalShares) as number[], + (shares) => shares ** 2 + ) + + 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 + }) + + const { outcome, amount, shares } = bet + + const betP = getDpmOutcomeProbability(totalShares, outcome) + const winnings = ((betP * shares) / weightedShareTotal) * totalPool + + return deductDpmFees(amount, winnings) +} + +function calculateBinaryMktDpmPayout( + contract: FullContract, + bet: Bet +) { + const { resolutionProbability, totalShares, phantomShares } = contract + const p = + resolutionProbability !== undefined + ? resolutionProbability + : getDpmProbability(totalShares) + + const pool = contract.pool.YES + contract.pool.NO + + const weightedShareTotal = + p * (totalShares.YES - (phantomShares?.YES ?? 0)) + + (1 - p) * (totalShares.NO - (phantomShares?.NO ?? 0)) + + const { outcome, amount, shares } = bet + + const betP = outcome === 'YES' ? p : 1 - p + const winnings = ((betP * shares) / weightedShareTotal) * pool + + return deductDpmFees(amount, winnings) +} + +export function resolvedDpmPayout(contract: FullContract, bet: Bet) { + if (contract.resolution) + return calculateDpmPayout(contract, bet, contract.resolution) + throw new Error('Contract was not resolved') +} + +export const deductDpmFees = (betAmount: number, winnings: number) => { + return winnings > betAmount + ? betAmount + (1 - DPM_FEES) * (winnings - betAmount) + : winnings +} + +export const calcDpmInitialPool = ( + initialProbInt: number, + ante: number, + phantomAnte: number +) => { + const p = initialProbInt / 100.0 + const totalAnte = phantomAnte + ante + + const sharesYes = Math.sqrt(p * totalAnte ** 2) + const sharesNo = Math.sqrt(totalAnte ** 2 - sharesYes ** 2) + + const poolYes = p * ante + const poolNo = (1 - p) * ante + + const phantomYes = Math.sqrt(p) * phantomAnte + const phantomNo = Math.sqrt(1 - p) * phantomAnte + + return { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } +} diff --git a/common/calculate-fixed-payouts.ts b/common/calculate-fixed-payouts.ts new file mode 100644 index 00000000..9c6ff536 --- /dev/null +++ b/common/calculate-fixed-payouts.ts @@ -0,0 +1,41 @@ +import { Bet } from './bet' +import { getProbability } from './calculate' +import { Binary, FixedPayouts, FullContract } from './contract' + +export function calculateFixedPayout( + contract: FullContract, + bet: Bet, + outcome: string +) { + if (outcome === 'CANCEL') return calculateFixedCancelPayout(bet) + if (outcome === 'MKT') return calculateFixedMktPayout(contract, bet) + + return calculateStandardFixedPayout(bet, outcome) +} + +export function calculateFixedCancelPayout(bet: Bet) { + return bet.amount +} + +export function calculateStandardFixedPayout(bet: Bet, outcome: string) { + const { outcome: betOutcome, shares } = bet + if (betOutcome !== outcome) return 0 + return shares +} + +function calculateFixedMktPayout( + contract: FullContract, + bet: Bet +) { + const { resolutionProbability } = contract + const p = + resolutionProbability !== undefined + ? resolutionProbability + : getProbability(contract) + + const { outcome, shares } = bet + + const betP = outcome === 'YES' ? p : 1 - p + + return betP * shares +} diff --git a/common/calculate.ts b/common/calculate.ts index b03868ed..3bf57c2b 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -1,254 +1,111 @@ -import * as _ from 'lodash' import { Bet } from './bet' -import { Contract } from './contract' -import { FEES } from './fees' +import { + calculateCpmmSale, + getCpmmProbability, + getCpmmOutcomeProbabilityAfterBet, + getCpmmProbabilityAfterSale, + calculateCpmmSharesAfterFee, +} from './calculate-cpmm' +import { + calculateDpmPayout, + calculateDpmPayoutAfterCorrectBet, + calculateDpmSaleAmount, + calculateDpmShares, + getDpmOutcomeProbability, + getDpmProbability, + getDpmOutcomeProbabilityAfterBet, + getDpmProbabilityAfterSale, +} from './calculate-dpm' +import { calculateFixedPayout } from './calculate-fixed-payouts' +import { Binary, Contract, CPMM, DPM, FullContract } from './contract' -export function getProbability(totalShares: { [outcome: string]: number }) { - // For binary contracts only. - return getOutcomeProbability(totalShares, 'YES') +export function getProbability(contract: FullContract) { + return contract.mechanism === 'cpmm-1' + ? getCpmmProbability(contract.pool, contract.p) + : getDpmProbability(contract.totalShares) } -export function getOutcomeProbability( - totalShares: { - [outcome: string]: number - }, - outcome: string +export function getInitialProbability( + contract: FullContract ) { - const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2) - const shares = totalShares[outcome] ?? 0 - return shares ** 2 / squareSum + return ( + contract.initialProbability ?? + (contract.mechanism === 'cpmm-1' + ? getCpmmProbability(contract.pool, contract.p) + : getDpmProbability(contract.phantomShares ?? contract.totalShares)) + ) } -export function getProbabilityAfterBet( - totalShares: { - [outcome: string]: number - }, +export function getOutcomeProbability(contract: Contract, outcome: string) { + return contract.mechanism === 'cpmm-1' + ? getCpmmProbability(contract.pool, contract.p) + : getDpmOutcomeProbability(contract.totalShares, outcome) +} + +export function getOutcomeProbabilityAfterBet( + contract: Contract, outcome: string, bet: number ) { - const shares = calculateShares(totalShares, bet, outcome) - - const prevShares = totalShares[outcome] ?? 0 - const newTotalShares = { ...totalShares, [outcome]: prevShares + shares } - - return getOutcomeProbability(newTotalShares, outcome) -} - -export function getProbabilityAfterSale( - totalShares: { - [outcome: string]: number - }, - outcome: string, - shares: number -) { - const prevShares = totalShares[outcome] ?? 0 - const newTotalShares = { ...totalShares, [outcome]: prevShares - shares } - - const predictionOutcome = outcome === 'NO' ? 'YES' : outcome - return getOutcomeProbability(newTotalShares, predictionOutcome) + return contract.mechanism === 'cpmm-1' + ? getCpmmOutcomeProbabilityAfterBet( + contract as FullContract, + outcome, + bet + ) + : getDpmOutcomeProbabilityAfterBet(contract.totalShares, outcome, bet) } export function calculateShares( - totalShares: { - [outcome: string]: number - }, + contract: Contract, bet: number, betChoice: string ) { - const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2) - const shares = totalShares[betChoice] ?? 0 - - const c = 2 * bet * Math.sqrt(squareSum) - - return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares -} - -export function calculateRawShareValue( - totalShares: { - [outcome: string]: number - }, - shares: number, - betChoice: string -) { - const currentValue = Math.sqrt( - _.sumBy(Object.values(totalShares), (shares) => shares ** 2) - ) - - const postSaleValue = Math.sqrt( - _.sumBy(Object.keys(totalShares), (outcome) => - outcome === betChoice - ? Math.max(0, totalShares[outcome] - shares) ** 2 - : totalShares[outcome] ** 2 - ) - ) - - return currentValue - postSaleValue -} - -export function calculateMoneyRatio( - contract: Contract, - bet: Bet, - shareValue: number -) { - const { totalShares, totalBets, pool } = contract - const { outcome, amount } = bet - - const p = getOutcomeProbability(totalShares, outcome) - - const actual = _.sum(Object.values(pool)) - shareValue - - const betAmount = p * amount - - const expected = - _.sumBy( - Object.keys(totalBets), - (outcome) => - getOutcomeProbability(totalShares, outcome) * - (totalBets as { [outcome: string]: number })[outcome] - ) - betAmount - - if (actual <= 0 || expected <= 0) return 0 - - return actual / expected -} - -export function calculateShareValue(contract: Contract, bet: Bet) { - const { pool, totalShares } = contract - const { shares, outcome } = bet - - const shareValue = calculateRawShareValue(totalShares, shares, outcome) - const f = calculateMoneyRatio(contract, bet, shareValue) - - const myPool = pool[outcome] - const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) - return adjShareValue + return contract.mechanism === 'cpmm-1' + ? calculateCpmmSharesAfterFee( + contract as FullContract, + bet, + betChoice + ) + : calculateDpmShares(contract.totalShares, bet, betChoice) } export function calculateSaleAmount(contract: Contract, bet: Bet) { - const { amount } = bet - const winnings = calculateShareValue(contract, bet) - return deductFees(amount, winnings) -} - -export function calculatePayout(contract: Contract, bet: Bet, outcome: string) { - 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, pool } = contract - const betTotal = _.sum(Object.values(totalBets)) - const poolTotal = _.sum(Object.values(pool)) - - return (bet.amount / betTotal) * poolTotal -} - -export function calculateStandardPayout( - contract: Contract, - bet: Bet, - outcome: string -) { - const { amount, outcome: betOutcome, shares } = bet - if (betOutcome !== outcome) return 0 - - const { totalShares, phantomShares, pool } = contract - if (!totalShares[outcome]) return 0 - - const poolTotal = _.sum(Object.values(pool)) - - const total = - totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0) - - const winnings = (shares / total) * poolTotal - // profit can be negative if using phantom shares - return amount + (1 - FEES) * Math.max(0, winnings - amount) + return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY' + ? calculateCpmmSale(contract, bet).saleValue + : calculateDpmSaleAmount(contract, bet) } export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { - const { totalShares, pool, totalBets } = contract - const { shares, amount, outcome } = bet - - const prevShares = totalShares[outcome] ?? 0 - const prevPool = pool[outcome] ?? 0 - const prevTotalBet = totalBets[outcome] ?? 0 - - const newContract = { - ...contract, - totalShares: { - ...totalShares, - [outcome]: prevShares + shares, - }, - pool: { - ...pool, - [outcome]: prevPool + amount, - }, - totalBets: { - ...totalBets, - [outcome]: prevTotalBet + amount, - }, - } - - return calculateStandardPayout(newContract, bet, outcome) + return contract.mechanism === 'cpmm-1' + ? bet.shares + : calculateDpmPayoutAfterCorrectBet(contract, bet) } -function calculateMktPayout(contract: Contract, bet: Bet) { - if (contract.outcomeType === 'BINARY') - return calculateBinaryMktPayout(contract, bet) - - const { totalShares, pool } = contract - - const totalPool = _.sum(Object.values(pool)) - const sharesSquareSum = _.sumBy( - Object.values(totalShares), - (shares) => shares ** 2 - ) - - 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 - }) - - const { outcome, amount, shares } = bet - - const betP = getOutcomeProbability(totalShares, outcome) - const winnings = ((betP * shares) / weightedShareTotal) * totalPool - - return deductFees(amount, winnings) +export function getProbabilityAfterSale( + contract: Contract, + outcome: string, + shares: number +) { + return contract.mechanism === 'cpmm-1' + ? getCpmmProbabilityAfterSale( + contract as FullContract, + { shares, outcome } as Bet + ) + : getDpmProbabilityAfterSale(contract.totalShares, outcome, shares) } -function calculateBinaryMktPayout(contract: Contract, bet: Bet) { - const { resolutionProbability, totalShares, phantomShares } = contract - const p = - resolutionProbability !== undefined - ? resolutionProbability - : getProbability(totalShares) - - const pool = contract.pool.YES + contract.pool.NO - - const weightedShareTotal = - p * (totalShares.YES - (phantomShares?.YES ?? 0)) + - (1 - p) * (totalShares.NO - (phantomShares?.NO ?? 0)) - - const { outcome, amount, shares } = bet - - const betP = outcome === 'YES' ? p : 1 - p - const winnings = ((betP * shares) / weightedShareTotal) * pool - - return deductFees(amount, winnings) +export function calculatePayout(contract: Contract, bet: Bet, outcome: string) { + return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY' + ? calculateFixedPayout(contract, bet, outcome) + : calculateDpmPayout(contract, bet, outcome) } export function resolvedPayout(contract: Contract, bet: Bet) { - if (contract.resolution) - return calculatePayout(contract, bet, contract.resolution) - throw new Error('Contract was not resolved') -} + const outcome = contract.resolution + if (!outcome) throw new Error('Contract not resolved') -export const deductFees = (betAmount: number, winnings: number) => { - return winnings > betAmount - ? betAmount + (1 - FEES) * (winnings - betAmount) - : winnings + return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY' + ? calculateFixedPayout(contract, bet, outcome) + : calculateDpmPayout(contract, bet, outcome) } diff --git a/common/contract.ts b/common/contract.ts index 83d1139e..35566c0b 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -1,6 +1,10 @@ import { Answer } from './answer' +import { Fees } from './fees' -export type Contract = { +export type FullContract< + M extends DPM | CPMM, + T extends Binary | Multi | FreeResponse +> = { id: string slug: string // auto-generated; must be unique @@ -15,16 +19,6 @@ export type Contract = { lowercaseTags: string[] visibility: 'public' | 'unlisted' - outcomeType: 'BINARY' | 'MULTI' | 'FREE_RESPONSE' - multiOutcomes?: string[] // Used for outcomeType 'MULTI'. - answers?: Answer[] // Used for outcomeType 'FREE_RESPONSE'. - - mechanism: 'dpm-2' - phantomShares?: { [outcome: string]: number } - pool: { [outcome: string]: number } - totalShares: { [outcome: string]: number } - totalBets: { [outcome: string]: number } - createdTime: number // Milliseconds since epoch lastUpdatedTime: number // If the question or description was changed closeTime?: number // When no more trading is allowed @@ -32,12 +26,52 @@ export type Contract = { isResolved: boolean resolutionTime?: number // When the contract creator resolved the market resolution?: string - resolutionProbability?: number // Used for BINARY markets resolved to MKT - resolutions?: { [outcome: string]: number } // Used for outcomeType FREE_RESPONSE resolved to MKT + closeEmailsSent?: number volume24Hours: number volume7Days: number + + collectedFees: Fees +} & M & + T + +export type Contract = FullContract + +export type DPM = { + mechanism: 'dpm-2' + + pool: { [outcome: string]: number } + phantomShares?: { [outcome: string]: number } + totalShares: { [outcome: string]: number } + totalBets: { [outcome: string]: number } +} + +export type CPMM = { + mechanism: 'cpmm-1' + pool: { [outcome: string]: number } + p: number // probability constant in y^p * n^(1-p) = k + totalLiquidity: number // in M$ +} + +export type FixedPayouts = CPMM + +export type Binary = { + outcomeType: 'BINARY' + initialProbability: number + resolutionProbability?: number // Used for BINARY markets resolved to MKT +} + +export type Multi = { + outcomeType: 'MULTI' + multiOutcomes: string[] // Used for outcomeType 'MULTI'. + resolutions?: { [outcome: string]: number } // Used for PROB +} + +export type FreeResponse = { + outcomeType: 'FREE_RESPONSE' + answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'. + resolutions?: { [outcome: string]: number } // Used for PROB } export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' diff --git a/common/fees.ts b/common/fees.ts index a17a4f06..3758a607 100644 --- a/common/fees.ts +++ b/common/fees.ts @@ -1,4 +1,19 @@ -export const PLATFORM_FEE = 0.01 -export const CREATOR_FEE = 0.04 +export const PLATFORM_FEE = 0.02 +export const CREATOR_FEE = 0.08 +export const LIQUIDITY_FEE = 0.08 -export const FEES = PLATFORM_FEE + CREATOR_FEE +export const DPM_PLATFORM_FEE = 0.01 +export const DPM_CREATOR_FEE = 0.04 +export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE + +export type Fees = { + creatorFee: number + platformFee: number + liquidityFee: number +} + +export const noFees: Fees = { + creatorFee: 0, + platformFee: 0, + liquidityFee: 0, +} diff --git a/common/liquidity-provision.ts b/common/liquidity-provision.ts new file mode 100644 index 00000000..53de3ec1 --- /dev/null +++ b/common/liquidity-provision.ts @@ -0,0 +1,13 @@ +export type LiquidityProvision = { + id: string + userId: string + contractId: string + createdTime: number + isAnte?: boolean + + amount: number // M$ quantity + + pool: { [outcome: string]: number } // pool shares before provision + liquidity: number // change in constant k after provision + p: number // p constant after provision +} diff --git a/common/new-bet.ts b/common/new-bet.ts index 0e94c85a..92feb715 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -1,19 +1,69 @@ import * as _ from 'lodash' + import { Bet, MAX_LOAN_PER_CONTRACT } from './bet' import { - calculateShares, - getProbability, - getOutcomeProbability, -} from './calculate' -import { Contract } from './contract' + calculateDpmShares, + getDpmProbability, + getDpmOutcomeProbability, +} from './calculate-dpm' +import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm' +import { + Binary, + CPMM, + DPM, + FreeResponse, + FullContract, + Multi, +} from './contract' import { User } from './user' +import { noFees } from './fees' -export const getNewBinaryBetInfo = ( +export const getNewBinaryCpmmBetInfo = ( user: User, outcome: 'YES' | 'NO', amount: number, + contract: FullContract, + loanAmount: number, + newBetId: string +) => { + const { shares, newPool, newP, fees } = calculateCpmmPurchase( + contract, + amount, + outcome + ) + + const newBalance = user.balance - (amount - loanAmount) + + const { pool, p, totalLiquidity } = contract + const probBefore = getCpmmProbability(pool, p) + const probAfter = getCpmmProbability(newPool, newP) + + const newBet: Bet = { + id: newBetId, + userId: user.id, + contractId: contract.id, + amount, + shares, + outcome, + fees, + loanAmount, + probBefore, + probAfter, + createdTime: Date.now(), + } + + const { liquidityFee } = fees + const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee + + return { newBet, newPool, newP, newBalance, newTotalLiquidity, fees } +} + +export const getNewBinaryDpmBetInfo = ( + user: User, + outcome: 'YES' | 'NO', + amount: number, + contract: FullContract, loanAmount: number, - contract: Contract, newBetId: string ) => { const { YES: yesPool, NO: noPool } = contract.pool @@ -23,7 +73,7 @@ export const getNewBinaryBetInfo = ( ? { YES: yesPool + amount, NO: noPool } : { YES: yesPool, NO: noPool + amount } - const shares = calculateShares(contract.totalShares, amount, outcome) + const shares = calculateDpmShares(contract.totalShares, amount, outcome) const { YES: yesShares, NO: noShares } = contract.totalShares @@ -39,8 +89,8 @@ export const getNewBinaryBetInfo = ( ? { YES: yesBets + amount, NO: noBets } : { YES: yesBets, NO: noBets + amount } - const probBefore = getProbability(contract.totalShares) - const probAfter = getProbability(newTotalShares) + const probBefore = getDpmProbability(contract.totalShares) + const probAfter = getDpmProbability(newTotalShares) const newBet: Bet = { id: newBetId, @@ -53,6 +103,7 @@ export const getNewBinaryBetInfo = ( probBefore, probAfter, createdTime: Date.now(), + fees: noFees, } const newBalance = user.balance - (amount - loanAmount) @@ -64,8 +115,8 @@ export const getNewMultiBetInfo = ( user: User, outcome: string, amount: number, + contract: FullContract, loanAmount: number, - contract: Contract, newBetId: string ) => { const { pool, totalShares, totalBets } = contract @@ -73,7 +124,7 @@ export const getNewMultiBetInfo = ( const prevOutcomePool = pool[outcome] ?? 0 const newPool = { ...pool, [outcome]: prevOutcomePool + amount } - const shares = calculateShares(contract.totalShares, amount, outcome) + const shares = calculateDpmShares(contract.totalShares, amount, outcome) const prevShares = totalShares[outcome] ?? 0 const newTotalShares = { ...totalShares, [outcome]: prevShares + shares } @@ -81,8 +132,8 @@ export const getNewMultiBetInfo = ( const prevTotalBets = totalBets[outcome] ?? 0 const newTotalBets = { ...totalBets, [outcome]: prevTotalBets + amount } - const probBefore = getOutcomeProbability(totalShares, outcome) - const probAfter = getOutcomeProbability(newTotalShares, outcome) + const probBefore = getDpmOutcomeProbability(totalShares, outcome) + const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) const newBet: Bet = { id: newBetId, @@ -95,6 +146,7 @@ export const getNewMultiBetInfo = ( probBefore, probAfter, createdTime: Date.now(), + fees: noFees, } const newBalance = user.balance - (amount - loanAmount) diff --git a/common/new-contract.ts b/common/new-contract.ts index cc742f2f..3b3278c2 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -1,8 +1,16 @@ -import { calcStartPool } from './antes' -import { Contract, outcomeType } from './contract' +import { PHANTOM_ANTE } from './antes' +import { + Binary, + Contract, + CPMM, + DPM, + FreeResponse, + outcomeType, +} from './contract' import { User } from './user' import { parseTags } from './util/parse' import { removeUndefinedProps } from './util/object' +import { calcDpmInitialPool } from './calculate-dpm' export function getNewContract( id: string, @@ -23,14 +31,12 @@ export function getNewContract( const propsByOutcomeType = outcomeType === 'BINARY' - ? getBinaryProps(initialProb, ante) + ? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante) : getFreeAnswerProps(ante) const contract: Contract = removeUndefinedProps({ id, slug, - mechanism: 'dpm-2', - outcomeType, ...propsByOutcomeType, creatorId: creator.id, @@ -50,30 +56,61 @@ export function getNewContract( volume24Hours: 0, volume7Days: 0, + + collectedFees: { + creatorFee: 0, + liquidityFee: 0, + platformFee: 0, + }, }) - return contract + return contract as Contract } -const getBinaryProps = (initialProb: number, ante: number) => { +const getBinaryDpmProps = (initialProb: number, ante: number) => { const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } = - calcStartPool(initialProb, ante) + calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE) - return { + const system: DPM & Binary = { + mechanism: 'dpm-2', + outcomeType: 'BINARY', + initialProbability: initialProb / 100, phantomShares: { YES: phantomYes, NO: phantomNo }, pool: { YES: poolYes, NO: poolNo }, totalShares: { YES: sharesYes, NO: sharesNo }, totalBets: { YES: poolYes, NO: poolNo }, } + + return system +} + +const getBinaryCpmmProps = (initialProb: number, ante: number) => { + const pool = { YES: ante, NO: ante } + const p = initialProb / 100 + + const system: CPMM & Binary = { + mechanism: 'cpmm-1', + outcomeType: 'BINARY', + totalLiquidity: ante, + initialProbability: p, + p, + pool: pool, + } + + return system } const getFreeAnswerProps = (ante: number) => { - return { + const system: DPM & FreeResponse = { + mechanism: 'dpm-2', + outcomeType: 'FREE_RESPONSE', pool: { '0': ante }, totalShares: { '0': ante }, totalBets: { '0': ante }, answers: [], } + + return system } const getMultiProps = ( diff --git a/common/payouts-dpm.ts b/common/payouts-dpm.ts new file mode 100644 index 00000000..a90cfb10 --- /dev/null +++ b/common/payouts-dpm.ts @@ -0,0 +1,189 @@ +import * as _ from 'lodash' + +import { Bet } from './bet' +import { deductDpmFees, getDpmProbability } from './calculate-dpm' +import { DPM, FreeResponse, FullContract, Multi } from './contract' +import { + DPM_CREATOR_FEE, + DPM_FEES, + DPM_PLATFORM_FEE, + Fees, + noFees, +} from './fees' +import { addObjects } from './util/object' + +export const getDpmCancelPayouts = ( + contract: FullContract, + bets: Bet[] +) => { + const { pool } = contract + const poolTotal = _.sum(Object.values(pool)) + console.log('resolved N/A, pool M$', poolTotal) + + const betSum = _.sumBy(bets, (b) => b.amount) + + const payouts = bets.map((bet) => ({ + userId: bet.userId, + payout: (bet.amount / betSum) * poolTotal, + })) + + return [payouts, contract.collectedFees ?? noFees] +} + +export const getDpmStandardPayouts = ( + outcome: string, + contract: FullContract, + bets: Bet[] +) => { + const winningBets = bets.filter((bet) => bet.outcome === outcome) + + 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) * poolTotal + const profit = winnings - amount + + // profit can be negative if using phantom shares + const payout = amount + (1 - DPM_FEES) * Math.max(0, profit) + return { userId, profit, payout } + }) + + const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit)) + const creatorFee = DPM_CREATOR_FEE * profits + const platformFee = DPM_PLATFORM_FEE * profits + + const finalFees: Fees = { + creatorFee, + platformFee, + liquidityFee: 0, + } + + const fees = addObjects(finalFees, contract.collectedFees ?? {}) + + console.log( + 'resolved', + outcome, + 'pool', + poolTotal, + 'profits', + profits, + 'creator fee', + creatorFee + ) + + const totalPayouts = payouts + .map(({ userId, payout }) => ({ userId, payout })) + .concat([{ userId: contract.creatorId, payout: creatorFee }]) // add creator fee + + return [totalPayouts, fees] +} + +export const getDpmMktPayouts = ( + contract: FullContract, + bets: Bet[], + resolutionProbability?: number +) => { + const p = + resolutionProbability === undefined + ? getDpmProbability(contract.totalShares) + : resolutionProbability + + const weightedShareTotal = _.sumBy(bets, (b) => + b.outcome === 'YES' ? p * b.shares : (1 - p) * b.shares + ) + + const pool = contract.pool.YES + contract.pool.NO + + const payouts = bets.map(({ userId, outcome, amount, shares }) => { + const betP = outcome === 'YES' ? p : 1 - p + const winnings = ((betP * shares) / weightedShareTotal) * pool + const profit = winnings - amount + const payout = deductDpmFees(amount, winnings) + return { userId, profit, payout } + }) + + const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit)) + + const creatorFee = DPM_CREATOR_FEE * profits + const platformFee = DPM_PLATFORM_FEE * profits + + const finalFees: Fees = { + creatorFee, + platformFee, + liquidityFee: 0, + } + + const fees = addObjects(finalFees, contract.collectedFees ?? {}) + + console.log( + 'resolved MKT', + p, + 'pool', + pool, + 'profits', + profits, + 'creator fee' + ) + + const totalPayouts = payouts + .map(({ userId, payout }) => ({ userId, payout })) + .concat([{ userId: contract.creatorId, payout: creatorFee }]) // add creator fee + + return [totalPayouts, fees] +} + +export const getPayoutsMultiOutcome = ( + resolutions: { [outcome: string]: number }, + contract: FullContract, + bets: Bet[] +) => { + const poolTotal = _.sum(Object.values(contract.pool)) + const winningBets = bets.filter((bet) => resolutions[bet.outcome]) + + const betsByOutcome = _.groupBy(winningBets, (bet) => bet.outcome) + const sharesByOutcome = _.mapValues(betsByOutcome, (bets) => + _.sumBy(bets, (bet) => bet.shares) + ) + + const probTotal = _.sum(Object.values(resolutions)) + + const payouts = winningBets.map(({ userId, outcome, amount, shares }) => { + const prob = resolutions[outcome] / probTotal + const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal + const profit = winnings - amount + + const payout = amount + (1 - DPM_FEES) * Math.max(0, profit) + return { userId, profit, payout } + }) + + const profits = _.sumBy(payouts, (po) => po.profit) + + const creatorFee = DPM_CREATOR_FEE * profits + const platformFee = DPM_PLATFORM_FEE * profits + + const finalFees: Fees = { + creatorFee, + platformFee, + liquidityFee: 0, + } + + const fees = addObjects(finalFees, contract.collectedFees ?? noFees) + + console.log( + 'resolved', + resolutions, + 'pool', + poolTotal, + 'profits', + profits, + 'creator fee', + creatorFee + ) + + const totalPayouts = payouts + .map(({ userId, payout }) => ({ userId, payout })) + .concat([{ userId: contract.creatorId, payout: creatorFee }]) // add creator fee + + return [totalPayouts, fees] +} diff --git a/common/payouts-fixed.ts b/common/payouts-fixed.ts new file mode 100644 index 00000000..3965c352 --- /dev/null +++ b/common/payouts-fixed.ts @@ -0,0 +1,123 @@ +import * as _ from 'lodash' + +import { Bet } from './bet' +import { getProbability } from './calculate' +import { Binary, CPMM, FixedPayouts, FullContract } from './contract' +import { LiquidityProvision } from './liquidity-provision' + +export const getFixedCancelPayouts = ( + bets: Bet[], + liquidities: LiquidityProvision[] +) => { + const liquidityPayouts = liquidities.map((lp) => ({ + userId: lp.userId, + payout: lp.amount, + })) + + return bets + .filter((b) => !b.isAnte && !b.isLiquidityProvision) + .map((bet) => ({ + userId: bet.userId, + payout: bet.amount, + })) + .concat(liquidityPayouts) +} + +export const getStandardFixedPayouts = ( + outcome: string, + contract: FullContract, + bets: Bet[], + liquidities: LiquidityProvision[] +) => { + const winningBets = bets.filter((bet) => bet.outcome === outcome) + + const payouts = winningBets.map(({ userId, shares }) => ({ + userId, + payout: shares, + })) + + const creatorPayout = contract.collectedFees.creatorFee + + console.log( + 'resolved', + outcome, + 'pool', + contract.pool[outcome], + 'payouts', + _.sum(payouts), + 'creator fee', + creatorPayout + ) + + return payouts + .map(({ userId, payout }) => ({ userId, payout })) + .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee + .concat(getLiquidityPoolPayouts(contract, outcome, liquidities)) +} + +export const getLiquidityPoolPayouts = ( + contract: FullContract, + outcome: string, + liquidities: LiquidityProvision[] +) => { + const providedLiquidity = _.sumBy(liquidities, (lp) => lp.liquidity) + + const { pool } = contract + const finalPool = pool[outcome] + + return liquidities.map((lp) => ({ + userId: lp.userId, + payout: (lp.liquidity / providedLiquidity) * finalPool, + })) +} + +export const getMktFixedPayouts = ( + contract: FullContract, + bets: Bet[], + liquidities: LiquidityProvision[], + resolutionProbability?: number +) => { + const p = + resolutionProbability === undefined + ? getProbability(contract) + : resolutionProbability + + const payouts = bets.map(({ userId, outcome, shares }) => { + const betP = outcome === 'YES' ? p : 1 - p + return { userId, payout: betP * shares } + }) + + const creatorPayout = contract.collectedFees.creatorFee + + console.log( + 'resolved PROB', + p, + 'pool', + p * contract.pool.YES + (1 - p) * contract.pool.NO, + 'payouts', + _.sum(payouts), + 'creator fee', + creatorPayout + ) + + return payouts + .map(({ userId, payout }) => ({ userId, payout })) + .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee + .concat(getLiquidityPoolProbPayouts(contract, p, liquidities)) +} + +export const getLiquidityPoolProbPayouts = ( + contract: FullContract, + p: number, + liquidities: LiquidityProvision[] +) => { + const providedLiquidity = _.sumBy(liquidities, (lp) => lp.liquidity) + + const { pool } = contract + const finalPool = p * pool.YES + (1 - p) * pool.NO + + return liquidities.map((lp) => ({ + userId: lp.userId, + payout: (lp.liquidity / providedLiquidity) * finalPool, + })) +} diff --git a/common/payouts.ts b/common/payouts.ts index ab7f00af..57df7257 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -1,167 +1,35 @@ import * as _ from 'lodash' import { Bet } from './bet' -import { deductFees, getProbability } from './calculate' -import { Contract } from './contract' -import { CREATOR_FEE, FEES } from './fees' +import { + Binary, + Contract, + DPM, + FixedPayouts, + FreeResponse, + FullContract, + Multi, +} from './contract' +import { Fees } from './fees' +import { LiquidityProvision } from './liquidity-provision' +import { + getDpmCancelPayouts, + getDpmMktPayouts, + getDpmStandardPayouts, + getPayoutsMultiOutcome, +} from './payouts-dpm' +import { + getFixedCancelPayouts, + getMktFixedPayouts, + getStandardFixedPayouts, +} from './payouts-fixed' -export const getCancelPayouts = (contract: Contract, bets: Bet[]) => { - const { pool } = contract - const poolTotal = _.sum(Object.values(pool)) - console.log('resolved N/A, pool M$', poolTotal) - - const betSum = _.sumBy(bets, (b) => b.amount) - - return bets.map((bet) => ({ - userId: bet.userId, - payout: (bet.amount / betSum) * poolTotal, - })) +export type Payout = { + userId: string + payout: number } -export const getStandardPayouts = ( - outcome: string, - contract: Contract, - bets: Bet[] -) => { - const winningBets = bets.filter((bet) => bet.outcome === outcome) - - 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) * poolTotal - const profit = winnings - amount - - // profit can be negative if using phantom shares - const payout = amount + (1 - FEES) * Math.max(0, profit) - return { userId, profit, payout } - }) - - const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit)) - const creatorPayout = CREATOR_FEE * profits - - console.log( - 'resolved', - outcome, - 'pool', - poolTotal, - 'profits', - profits, - 'creator fee', - creatorPayout - ) - - return payouts - .map(({ userId, payout }) => ({ userId, payout })) - .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee -} - -export const getMktPayouts = ( - contract: Contract, - bets: Bet[], - resolutionProbability?: number -) => { - const p = - resolutionProbability === undefined - ? getProbability(contract.totalShares) - : resolutionProbability - - const weightedShareTotal = _.sumBy(bets, (b) => - b.outcome === 'YES' ? p * b.shares : (1 - p) * b.shares - ) - - const pool = contract.pool.YES + contract.pool.NO - - const payouts = bets.map(({ userId, outcome, amount, shares }) => { - const betP = outcome === 'YES' ? p : 1 - p - const winnings = ((betP * shares) / weightedShareTotal) * pool - const profit = winnings - amount - const payout = deductFees(amount, winnings) - return { userId, profit, payout } - }) - - const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit)) - const creatorPayout = CREATOR_FEE * profits - - console.log( - 'resolved MKT', - p, - 'pool', - pool, - 'profits', - profits, - 'creator fee', - creatorPayout - ) - - return payouts - .map(({ userId, payout }) => ({ userId, payout })) - .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee -} - -export const getPayouts = ( - outcome: string, - contract: Contract, - bets: Bet[], - resolutionProbability?: number -) => { - switch (outcome) { - case 'YES': - case 'NO': - return getStandardPayouts(outcome, contract, bets) - case 'MKT': - return getMktPayouts(contract, bets, resolutionProbability) - case 'CANCEL': - return getCancelPayouts(contract, bets) - default: - // Multi outcome. - return getStandardPayouts(outcome, contract, bets) - } -} - -export const getPayoutsMultiOutcome = ( - resolutions: { [outcome: string]: number }, - contract: Contract, - bets: Bet[] -) => { - const poolTotal = _.sum(Object.values(contract.pool)) - const winningBets = bets.filter((bet) => resolutions[bet.outcome]) - - const betsByOutcome = _.groupBy(winningBets, (bet) => bet.outcome) - const sharesByOutcome = _.mapValues(betsByOutcome, (bets) => - _.sumBy(bets, (bet) => bet.shares) - ) - - const probTotal = _.sum(Object.values(resolutions)) - - const payouts = winningBets.map(({ userId, outcome, amount, shares }) => { - const prob = resolutions[outcome] / probTotal - const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal - const profit = winnings - amount - const payout = deductFees(amount, winnings) - return { userId, profit, payout } - }) - - const profits = _.sumBy(payouts, (po) => po.profit) - const creatorPayout = CREATOR_FEE * profits - - console.log( - 'resolved', - resolutions, - 'pool', - poolTotal, - 'profits', - profits, - 'creator fee', - creatorPayout - ) - - return payouts - .map(({ userId, payout }) => ({ userId, payout })) - .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee -} - -export const getLoanPayouts = (bets: Bet[]) => { +export const getLoanPayouts = (bets: Bet[]): Payout[] => { const betsWithLoans = bets.filter((bet) => bet.loanAmount) const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId) const loansByUser = _.mapValues(betsByUser, (bets) => @@ -169,3 +37,99 @@ export const getLoanPayouts = (bets: Bet[]) => { ) return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout })) } + +export const getPayouts = ( + outcome: string, + resolutions: { + [outcome: string]: number + }, + contract: Contract, + bets: Bet[], + liquidities: LiquidityProvision[], + resolutionProbability?: number +): [Payout[], Fees] => { + if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') { + const payouts = getFixedPayouts( + outcome, + contract, + bets, + liquidities, + resolutionProbability + ) + return [payouts, contract.collectedFees] + } + + return getDpmPayouts( + outcome, + resolutions, + contract, + bets, + resolutionProbability + ) +} + +export const getFixedPayouts = ( + outcome: string, + contract: FullContract, + bets: Bet[], + liquidities: LiquidityProvision[], + resolutionProbability?: number +): Payout[] => { + switch (outcome) { + case 'YES': + case 'NO': + return getStandardFixedPayouts(outcome, contract, bets, liquidities) + case 'MKT': + return getMktFixedPayouts( + contract, + bets, + liquidities, + resolutionProbability + ) + default: + case 'CANCEL': + return getFixedCancelPayouts(bets, liquidities) + } +} + +export const getDpmPayouts = ( + outcome: string, + resolutions: { + [outcome: string]: number + }, + contract: Contract, + bets: Bet[], + resolutionProbability?: number +) => { + const openBets = bets.filter((b) => !b.isSold && !b.sale) + + switch (outcome) { + case 'YES': + case 'NO': + return getDpmStandardPayouts(outcome, contract, openBets) as [ + Payout[], + Fees + ] + + case 'MKT': + return contract.outcomeType === 'FREE_RESPONSE' + ? (getPayoutsMultiOutcome( + resolutions, + contract as FullContract, + openBets + ) as [Payout[], Fees]) + : (getDpmMktPayouts(contract, openBets, resolutionProbability) as [ + Payout[], + Fees + ]) + case 'CANCEL': + return getDpmCancelPayouts(contract, openBets) as [Payout[], Fees] + + default: + // Outcome is a free response answer id. + return getDpmStandardPayouts(outcome, contract, openBets) as [ + Payout[], + Fees + ] + } +} diff --git a/common/scoring.ts b/common/scoring.ts index f7ed1532..c4cd4c48 100644 --- a/common/scoring.ts +++ b/common/scoring.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash' + import { Bet } from './bet' -import { Contract } from './contract' +import { Binary, Contract, FullContract } from './contract' import { getPayouts } from './payouts' export function scoreCreators(contracts: Contract[], bets: Bet[][]) { @@ -23,17 +24,22 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) { return userScores } -export function scoreUsersByContract(contract: Contract, bets: Bet[]) { +export function scoreUsersByContract( + contract: FullContract, + bets: Bet[] +) { const { resolution, resolutionProbability } = contract const [closedBets, openBets] = _.partition( bets, (bet) => bet.isSold || bet.sale ) - const resolvePayouts = getPayouts( - resolution ?? 'MKT', + const [resolvePayouts] = getPayouts( + resolution, + {}, contract, openBets, + [], resolutionProbability ) diff --git a/common/sell-bet.ts b/common/sell-bet.ts index 1b3b133d..246e8649 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -1,19 +1,24 @@ import { Bet } from './bet' -import { calculateShareValue, deductFees, getProbability } from './calculate' -import { Contract } from './contract' -import { CREATOR_FEE } from './fees' +import { + getDpmProbability, + calculateDpmShareValue, + deductDpmFees, +} from './calculate-dpm' +import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm' +import { Binary, DPM, CPMM, FullContract } from './contract' +import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' import { User } from './user' export const getSellBetInfo = ( user: User, bet: Bet, - contract: Contract, + contract: FullContract, newBetId: string ) => { const { pool, totalShares, totalBets } = contract const { id: betId, amount, shares, outcome, loanAmount } = bet - const adjShareValue = calculateShareValue(contract, bet) + const adjShareValue = calculateDpmShareValue(contract, bet) const newPool = { ...pool, [outcome]: pool[outcome] - adjShareValue } @@ -24,12 +29,20 @@ export const getSellBetInfo = ( const newTotalBets = { ...totalBets, [outcome]: totalBets[outcome] - amount } - const probBefore = getProbability(totalShares) - const probAfter = getProbability(newTotalShares) + const probBefore = getDpmProbability(totalShares) + const probAfter = getDpmProbability(newTotalShares) const profit = adjShareValue - amount - const creatorFee = CREATOR_FEE * Math.max(0, profit) - const saleAmount = deductFees(amount, adjShareValue) + + const creatorFee = DPM_CREATOR_FEE * Math.max(0, profit) + const platformFee = DPM_PLATFORM_FEE * Math.max(0, profit) + const fees: Fees = { + creatorFee, + platformFee, + liquidityFee: 0, + } + + const saleAmount = deductDpmFees(amount, adjShareValue) console.log( 'SELL M$', @@ -55,6 +68,7 @@ export const getSellBetInfo = ( amount: saleAmount, betId, }, + fees, } const newBalance = user.balance + saleAmount - (loanAmount ?? 0) @@ -65,6 +79,57 @@ export const getSellBetInfo = ( newTotalShares, newTotalBets, newBalance, - creatorFee, + fees, + } +} + +export const getCpmmSellBetInfo = ( + user: User, + bet: Bet, + contract: FullContract, + newBetId: string +) => { + const { pool, p } = contract + const { id: betId, amount, shares, outcome } = bet + + const { saleValue, newPool, fees } = calculateCpmmSale(contract, bet) + + const probBefore = getCpmmProbability(pool, p) + const probAfter = getCpmmProbability(newPool, p) + + console.log( + 'SELL M$', + amount, + outcome, + 'for M$', + saleValue, + 'creator fee: M$', + fees.creatorFee + ) + + const newBet: Bet = { + id: newBetId, + userId: user.id, + contractId: contract.id, + amount: -saleValue, + shares: -shares, + outcome, + probBefore, + probAfter, + createdTime: Date.now(), + sale: { + amount: saleValue, + betId, + }, + fees, + } + + const newBalance = user.balance + saleValue + + return { + newBet, + newPool, + newBalance, + fees, } } diff --git a/common/util/format.ts b/common/util/format.ts index a8e6f58a..c8d363d3 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -8,8 +8,9 @@ const formatter = new Intl.NumberFormat('en-US', { }) export function formatMoney(amount: number) { + const newAmount = Math.round(amount) === 0 ? 0 : amount // handle -0 case return ( - ENV_CONFIG.moneyMoniker + ' ' + formatter.format(amount).replace('$', '') + ENV_CONFIG.moneyMoniker + ' ' + formatter.format(newAmount).replace('$', '') ) } diff --git a/common/util/object.ts b/common/util/object.ts index 4148b057..db504cc3 100644 --- a/common/util/object.ts +++ b/common/util/object.ts @@ -1,3 +1,5 @@ +import * as _ from 'lodash' + export const removeUndefinedProps = (obj: T): T => { let newObj: any = {} @@ -7,3 +9,17 @@ export const removeUndefinedProps = (obj: T): T => { return newObj } + +export const addObjects = ( + obj1: T, + obj2: T +) => { + const keys = _.union(Object.keys(obj1), Object.keys(obj2)) + const newObj = {} as any + + for (let key of keys) { + newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0) + } + + return newObj as T +} diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index f192fc7e..0df8c0df 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -1,7 +1,12 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Contract } from '../../common/contract' +import { + Contract, + DPM, + FreeResponse, + FullContract, +} from '../../common/contract' import { User } from '../../common/user' import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet' import { Answer } from '../../common/answer' @@ -105,8 +110,8 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( user, answerId, amount, + contract as FullContract, loanAmount, - contract, newBetDoc.id ) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 0bad05ea..85bc211d 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -4,7 +4,12 @@ import * as _ from 'lodash' import { chargeUser, getUser } from './utils' import { + Binary, Contract, + CPMM, + DPM, + FreeResponse, + FullContract, MAX_DESCRIPTION_LENGTH, MAX_QUESTION_LENGTH, MAX_TAG_LENGTH, @@ -15,6 +20,7 @@ import { randomString } from '../../common/util/random' import { getNewContract } from '../../common/new-contract' import { getAnteBets, + getCpmmInitialLiquidity, getFreeAnswerAnte, MINIMUM_ANTE, } from '../../common/antes' @@ -105,7 +111,7 @@ export const createContract = functions await contractRef.create(contract) if (ante) { - if (outcomeType === 'BINARY') { + if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') { const yesBetDoc = firestore .collection(`contracts/${contract.id}/bets`) .doc() @@ -116,23 +122,43 @@ export const createContract = functions const { yesBet, noBet } = getAnteBets( creator, - contract, + contract as FullContract, yesBetDoc.id, noBetDoc.id ) + await yesBetDoc.set(yesBet) await noBetDoc.set(noBet) + } else if (outcomeType === 'BINARY') { + const liquidityDoc = firestore + .collection(`contracts/${contract.id}/liquidity`) + .doc() + + const lp = getCpmmInitialLiquidity( + creator, + contract as FullContract, + liquidityDoc.id, + ante + ) + + await liquidityDoc.set(lp) } else if (outcomeType === 'FREE_RESPONSE') { const noneAnswerDoc = firestore .collection(`contracts/${contract.id}/answers`) .doc('0') + const noneAnswer = getNoneAnswer(contract.id, creator) await noneAnswerDoc.set(noneAnswer) const anteBetDoc = firestore .collection(`contracts/${contract.id}/bets`) .doc() - const anteBet = getFreeAnswerAnte(creator, contract, anteBetDoc.id) + + const anteBet = getFreeAnswerAnte( + creator, + contract as FullContract, + anteBetDoc.id + ) await anteBetDoc.set(anteBet) } } diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 3d37ba2b..df2a1e5b 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,4 +1,5 @@ -import _ = require('lodash') +import * as _ from 'lodash' + import { DOMAIN, PROJECT_ID } from '../../common/envs/constants' import { Answer } from '../../common/answer' import { Bet } from '../../common/bet' @@ -11,34 +12,6 @@ import { formatMoney, formatPercent } from '../../common/util/format' import { sendTemplateEmail, sendTextEmail } from './send-email' import { getPrivateUser, getUser } from './utils' -type market_resolved_template = { - userId: string - name: string - creatorName: string - question: string - outcome: string - investment: string - payout: string - url: string -} - -const toDisplayResolution = ( - outcome: string, - prob?: number, - resolutions?: { [outcome: string]: number } -) => { - if (outcome === 'MKT' && resolutions) return 'MULTI' - - const display = { - YES: 'YES', - NO: 'NO', - CANCEL: 'N/A', - MKT: formatPercent(prob ?? 0), - }[outcome] - - return display === undefined ? `#${outcome}` : display -} - export const sendMarketResolutionEmail = async ( userId: string, investment: number, @@ -60,9 +33,12 @@ export const sendMarketResolutionEmail = async ( const user = await getUser(userId) if (!user) return - const prob = resolutionProbability ?? getProbability(contract.totalShares) - - const outcome = toDisplayResolution(resolution, prob, resolutions) + const outcome = toDisplayResolution( + contract, + resolution, + resolutionProbability, + resolutions + ) const subject = `Resolved ${outcome}: ${contract.question}` @@ -89,6 +65,41 @@ export const sendMarketResolutionEmail = async ( ) } +type market_resolved_template = { + userId: string + name: string + creatorName: string + question: string + outcome: string + investment: string + payout: string + url: string +} + +const toDisplayResolution = ( + contract: Contract, + resolution: string, + resolutionProbability?: number, + resolutions?: { [outcome: string]: number } +) => { + if (contract.outcomeType === 'BINARY') { + const prob = resolutionProbability ?? getProbability(contract) + + const display = { + YES: 'YES', + NO: 'NO', + CANCEL: 'N/A', + MKT: formatPercent(prob ?? 0), + }[resolution] + + return display || resolution + } + + if (resolution === 'MKT' && resolutions) return 'MULTI' + + return `#${resolution}` +} + export const sendWelcomeEmail = async ( user: User, privateUser: PrivateUser @@ -197,7 +208,10 @@ export const sendNewCommentEmail = async ( { from } ) } else { - betDescription = `${betDescription} of ${toDisplayResolution(outcome)}` + betDescription = `${betDescription} of ${toDisplayResolution( + contract, + outcome + )}` await sendTemplateEmail( privateUser.email, diff --git a/functions/src/index.ts b/functions/src/index.ts index dc333343..5be29b4d 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // export * from './keep-awake' -export * from './markets' export * from './place-bet' export * from './resolve-market' export * from './stripe' diff --git a/functions/src/markets.ts b/functions/src/markets.ts deleted file mode 100644 index f9188987..00000000 --- a/functions/src/markets.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' -import * as _ from 'lodash' - -import { getValues } from './utils' -import { Contract } from '../../common/contract' -import { getProbability } from '../../common/calculate' - -const cache = { lastUpdated: 0, data: '' } - -export const markets = functions - .runWith({ minInstances: 1 }) - .https.onRequest(async (req, res) => { - const contracts: Contract[] = await getValues( - firestore.collection('contracts').orderBy('volume24Hours', 'desc') - ) - - if (!cache.data || Date.now() - cache.lastUpdated > 120 * 1000) { - const contractsInfo = contracts.map(getContractInfo) - cache.data = JSON.stringify(contractsInfo) - cache.lastUpdated = Date.now() - } - - res.status(200).send(cache.data) - }) - -const getContractInfo = ({ - id, - creatorUsername, - creatorName, - createdTime, - closeTime, - question, - description, - slug, - pool, - totalShares, - volume7Days, - volume24Hours, - isResolved, - resolution, -}: Contract) => { - return { - id, - creatorUsername, - creatorName, - createdTime, - closeTime, - question, - description, - url: `https://manifold.markets/${creatorUsername}/${slug}`, - pool: pool.YES + pool.NO, - probability: getProbability(totalShares), - volume7Days, - volume24Hours, - isResolved, - resolution, - } -} - -const firestore = admin.firestore() diff --git a/functions/src/on-create-comment.ts b/functions/src/on-create-comment.ts index 85af37ee..e48d6039 100644 --- a/functions/src/on-create-comment.ts +++ b/functions/src/on-create-comment.ts @@ -33,8 +33,9 @@ export const onCreateComment = functions.firestore const bet = betSnapshot.data() as Bet const answer = - contract.answers && - contract.answers.find((answer) => answer.id === bet.outcome) + contract.outcomeType === 'FREE_RESPONSE' && contract.answers + ? contract.answers.find((answer) => answer.id === bet.outcome) + : undefined const comments = await getValues( firestore.collection('contracts').doc(contractId).collection('comments') diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index f473a2e2..6971136b 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -4,11 +4,15 @@ import * as admin from 'firebase-admin' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { - getLoanAmount, - getNewBinaryBetInfo, + getNewBinaryCpmmBetInfo, + getNewBinaryDpmBetInfo, getNewMultiBetInfo, + getLoanAmount, } from '../../common/new-bet' +import { addObjects, removeUndefinedProps } from '../../common/util/object' import { Bet } from '../../common/bet' +import { redeemShares } from './redeem-shares' +import { Fees } from '../../common/fees' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -31,73 +35,111 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid outcome' } // run as transaction to prevent race conditions - return await firestore.runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) - return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User + return await firestore + .runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) + return { status: 'error', message: 'User not found' } + const user = userSnap.data() as User - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract - - const { closeTime, outcomeType } = contract - if (closeTime && Date.now() > closeTime) - return { status: 'error', message: 'Trading is closed' } - - const yourBetsSnap = await transaction.get( - contractDoc.collection('bets').where('userId', '==', userId) - ) - const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) - - const loanAmount = getLoanAmount(yourBets, amount) - if (user.balance < amount - loanAmount) - return { status: 'error', message: 'Insufficient balance' } - - if (outcomeType === 'FREE_RESPONSE') { - const answerSnap = await transaction.get( - contractDoc.collection('answers').doc(outcome) - ) - if (!answerSnap.exists) + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } - } + const contract = contractSnap.data() as Contract - const newBetDoc = firestore - .collection(`contracts/${contractId}/bets`) - .doc() + const { closeTime, outcomeType, mechanism, collectedFees } = contract + if (closeTime && Date.now() > closeTime) + return { status: 'error', message: 'Trading is closed' } - const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = - outcomeType === 'BINARY' - ? getNewBinaryBetInfo( - user, - outcome as 'YES' | 'NO', - amount, - loanAmount, - contract, - newBetDoc.id - ) - : getNewMultiBetInfo( - user, - outcome, - amount, - loanAmount, - contract, - newBetDoc.id - ) + const yourBetsSnap = await transaction.get( + contractDoc.collection('bets').where('userId', '==', userId) + ) + const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) - transaction.create(newBetDoc, newBet) - transaction.update(contractDoc, { - pool: newPool, - totalShares: newTotalShares, - totalBets: newTotalBets, + const loanAmount = getLoanAmount(yourBets, amount) + if (user.balance < amount - loanAmount) + return { status: 'error', message: 'Insufficient balance' } + + if (outcomeType === 'FREE_RESPONSE') { + const answerSnap = await transaction.get( + contractDoc.collection('answers').doc(outcome) + ) + if (!answerSnap.exists) + return { status: 'error', message: 'Invalid contract' } + } + + const newBetDoc = firestore + .collection(`contracts/${contractId}/bets`) + .doc() + + const { + newBet, + newPool, + newTotalShares, + newTotalBets, + newBalance, + newTotalLiquidity, + fees, + newP, + } = + outcomeType === 'BINARY' + ? mechanism === 'dpm-2' + ? getNewBinaryDpmBetInfo( + user, + outcome as 'YES' | 'NO', + amount, + contract, + loanAmount, + newBetDoc.id + ) + : (getNewBinaryCpmmBetInfo( + user, + outcome as 'YES' | 'NO', + amount, + contract, + loanAmount, + newBetDoc.id + ) as any) + : getNewMultiBetInfo( + user, + outcome, + amount, + contract as any, + loanAmount, + newBetDoc.id + ) + + if (newP !== undefined && !isFinite(newP)) { + return { + status: 'error', + message: 'Trade rejected due to overflow error.', + } + } + + transaction.create(newBetDoc, newBet) + + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + p: newP, + totalShares: newTotalShares, + totalBets: newTotalBets, + totalLiquidity: newTotalLiquidity, + collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), + }) + ) + + transaction.update(userDoc, { balance: newBalance }) + + return { status: 'success', betId: newBetDoc.id } + }) + .then(async (result) => { + await redeemShares(userId, contractId) + return result }) - transaction.update(userDoc, { balance: newBalance }) - - return { status: 'success', betId: newBetDoc.id } - }) } ) diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts new file mode 100644 index 00000000..55fb2d14 --- /dev/null +++ b/functions/src/redeem-shares.ts @@ -0,0 +1,90 @@ +import * as admin from 'firebase-admin' +import * as _ from 'lodash' + +import { Bet } from '../../common/bet' +import { getProbability } from '../../common/calculate' + +import { Binary, CPMM, FullContract } from '../../common/contract' +import { noFees } from '../../common/fees' +import { User } from '../../common/user' + +export const redeemShares = async (userId: string, contractId: string) => { + return await firestore.runTransaction(async (transaction) => { + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) + return { status: 'error', message: 'Invalid contract' } + + const contract = contractSnap.data() as FullContract + if (contract.outcomeType !== 'BINARY' || contract.mechanism !== 'cpmm-1') + return { status: 'success' } + + const betsSnap = await transaction.get( + firestore + .collection(`contracts/${contract.id}/bets`) + .where('userId', '==', userId) + ) + const bets = betsSnap.docs.map((doc) => doc.data() as Bet) + const [yesBets, noBets] = _.partition(bets, (b) => b.outcome === 'YES') + const yesShares = _.sumBy(yesBets, (b) => b.shares) + const noShares = _.sumBy(noBets, (b) => b.shares) + + const amount = Math.min(yesShares, noShares) + if (amount <= 0) return + + const prevLoanAmount = _.sumBy(bets, (bet) => bet.loanAmount ?? 0) + const loanPaid = Math.min(prevLoanAmount, amount) + const netAmount = amount - loanPaid + + const p = getProbability(contract) + const createdTime = Date.now() + + const yesDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() + const yesBet: Bet = { + id: yesDoc.id, + userId: userId, + contractId: contract.id, + amount: p * -amount, + shares: -amount, + loanAmount: loanPaid ? -loanPaid / 2 : 0, + outcome: 'YES', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + fees: noFees, + } + + const noDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() + const noBet: Bet = { + id: noDoc.id, + userId: userId, + contractId: contract.id, + amount: (1 - p) * -amount, + shares: -amount, + loanAmount: loanPaid ? -loanPaid / 2 : 0, + outcome: 'NO', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + fees: noFees, + } + + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) return { status: 'error', message: 'User not found' } + + const user = userSnap.data() as User + + const newBalance = user.balance + netAmount + transaction.update(userDoc, { balance: newBalance }) + + transaction.create(yesDoc, yesBet) + transaction.create(noDoc, noBet) + + return { status: 'success' } + }) +} + +const firestore = admin.firestore() diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 9e3746a2..6fe9e46d 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -5,14 +5,11 @@ import * as _ from 'lodash' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' -import { getUser, payUser } from './utils' +import { getUser, isProd, payUser } from './utils' import { sendMarketResolutionEmail } from './emails' -import { - getLoanPayouts, - getPayouts, - getPayoutsMultiOutcome, -} from '../../common/payouts' +import { getLoanPayouts, getPayouts } from '../../common/payouts' import { removeUndefinedProps } from '../../common/util/object' +import { LiquidityProvision } from '../../common/liquidity-provision' export const resolveMarket = functions .runWith({ minInstances: 1 }) @@ -78,6 +75,29 @@ export const resolveMarket = functions ? Math.min(closeTime, resolutionTime) : closeTime + const betsSnap = await firestore + .collection(`contracts/${contractId}/bets`) + .get() + + const bets = betsSnap.docs.map((doc) => doc.data() as Bet) + + const liquiditiesSnap = await firestore + .collection(`contracts/${contractId}/liquidity`) + .get() + + const liquidities = liquiditiesSnap.docs.map( + (doc) => doc.data() as LiquidityProvision + ) + + const [payouts, collectedFees] = getPayouts( + outcome, + resolutions ?? {}, + contract, + bets, + liquidities, + resolutionProbability + ) + await contractDoc.update( removeUndefinedProps({ isResolved: true, @@ -86,26 +106,16 @@ export const resolveMarket = functions closeTime: newCloseTime, resolutionProbability, resolutions, + collectedFees, }) ) 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 openBets = bets.filter((b) => !b.isSold && !b.sale) - - const payouts = - outcomeType === 'FREE_RESPONSE' && resolutions - ? getPayoutsMultiOutcome(resolutions, contract, openBets) - : getPayouts(outcome, contract, openBets, resolutionProbability) - const loanPayouts = getLoanPayouts(openBets) - console.log('payouts:', payouts) + if (!isProd) console.log('payouts:', payouts) const groups = _.groupBy( [...payouts, ...loanPayouts], diff --git a/functions/src/scripts/correct-bet-probability.ts b/functions/src/scripts/correct-bet-probability.ts index 47ffdb58..30be7a1c 100644 --- a/functions/src/scripts/correct-bet-probability.ts +++ b/functions/src/scripts/correct-bet-probability.ts @@ -5,13 +5,16 @@ import { initAdmin } from './script-init' initAdmin('stephen') import { Bet } from '../../../common/bet' -import { getProbability } from '../../../common/calculate' -import { Contract } from '../../../common/contract' +import { getDpmProbability } from '../../../common/calculate-dpm' +import { Binary, Contract, DPM, FullContract } from '../../../common/contract' type DocRef = admin.firestore.DocumentReference const firestore = admin.firestore() -async function migrateContract(contractRef: DocRef, contract: Contract) { +async function migrateContract( + contractRef: DocRef, + contract: FullContract +) { const bets = await contractRef .collection('bets') .get() @@ -19,7 +22,7 @@ async function migrateContract(contractRef: DocRef, contract: Contract) { const lastBet = _.sortBy(bets, (bet) => -bet.createdTime)[0] if (lastBet) { - const probAfter = getProbability(contract.totalShares) + const probAfter = getDpmProbability(contract.totalShares) await firestore .doc(`contracts/${contract.id}/bets/${lastBet.id}`) @@ -31,7 +34,9 @@ async function migrateContract(contractRef: DocRef, contract: Contract) { async function migrateContracts() { const snapshot = await firestore.collection('contracts').get() - const contracts = snapshot.docs.map((doc) => doc.data() as Contract) + const contracts = snapshot.docs.map( + (doc) => doc.data() as FullContract + ) console.log('Loaded contracts', contracts.length) diff --git a/functions/src/scripts/migrate-to-dpm-2.ts b/functions/src/scripts/migrate-to-dpm-2.ts index 80523a95..a7e611b6 100644 --- a/functions/src/scripts/migrate-to-dpm-2.ts +++ b/functions/src/scripts/migrate-to-dpm-2.ts @@ -4,9 +4,12 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin('stephenDev') -import { Contract } from '../../../common/contract' +import { Binary, Contract, DPM, FullContract } from '../../../common/contract' import { Bet } from '../../../common/bet' -import { calculateShares, getProbability } from '../../../common/calculate' +import { + calculateDpmShares, + getDpmProbability, +} from '../../../common/calculate-dpm' import { getSellBetInfo } from '../../../common/sell-bet' import { User } from '../../../common/user' @@ -29,7 +32,7 @@ async function recalculateContract( await firestore.runTransaction(async (transaction) => { const contractDoc = await transaction.get(contractRef) - const contract = contractDoc.data() as Contract + const contract = contractDoc.data() as FullContract const betDocs = await transaction.get(contractRef.collection('bets')) const bets = _.sortBy( @@ -126,7 +129,7 @@ async function recalculateContract( continue } - const shares = calculateShares(totalShares, bet.amount, bet.outcome) + const shares = calculateDpmShares(totalShares, bet.amount, bet.outcome) const probBefore = p const ind = bet.outcome === 'YES' ? 1 : 0 @@ -145,7 +148,7 @@ async function recalculateContract( NO: totalBets.NO + (1 - ind) * bet.amount, } - p = getProbability(totalShares) + p = getDpmProbability(totalShares) const probAfter = p diff --git a/functions/src/scripts/pay-out-contract-again.ts b/functions/src/scripts/pay-out-contract-again.ts index 96671ed8..2696ddcb 100644 --- a/functions/src/scripts/pay-out-contract-again.ts +++ b/functions/src/scripts/pay-out-contract-again.ts @@ -6,13 +6,8 @@ initAdmin('james') import { Bet } from '../../../common/bet' import { Contract } from '../../../common/contract' -import { - getLoanPayouts, - getPayouts, - getPayoutsMultiOutcome, -} from '../../../common/payouts' +import { getLoanPayouts, getPayouts } from '../../../common/payouts' import { filterDefined } from '../../../common/util/array' -import { payUser } from '../utils' type DocRef = admin.firestore.DocumentReference @@ -28,12 +23,15 @@ async function checkIfPayOutAgain(contractRef: DocRef, contract: Contract) { const loanedBets = openBets.filter((bet) => bet.loanAmount) if (loanedBets.length && contract.resolution) { - const { resolution, outcomeType, resolutions, resolutionProbability } = - contract - const payouts = - outcomeType === 'FREE_RESPONSE' && resolutions - ? getPayoutsMultiOutcome(resolutions, contract, openBets) - : getPayouts(resolution, contract, openBets, resolutionProbability) + const { resolution, resolutions, resolutionProbability } = contract as any + const [payouts] = getPayouts( + resolution, + resolutions, + contract, + openBets, + [], + resolutionProbability + ) const loanPayouts = getLoanPayouts(openBets) const groups = _.groupBy( diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index a5a7b30e..14c15dd7 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -4,7 +4,9 @@ import * as functions from 'firebase-functions' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' -import { getSellBetInfo } from '../../common/sell-bet' +import { getCpmmSellBetInfo, getSellBetInfo } from '../../common/sell-bet' +import { addObjects, removeUndefinedProps } from '../../common/util/object' +import { Fees } from '../../common/fees' export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -33,7 +35,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { closeTime } = contract + const { closeTime, mechanism, collectedFees } = contract if (closeTime && Date.now() > closeTime) return { status: 'error', message: 'Trading is closed' } @@ -54,31 +56,30 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( newTotalShares, newTotalBets, newBalance, - creatorFee, - } = getSellBetInfo(user, bet, contract, newBetDoc.id) + fees, + } = + mechanism === 'dpm-2' + ? getSellBetInfo(user, bet, contract, newBetDoc.id) + : (getCpmmSellBetInfo( + user, + bet, + contract as any, + newBetDoc.id + ) as any) - if (contract.creatorId === userId) { - transaction.update(userDoc, { balance: newBalance + creatorFee }) - } else { - const creatorDoc = firestore.doc(`users/${contract.creatorId}`) - const creatorSnap = await transaction.get(creatorDoc) - - if (creatorSnap.exists) { - const creator = creatorSnap.data() as User - const creatorNewBalance = creator.balance + creatorFee - transaction.update(creatorDoc, { balance: creatorNewBalance }) - } - - transaction.update(userDoc, { balance: newBalance }) - } + transaction.update(userDoc, { balance: newBalance }) transaction.update(betDoc, { isSold: true }) transaction.create(newBetDoc, newBet) - transaction.update(contractDoc, { - pool: newPool, - totalShares: newTotalShares, - totalBets: newTotalBets, - }) + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + totalShares: newTotalShares, + totalBets: newTotalBets, + collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), + }) + ) return { status: 'success' } }) diff --git a/functions/src/update-user-metrics.ts b/functions/src/update-user-metrics.ts index 358dd936..50069423 100644 --- a/functions/src/update-user-metrics.ts +++ b/functions/src/update-user-metrics.ts @@ -6,7 +6,7 @@ import { getValues } from './utils' import { Contract } from '../../common/contract' import { Bet } from '../../common/bet' import { User } from '../../common/user' -import { calculatePayout } from '../../common/calculate' +import { calculateDpmPayout } from '../../common/calculate-dpm' import { batchedWaitAll } from '../../common/util/promise' const firestore = admin.firestore() @@ -53,7 +53,7 @@ const computeInvestmentValue = async ( if (!contract || contract.isResolved) return 0 if (bet.sale || bet.isSold) return 0 - const payout = calculatePayout(contract, bet, 'MKT') + const payout = calculateDpmPayout(contract, bet, 'MKT') return payout - (bet.loanAmount ?? 0) }) } diff --git a/functions/src/utils.ts b/functions/src/utils.ts index a5d5640f..88c25570 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -53,6 +53,11 @@ const updateUserBalance = ( const newUserBalance = user.balance + delta + // if (newUserBalance < 0) + // throw new Error( + // `User (${userId}) balance cannot be negative: ${newUserBalance}` + // ) + if (isDeposit) { const newTotalDeposits = (user.totalDeposits || 0) + delta transaction.update(userDoc, { totalDeposits: newTotalDeposits }) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 82b0967b..e426c370 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react' import { XIcon } from '@heroicons/react/solid' import { Answer } from '../../../common/answer' -import { Contract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { AmountInput } from '../amount-input' import { Col } from '../layout/col' import { placeBet } from '../../lib/firebase/api-call' @@ -18,17 +18,17 @@ import { import { InfoTooltip } from '../info-tooltip' import { useUser } from '../../hooks/use-user' import { - getProbabilityAfterBet, - getOutcomeProbability, - calculateShares, - calculatePayoutAfterCorrectBet, -} from '../../../common/calculate' + getDpmOutcomeProbability, + calculateDpmShares, + calculateDpmPayoutAfterCorrectBet, + getDpmOutcomeProbabilityAfterBet, +} from '../../../common/calculate-dpm' import { firebaseLogin } from '../../lib/firebase/users' import { Bet } from '../../../common/bet' export function AnswerBetPanel(props: { answer: Answer - contract: Contract + contract: FullContract closePanel: () => void className?: string }) { @@ -71,18 +71,22 @@ export function AnswerBetPanel(props: { const betDisabled = isSubmitting || !betAmount || error - const initialProb = getOutcomeProbability(contract.totalShares, answer.id) + const initialProb = getDpmOutcomeProbability(contract.totalShares, answer.id) - const resultProb = getProbabilityAfterBet( + const resultProb = getDpmOutcomeProbabilityAfterBet( contract.totalShares, answerId, betAmount ?? 0 ) - const shares = calculateShares(contract.totalShares, betAmount ?? 0, answerId) + const shares = calculateDpmShares( + contract.totalShares, + betAmount ?? 0, + answerId + ) const currentPayout = betAmount - ? calculatePayoutAfterCorrectBet(contract, { + ? calculateDpmPayoutAfterCorrectBet(contract, { outcome: answerId, amount: betAmount, shares, diff --git a/web/components/answers/answer-item.tsx b/web/components/answers/answer-item.tsx index b4ac8f44..c48f2048 100644 --- a/web/components/answers/answer-item.tsx +++ b/web/components/answers/answer-item.tsx @@ -3,14 +3,14 @@ import _ from 'lodash' import { useState } from 'react' import { Answer } from '../../../common/answer' -import { Contract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { Col } from '../layout/col' import { Row } from '../layout/row' import { Avatar } from '../avatar' import { SiteLink } from '../site-link' import { BuyButton } from '../yes-no-selector' import { formatPercent } from '../../../common/util/format' -import { getOutcomeProbability } from '../../../common/calculate' +import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' import { tradingAllowed } from '../../lib/firebase/contracts' import { AnswerBetPanel } from './answer-bet-panel' import { Linkify } from '../linkify' @@ -19,7 +19,7 @@ import { ContractActivity } from '../feed/contract-activity' export function AnswerItem(props: { answer: Answer - contract: Contract + contract: FullContract user: User | null | undefined showChoice: 'radio' | 'checkbox' | undefined chosenProb: number | undefined @@ -41,7 +41,7 @@ export function AnswerItem(props: { const { username, avatarUrl, name, number, text } = answer const isChosen = chosenProb !== undefined - const prob = getOutcomeProbability(totalShares, answer.id) + const prob = getDpmOutcomeProbability(totalShares, answer.id) const roundedProb = Math.round(prob * 100) const probPercent = formatPercent(prob) const wasResolvedTo = diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index c244d72b..ae79cdf3 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx' import _ from 'lodash' import { useState } from 'react' -import { Contract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { Col } from '../layout/col' import { resolveMarket } from '../../lib/firebase/api-call' import { Row } from '../layout/row' @@ -11,7 +11,7 @@ import { ResolveConfirmationButton } from '../confirmation-button' import { removeUndefinedProps } from '../../../common/util/object' export function AnswerResolvePanel(props: { - contract: Contract + contract: FullContract resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined setResolveOption: ( option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined diff --git a/web/components/answers/answers-graph.tsx b/web/components/answers/answers-graph.tsx index 27b8846d..63824dd4 100644 --- a/web/components/answers/answers-graph.tsx +++ b/web/components/answers/answers-graph.tsx @@ -4,20 +4,28 @@ import dayjs from 'dayjs' import _ from 'lodash' import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' +import { + Contract, + DPM, + FreeResponse, + FullContract, +} from '../../../common/contract' import { getOutcomeProbability } from '../../../common/calculate' import { useBets } from '../../hooks/use-bets' import { useWindowSize } from '../../hooks/use-window-size' -export function AnswersGraph(props: { contract: Contract; bets: Bet[] }) { +export function AnswersGraph(props: { + contract: FullContract + bets: Bet[] +}) { const { contract } = props - const { resolutionTime, closeTime, answers, totalShares } = contract + const { resolutionTime, closeTime, answers } = contract const bets = (useBets(contract.id) ?? props.bets).filter((bet) => !bet.sale) const { probsByOutcome, sortedOutcomes } = computeProbsByOutcome( bets, - totalShares + contract ) const isClosed = !!closeTime && Date.now() > closeTime @@ -134,10 +142,7 @@ function formatTime(time: number, includeTime: boolean) { return dayjs(time).format('MMM D') } -const computeProbsByOutcome = ( - bets: Bet[], - totalShares: { [outcome: string]: number } -) => { +const computeProbsByOutcome = (bets: Bet[], contract: Contract) => { const betsByOutcome = _.groupBy(bets, (bet) => bet.outcome) const outcomes = Object.keys(betsByOutcome).filter((outcome) => { const maxProb = Math.max( @@ -148,7 +153,7 @@ const computeProbsByOutcome = ( const trackedOutcomes = _.sortBy( outcomes, - (outcome) => -1 * getOutcomeProbability(totalShares, outcome) + (outcome) => -1 * getOutcomeProbability(contract, outcome) ).slice(0, 5) const probsByOutcome = _.fromPairs( diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 45b47ef4..d8928c49 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -2,18 +2,21 @@ import _ from 'lodash' import { useLayoutEffect, useState } from 'react' import { Answer } from '../../../common/answer' -import { Contract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { Col } from '../layout/col' import { formatPercent } from '../../../common/util/format' import { useUser } from '../../hooks/use-user' -import { getOutcomeProbability } from '../../../common/calculate' +import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' import { useAnswers } from '../../hooks/use-answers' import { tradingAllowed } from '../../lib/firebase/contracts' import { AnswerItem } from './answer-item' import { CreateAnswerPanel } from './create-answer-panel' import { AnswerResolvePanel } from './answer-resolve-panel' -export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) { +export function AnswersPanel(props: { + contract: FullContract + answers: Answer[] +}) { const { contract } = props const { creatorId, resolution, resolutions, totalBets } = contract @@ -31,7 +34,7 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) { ), ..._.sortBy( otherAnswers, - (answer) => -1 * getOutcomeProbability(contract.totalShares, answer.id) + (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id) ), ] @@ -100,7 +103,7 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) { ) : (
None of the above:{' '} - {formatPercent(getOutcomeProbability(contract.totalShares, '0'))} + {formatPercent(getDpmOutcomeProbability(contract.totalShares, '0'))}
)} diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 8e5b88a8..35e04db2 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -3,7 +3,7 @@ import _ from 'lodash' import { useState } from 'react' import Textarea from 'react-expanding-textarea' -import { Contract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { AmountInput } from '../amount-input' import { Col } from '../layout/col' import { createAnswer } from '../../lib/firebase/api-call' @@ -16,14 +16,16 @@ import { import { InfoTooltip } from '../info-tooltip' import { useUser } from '../../hooks/use-user' import { - getProbabilityAfterBet, - calculateShares, - calculatePayoutAfterCorrectBet, -} from '../../../common/calculate' + calculateDpmShares, + calculateDpmPayoutAfterCorrectBet, + getDpmOutcomeProbabilityAfterBet, +} from '../../../common/calculate-dpm' import { firebaseLogin } from '../../lib/firebase/users' import { Bet } from '../../../common/bet' -export function CreateAnswerPanel(props: { contract: Contract }) { +export function CreateAnswerPanel(props: { + contract: FullContract +}) { const { contract } = props const user = useUser() const [text, setText] = useState('') @@ -53,16 +55,16 @@ export function CreateAnswerPanel(props: { contract: Contract }) { } } - const resultProb = getProbabilityAfterBet( + const resultProb = getDpmOutcomeProbabilityAfterBet( contract.totalShares, 'new', betAmount ?? 0 ) - const shares = calculateShares(contract.totalShares, betAmount ?? 0, 'new') + const shares = calculateDpmShares(contract.totalShares, betAmount ?? 0, 'new') const currentPayout = betAmount - ? calculatePayoutAfterCorrectBet(contract, { + ? calculateDpmPayoutAfterCorrectBet(contract, { outcome: 'new', amount: betAmount, shares, diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 03b3e34a..21c15a50 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx' -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useUser } from '../hooks/use-user' -import { Contract } from '../../common/contract' +import { Binary, CPMM, DPM, FullContract } from '../../common/contract' import { Col } from './layout/col' import { Row } from './layout/row' import { Spacer } from './layout/spacer' @@ -13,31 +13,22 @@ import { formatWithCommas, } from '../../common/util/format' import { Title } from './title' -import { - getProbability, - calculateShares, - getProbabilityAfterBet, - calculatePayoutAfterCorrectBet, -} from '../../common/calculate' import { firebaseLogin } from '../lib/firebase/users' import { Bet } from '../../common/bet' import { placeBet } from '../lib/firebase/api-call' import { AmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' import { OutcomeLabel } from './outcome-label' - -// Focus helper from https://stackoverflow.com/a/54159564/1222351 -function useFocus(): [React.RefObject, () => void] { - const htmlElRef = useRef(null) - const setFocus = () => { - htmlElRef.current && htmlElRef.current.focus() - } - - return [htmlElRef, setFocus] -} +import { + calculatePayoutAfterCorrectBet, + calculateShares, + getProbability, + getOutcomeProbabilityAfterBet, +} from '../../common/calculate' +import { useFocus } from '../hooks/use-focus' export function BetPanel(props: { - contract: Contract + contract: FullContract className?: string title?: string // Set if BetPanel is on a feed modal selected?: 'YES' | 'NO' @@ -49,7 +40,6 @@ export function BetPanel(props: { }, []) const { contract, className, title, selected, onBetSuccess } = props - const { totalShares, phantomShares } = contract const user = useUser() @@ -102,20 +92,16 @@ export function BetPanel(props: { const betDisabled = isSubmitting || !betAmount || error - const initialProb = getProbability(contract.totalShares) + const initialProb = getProbability(contract) - const outcomeProb = getProbabilityAfterBet( - contract.totalShares, + const outcomeProb = getOutcomeProbabilityAfterBet( + contract, betChoice || 'YES', betAmount ?? 0 ) const resultProb = betChoice === 'NO' ? 1 - outcomeProb : outcomeProb - const shares = calculateShares( - contract.totalShares, - betAmount ?? 0, - betChoice || 'YES' - ) + const shares = calculateShares(contract, betAmount ?? 0, betChoice || 'YES') const currentPayout = betAmount ? calculatePayoutAfterCorrectBet(contract, { @@ -127,11 +113,23 @@ export function BetPanel(props: { const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturnPercent = (currentReturn * 100).toFixed() + '%' + const panelTitle = title ?? 'Place a trade' if (title) { focusAmountInput() } + const tooltip = + contract.mechanism === 'dpm-2' + ? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas( + shares + + contract.totalShares[betChoice ?? 'YES'] - + (contract.phantomShares + ? contract.phantomShares[betChoice ?? 'YES'] + : 0) + )} ${betChoice} shares` + : undefined + return ( Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> </div> - <InfoTooltip - text={`Current payout for ${formatWithCommas( - shares - )} / ${formatWithCommas( - shares + - totalShares[betChoice ?? 'YES'] - - (phantomShares ? phantomShares[betChoice ?? 'YES'] : 0) - )} ${betChoice} shares`} - /> + + {tooltip && <InfoTooltip text={tooltip} />} </Row> <Row className="flex-wrap items-end justify-end gap-2"> <span className="whitespace-nowrap"> diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index 60967e25..f766e51d 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -1,14 +1,15 @@ import clsx from 'clsx' import { Fragment, useState } from 'react' import { Dialog, Transition } from '@headlessui/react' -import { Contract } from '../lib/firebase/contracts' + import { BetPanel } from './bet-panel' import { Row } from './layout/row' import { YesNoSelector } from './yes-no-selector' +import { Binary, CPMM, DPM, FullContract } from '../../common/contract' // Inline version of a bet panel. Opens BetPanel in a new modal. export default function BetRow(props: { - contract: Contract + contract: FullContract<DPM | CPMM, Binary> className?: string labelClassName?: string }) { diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 6c8dac64..4b367621 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -22,6 +22,12 @@ import { } from '../lib/firebase/contracts' import { Row } from './layout/row' import { UserLink } from './user-page' +import { sellBet } from '../lib/firebase/api-call' +import { ConfirmationButton } from './confirmation-button' +import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' +import { filterDefined } from '../../common/util/array' +import { LoadingIndicator } from './loading-indicator' +import { SiteLink } from './site-link' import { calculatePayout, calculateSaleAmount, @@ -30,12 +36,6 @@ import { getProbabilityAfterSale, resolvedPayout, } from '../../common/calculate' -import { sellBet } from '../lib/firebase/api-call' -import { ConfirmationButton } from './confirmation-button' -import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' -import { filterDefined } from '../../common/util/array' -import { LoadingIndicator } from './loading-indicator' -import { SiteLink } from './site-link' type BetSort = 'newest' | 'profit' | 'settled' | 'value' type BetFilter = 'open' | 'closed' | 'resolved' | 'all' @@ -400,7 +400,7 @@ export function MyBetsSummary(props: { <> Payout at{' '} <span className="text-blue-400"> - {formatPercent(getProbability(contract.totalShares))} + {formatPercent(getProbability(contract))} </span> </> ) : ( @@ -427,28 +427,46 @@ export function ContractBetsTable(props: { const { contract, bets, className } = props const [sales, buys] = _.partition(bets, (bet) => bet.sale) + const salesDict = _.fromPairs( sales.map((sale) => [sale.sale?.betId ?? '', sale]) ) - const { isResolved } = contract + const [redemptions, normalBets] = _.partition(buys, (b) => b.isRedemption) + const amountRedeemed = Math.floor( + -0.5 * _.sumBy(redemptions, (b) => b.shares) + ) + + const { isResolved, mechanism } = contract + const isCPMM = mechanism === 'cpmm-1' + return ( <div className={clsx('overflow-x-auto', className)}> + {amountRedeemed > 0 && ( + <> + <div className="text-gray-500 text-sm pl-2"> + {amountRedeemed} YES shares and {amountRedeemed} NO shares + automatically redeemed for {formatMoney(amountRedeemed)}. + </div> + <Spacer h={4} /> + </> + )} + <table className="table-zebra table-compact table w-full text-gray-500"> <thead> <tr className="p-2"> <th></th> <th>Outcome</th> <th>Amount</th> - <th>{isResolved ? <>Payout</> : <>Sale price</>}</th> - {!isResolved && <th>Payout if chosen</th>} - <th>Probability</th> + {!isCPMM && <th>{isResolved ? <>Payout</> : <>Sale price</>}</th>} + {!isCPMM && !isResolved && <th>Payout if chosen</th>} <th>Shares</th> + <th>Probability</th> <th>Date</th> </tr> </thead> <tbody> - {buys.map((bet) => ( + {normalBets.map((bet) => ( <BetRow key={bet.id} bet={bet} @@ -476,9 +494,12 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) { loanAmount, } = bet - const { isResolved, closeTime } = contract + const { isResolved, closeTime, mechanism } = contract + const isClosed = closeTime && Date.now() > closeTime + const isCPMM = mechanism === 'cpmm-1' + const saleAmount = saleBet?.sale?.amount const saleDisplay = isAnte ? ( @@ -501,7 +522,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) { return ( <tr> <td className="text-neutral"> - {!isResolved && !isClosed && !isSold && !isAnte && ( + {!isCPMM && !isResolved && !isClosed && !isSold && !isAnte && ( <SellButton contract={contract} bet={bet} /> )} </td> @@ -512,12 +533,12 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) { {formatMoney(amount)} {loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''} </td> - <td>{saleDisplay}</td> - {!isResolved && <td>{payoutIfChosenDisplay}</td>} + {!isCPMM && <td>{saleDisplay}</td>} + {!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>} + <td>{formatWithCommas(shares)}</td> <td> {formatPercent(probBefore)} → {formatPercent(probAfter)} </td> - <td>{formatWithCommas(shares)}</td> <td>{dayjs(createdTime).format('MMM D, h:mma')}</td> </tr> ) @@ -535,15 +556,11 @@ function SellButton(props: { contract: Contract; bet: Bet }) { const [isSubmitting, setIsSubmitting] = useState(false) const initialProb = getOutcomeProbability( - contract.totalShares, + contract, outcome === 'NO' ? 'YES' : outcome ) - const outcomeProb = getProbabilityAfterSale( - contract.totalShares, - outcome, - shares - ) + const outcomeProb = getProbabilityAfterSale(contract, outcome, shares) const saleAmount = calculateSaleAmount(contract, bet) const profit = saleAmount - bet.amount diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index cadf7764..1e36fb6b 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -125,7 +125,7 @@ function AbbrContractDetails(props: { }) { const { contract, showHotVolume, showCloseTime } = props const { volume24Hours, creatorName, creatorUsername, closeTime } = contract - const { truePool } = contractMetrics(contract) + const { liquidityLabel } = contractMetrics(contract) return ( <Col className={clsx('gap-2 text-sm text-gray-500')}> @@ -156,7 +156,7 @@ function AbbrContractDetails(props: { ) : ( <Row className="gap-1"> {/* <DatabaseIcon className="h-5 w-5" /> */} - {formatMoney(truePool)} pool + {liquidityLabel} </Row> )} </Row> @@ -170,7 +170,8 @@ export function ContractDetails(props: { }) { const { contract, isCreator } = props const { closeTime, creatorName, creatorUsername } = contract - const { truePool, createdDate, resolvedDate } = contractMetrics(contract) + const { liquidityLabel, createdDate, resolvedDate } = + contractMetrics(contract) const tweetText = getTweetText(contract, !!isCreator) @@ -224,7 +225,7 @@ export function ContractDetails(props: { <Row className="items-center gap-1"> <DatabaseIcon className="h-5 w-5" /> - <div className="whitespace-nowrap">{formatMoney(truePool)} pool</div> + <div className="whitespace-nowrap">{liquidityLabel}</div> </Row> <TweetButton className={'self-end'} tweetText={tweetText} /> @@ -236,7 +237,8 @@ export function ContractDetails(props: { // String version of the above, to send to the OpenGraph image generator export function contractTextDetails(contract: Contract) { const { closeTime, tags } = contract - const { truePool, createdDate, resolvedDate } = contractMetrics(contract) + const { createdDate, resolvedDate, liquidityLabel } = + contractMetrics(contract) const hashtags = tags.map((tag) => `#${tag}`) @@ -247,7 +249,7 @@ export function contractTextDetails(contract: Contract) { closeTime ).format('MMM D, h:mma')}` : '') + - ` • ${formatMoney(truePool)} pool` + + ` • ${liquidityLabel}` + (hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '') ) } diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx index f92f2ade..e5ac8bb2 100644 --- a/web/components/contract-overview.tsx +++ b/web/components/contract-overview.tsx @@ -20,6 +20,7 @@ import { Fold } from '../../common/fold' import { FoldTagList } from './tags-list' import { ContractActivity } from './feed/contract-activity' import { AnswersGraph } from './answers/answers-graph' +import { DPM, FreeResponse, FullContract } from '../../common/contract' export const ContractOverview = (props: { contract: Contract @@ -81,7 +82,10 @@ export const ContractOverview = (props: { {isBinary ? ( <ContractProbGraph contract={contract} bets={bets} /> ) : ( - <AnswersGraph contract={contract} bets={bets} /> + <AnswersGraph + contract={contract as FullContract<DPM, FreeResponse>} + bets={bets} + /> )} {children} diff --git a/web/components/contract-prob-graph.tsx b/web/components/contract-prob-graph.tsx index 8ece4335..4513c339 100644 --- a/web/components/contract-prob-graph.tsx +++ b/web/components/contract-prob-graph.tsx @@ -2,27 +2,29 @@ import { DatumValue } from '@nivo/core' import { ResponsiveLine } from '@nivo/line' import dayjs from 'dayjs' import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' +import { getInitialProbability } from '../../common/calculate' +import { Binary, CPMM, DPM, FullContract } from '../../common/contract' import { useBetsWithoutAntes } from '../hooks/use-bets' import { useWindowSize } from '../hooks/use-window-size' -import { Contract } from '../lib/firebase/contracts' -export function ContractProbGraph(props: { contract: Contract; bets: Bet[] }) { +export function ContractProbGraph(props: { + contract: FullContract<DPM | CPMM, Binary> + bets: Bet[] +}) { const { contract } = props - const { phantomShares, resolutionTime, closeTime } = contract + const { resolutionTime, closeTime } = contract - const bets = useBetsWithoutAntes(contract, props.bets) - - const startProb = getProbability( - phantomShares as { [outcome: string]: number } + const bets = useBetsWithoutAntes(contract, props.bets).filter( + (b) => !b.isRedemption ) - const times = bets - ? [contract.createdTime, ...bets.map((bet) => bet.createdTime)].map( - (time) => new Date(time) - ) - : [] - const probs = bets ? [startProb, ...bets.map((bet) => bet.probAfter)] : [] + const startProb = getInitialProbability(contract) + + const times = [ + contract.createdTime, + ...bets.map((bet) => bet.createdTime), + ].map((time) => new Date(time)) + const probs = [startProb, ...bets.map((bet) => bet.probAfter)] const isClosed = !!closeTime && Date.now() > closeTime const latestTime = dayjs( diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 214c5e7b..5cc8376d 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -13,6 +13,7 @@ import { Col } from './layout/col' import { SiteLink } from './site-link' import { ContractCard } from './contract-card' import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' +import { Answer } from '../../common/answer' export function ContractsGrid(props: { contracts: Contract[] @@ -217,7 +218,11 @@ export function SearchableGrid(props: { check(c.creatorName) || check(c.creatorUsername) || check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) || - check((c.answers ?? []).map((answer) => answer.text).join(' ')) + check( + ((c as any).answers ?? []) + .map((answer: Answer) => answer.text) + .join(' ') + ) ) if (sort === 'newest' || sort === 'all') { diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index 08ae0178..01507afd 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -4,7 +4,12 @@ import { Answer } from '../../../common/answer' import { Bet } from '../../../common/bet' import { getOutcomeProbability } from '../../../common/calculate' import { Comment } from '../../../common/comment' -import { Contract } from '../../../common/contract' +import { + Contract, + DPM, + FreeResponse, + FullContract, +} from '../../../common/contract' import { User } from '../../../common/user' import { mapCommentsByBetId } from '../../lib/firebase/comments' @@ -169,7 +174,7 @@ function groupBets( } function getAnswerGroups( - contract: Contract, + contract: FullContract<DPM, FreeResponse>, bets: Bet[], comments: Comment[], user: User | undefined | null, @@ -181,7 +186,7 @@ function getAnswerGroups( const { sortByProb, abbreviated } = options let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter( - (outcome) => getOutcomeProbability(contract.totalShares, outcome) > 0.01 + (outcome) => getOutcomeProbability(contract, outcome) > 0.01 ) if (abbreviated) { const lastComment = _.last(comments) @@ -204,7 +209,7 @@ function getAnswerGroups( if (sortByProb) { outcomes = _.sortBy( outcomes, - (outcome) => -1 * getOutcomeProbability(contract.totalShares, outcome) + (outcome) => -1 * getOutcomeProbability(contract, outcome) ) } else { // Sort by recent bet. @@ -266,7 +271,9 @@ export function getAllContractActivityItems( let answer: Answer | undefined if (filterToOutcome) { bets = bets.filter((bet) => bet.outcome === filterToOutcome) - answer = contract.answers?.find((answer) => answer.id === filterToOutcome) + answer = (contract as FullContract<DPM, FreeResponse>).answers?.find( + (answer) => answer.id === filterToOutcome + ) } const items: ActivityItem[] = @@ -278,10 +285,16 @@ export function getAllContractActivityItems( items.push( ...(outcomeType === 'FREE_RESPONSE' && !filterToOutcome - ? getAnswerGroups(contract, bets, comments, user, { - sortByProb: true, - abbreviated, - }) + ? getAnswerGroups( + contract as FullContract<DPM, FreeResponse>, + bets, + comments, + user, + { + sortByProb: true, + abbreviated, + } + ) : groupBets(bets, comments, contract, user?.id, { hideOutcome: !!filterToOutcome, abbreviated, @@ -317,10 +330,16 @@ export function getRecentContractActivityItems( const items = contract.outcomeType === 'FREE_RESPONSE' - ? getAnswerGroups(contract, bets, comments, user, { - sortByProb: false, - abbreviated: true, - }) + ? getAnswerGroups( + contract as FullContract<DPM, FreeResponse>, + bets, + comments, + user, + { + sortByProb: false, + abbreviated: true, + } + ) : groupBets(bets, comments, contract, user?.id, { hideOutcome: false, abbreviated: true, diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index be9a9e21..82815e4f 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -1,6 +1,6 @@ // From https://tailwindui.com/components/application-ui/lists/feeds import { Fragment, useState } from 'react' -import _ from 'lodash' +import * as _ from 'lodash' import { BanIcon, CheckIcon, @@ -46,6 +46,7 @@ import { Avatar } from '../avatar' import { useAdmin } from '../../hooks/use-admin' import { Answer } from '../../../common/answer' import { ActivityItem } from './activity-items' +import { FreeResponse, FullContract } from '../../../common/contract' export function FeedItems(props: { contract: Contract @@ -191,12 +192,6 @@ function FeedBet(props: { const bought = amount >= 0 ? 'bought' : 'sold' const money = formatMoney(Math.abs(amount)) - const answer = - !hideOutcome && - (contract.answers?.find((answer: Answer) => answer?.id === outcome) as - | Answer - | undefined) - return ( <> <div> @@ -222,18 +217,13 @@ function FeedBet(props: { </div> )} </div> - <div className={clsx('min-w-0 flex-1 pb-1.5', !answer && 'pt-1.5')}> - {answer && ( - <div className="text-neutral mb-2" style={{ fontSize: 15 }}> - <Linkify text={answer.text} /> - </div> - )} + <div className={'min-w-0 flex-1 pb-1.5'}> <div className="text-sm text-gray-500"> <span> {isSelf ? 'You' : isCreator ? contract.creatorName : 'A trader'} </span>{' '} {bought} {money} - {!answer && !hideOutcome && ( + {!hideOutcome && ( <> {' '} of <OutcomeLabel outcome={outcome} /> @@ -425,7 +415,7 @@ export function FeedQuestion(props: { const { contract, showDescription } = props const { creatorName, creatorUsername, question, resolution, outcomeType } = contract - const { truePool } = contractMetrics(contract) + const { liquidityLabel } = contractMetrics(contract) const isBinary = outcomeType === 'BINARY' const closeMessage = @@ -453,7 +443,7 @@ export function FeedQuestion(props: { asked {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} <span className="float-right hidden text-gray-400 sm:inline"> - {formatMoney(truePool)} pool + {liquidityLabel} {closeMessage} </span> </div> @@ -517,7 +507,10 @@ function FeedDescription(props: { contract: Contract }) { ) } -function FeedCreateAnswer(props: { contract: Contract; answer: Answer }) { +function FeedCreateAnswer(props: { + contract: FullContract<any, FreeResponse> + answer: Answer +}) { const { answer } = props return ( @@ -677,7 +670,7 @@ function FeedBetGroup(props: { } function FeedAnswerGroup(props: { - contract: Contract + contract: FullContract<any, FreeResponse> answer: Answer items: ActivityItem[] }) { diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index f6cc58e1..2fe4717d 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -1,7 +1,6 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' -import { Contract } from '../lib/firebase/contracts' import { Col } from './layout/col' import { Title } from './title' import { User } from '../lib/firebase/users' @@ -10,12 +9,14 @@ import { Spacer } from './layout/spacer' import { ResolveConfirmationButton } from './confirmation-button' import { resolveMarket } from '../lib/firebase/api-call' import { ProbabilitySelector } from './probability-selector' +import { DPM_CREATOR_FEE } from '../../common/fees' import { getProbability } from '../../common/calculate' -import { CREATOR_FEE } from '../../common/fees' +import { Binary, CPMM, DPM, FullContract } from '../../common/contract' +import { formatMoney } from '../../common/util/format' export function ResolutionPanel(props: { creator: User - contract: Contract + contract: FullContract<DPM | CPMM, Binary> className?: string }) { useEffect(() => { @@ -25,11 +26,16 @@ export function ResolutionPanel(props: { const { contract, className } = props + const earnedFees = + contract.mechanism === 'dpm-2' + ? `${DPM_CREATOR_FEE * 100}% of trader profits` + : `${formatMoney(contract.collectedFees.creatorFee)} in fees` + const [outcome, setOutcome] = useState< 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined >() - const [prob, setProb] = useState(getProbability(contract.totalShares) * 100) + const [prob, setProb] = useState(getProbability(contract) * 100) const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState<string | undefined>(undefined) @@ -85,14 +91,14 @@ export function ResolutionPanel(props: { Winnings will be paid out to YES bettors. <br /> <br /> - You earn {CREATOR_FEE * 100}% of trader profits. + You will earn {earnedFees}. </> ) : outcome === 'NO' ? ( <> Winnings will be paid out to NO bettors. <br /> <br /> - You earn {CREATOR_FEE * 100}% of trader profits. + You will earn {earnedFees}. </> ) : outcome === 'CANCEL' ? ( <>The pool will be returned to traders with no fees.</> @@ -103,7 +109,7 @@ export function ResolutionPanel(props: { probabilityInt={Math.round(prob)} setProbabilityInt={setProb} /> - <div>You earn {CREATOR_FEE * 100}% of trader profits.</div> + You will earn {earnedFees}. </Col> ) : ( <>Resolving this market will immediately pay out traders.</> diff --git a/web/hooks/use-focus.ts b/web/hooks/use-focus.ts new file mode 100644 index 00000000..a71a0292 --- /dev/null +++ b/web/hooks/use-focus.ts @@ -0,0 +1,11 @@ +import { useRef } from 'react' + +// Focus helper from https://stackoverflow.com/a/54159564/1222351 +export function useFocus(): [React.RefObject<HTMLElement>, () => void] { + const htmlElRef = useRef<HTMLElement>(null) + const setFocus = () => { + htmlElRef.current && htmlElRef.current.focus() + } + + return [htmlElRef, setFocus] +} diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index e75a34fb..24133c65 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -17,9 +17,12 @@ import _ from 'lodash' import { app } from './init' import { getValues, listenForValue, listenForValues } from './utils' -import { Contract } from '../../../common/contract' -import { getProbability } from '../../../common/calculate' +import { Binary, Contract, FullContract } from '../../../common/contract' +import { getDpmProbability } from '../../../common/calculate-dpm' import { createRNG, shuffle } from '../../../common/util/random' +import { getCpmmProbability } from '../../../common/calculate-cpmm' +import { formatMoney } from '../../../common/util/format' +import { getCpmmLiquidity } from '../../../common/calculate-cpmm' export type { Contract } export function contractPath(contract: Contract) { @@ -37,15 +40,25 @@ export function contractMetrics(contract: Contract) { ? dayjs(resolutionTime).format('MMM D') : undefined - return { truePool, createdDate, resolvedDate } + const liquidityLabel = + contract.mechanism === 'dpm-2' + ? `${formatMoney(truePool)} pool` + : `${formatMoney( + contract.totalLiquidity ?? getCpmmLiquidity(pool, contract.p) + )} liquidity` + + return { truePool, liquidityLabel, createdDate, resolvedDate } } -export function getBinaryProbPercent(contract: Contract) { - const { totalShares, resolutionProbability } = contract +export function getBinaryProbPercent(contract: FullContract<any, Binary>) { + const { totalShares, pool, p, resolutionProbability, mechanism } = contract + + const prob = + resolutionProbability ?? mechanism === 'cpmm-1' + ? getCpmmProbability(pool, p) + : getDpmProbability(totalShares) - const prob = resolutionProbability ?? getProbability(totalShares) const probPercent = Math.round(prob * 100) + '%' - return probPercent } diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index ea985f81..90929ec9 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -175,7 +175,6 @@ function BetsSection(props: { bets: Bet[] }) { const { contract, user } = props - const isBinary = contract.outcomeType === 'BINARY' const bets = useBets(contract.id) ?? props.bets // Decending creation time. diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index 44a6119e..2eadf8dc 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -1,7 +1,7 @@ import { Bet } from '../../../../common/bet' -import { getProbability } from '../../../../common/calculate' +import { getDpmProbability } from '../../../../common/calculate-dpm' import { Comment } from '../../../../common/comment' -import { Contract } from '../../../../common/contract' +import { DPM, FullContract } from '../../../../common/contract' export type LiteMarket = { // Unique identifer for this market @@ -56,7 +56,7 @@ export function toLiteMarket({ isResolved, resolution, resolutionTime, -}: Contract): LiteMarket { +}: FullContract<DPM, any>): LiteMarket { return { id, creatorUsername, @@ -72,7 +72,7 @@ export function toLiteMarket({ tags, url: `https://manifold.markets/${creatorUsername}/${slug}`, pool: pool.YES + pool.NO, - probability: getProbability(totalShares), + probability: getDpmProbability(totalShares), volume7Days, volume24Hours, isResolved, diff --git a/web/pages/create.tsx b/web/pages/create.tsx index c4feb11c..d6600cd0 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -236,10 +236,9 @@ export function NewContract(props: { question: string; tag?: string }) { <div className="form-control mb-1 items-start"> <label className="label mb-1 gap-2"> - <span>Market ante</span> + <span>Market subsidy</span> <InfoTooltip - text={`Subsidize your market to encourage trading. Ante bets are set to match your initial probability. - You earn ${CREATOR_FEE * 100}% of trader profits.`} + text={`Provide liquidity to encourage traders to participate.`} /> </label> <AmountInput diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx index eceb692a..36d210ef 100644 --- a/web/pages/make-predictions.tsx +++ b/web/pages/make-predictions.tsx @@ -3,11 +3,12 @@ import dayjs from 'dayjs' import Link from 'next/link' import { useState } from 'react' import Textarea from 'react-expanding-textarea' + import { getProbability } from '../../common/calculate' +import { Binary, CPMM, DPM, FullContract } from '../../common/contract' import { parseWordsAsTags } from '../../common/util/parse' import { AmountInput } from '../components/amount-input' import { InfoTooltip } from '../components/info-tooltip' - import { Col } from '../components/layout/col' import { Row } from '../components/layout/row' import { Spacer } from '../components/layout/spacer' @@ -16,7 +17,7 @@ import { Page } from '../components/page' import { Title } from '../components/title' import { useUser } from '../hooks/use-user' import { createContract } from '../lib/firebase/api-call' -import { Contract, contractPath } from '../lib/firebase/contracts' +import { contractPath } from '../lib/firebase/contracts' type Prediction = { question: string @@ -25,8 +26,8 @@ type Prediction = { createdUrl?: string } -function toPrediction(contract: Contract): Prediction { - const startProb = getProbability(contract.totalShares) +function toPrediction(contract: FullContract<DPM | CPMM, Binary>): Prediction { + const startProb = getProbability(contract) return { question: contract.question, description: contract.description, @@ -101,7 +102,9 @@ export default function MakePredictions() { const [description, setDescription] = useState('') const [tags, setTags] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) - const [createdContracts, setCreatedContracts] = useState<Contract[]>([]) + const [createdContracts, setCreatedContracts] = useState< + FullContract<DPM | CPMM, Binary>[] + >([]) const [ante, setAnte] = useState<number | undefined>(100) const [anteError, setAnteError] = useState<string | undefined>()