Merge branch 'main' into free-response

This commit is contained in:
James Grugett 2022-02-14 14:23:19 -06:00
commit 26717295b8
11 changed files with 176 additions and 196 deletions

View File

@ -3,7 +3,7 @@ import { getProbability } from './calculate'
import { Contract } from './contract' import { Contract } from './contract'
import { User } from './user' import { User } from './user'
export const PHANTOM_ANTE = 100 export const PHANTOM_ANTE = 0.001
export const MINIMUM_ANTE = 10 export const MINIMUM_ANTE = 10
export const calcStartPool = (initialProbInt: number, ante = 0) => { export const calcStartPool = (initialProbInt: number, ante = 0) => {

View File

@ -116,7 +116,9 @@ export function calculateShareValue(contract: Contract, bet: Bet) {
} }
export function calculateSaleAmount(contract: Contract, bet: Bet) { export function calculateSaleAmount(contract: Contract, bet: Bet) {
return (1 - FEES) * calculateShareValue(contract, bet) const { amount } = bet
const winnings = calculateShareValue(contract, bet)
return deductFees(amount, winnings)
} }
export function calculatePayout( export function calculatePayout(
@ -145,19 +147,16 @@ export function calculateStandardPayout(
const { amount, outcome: betOutcome, shares } = bet const { amount, outcome: betOutcome, shares } = bet
if (betOutcome !== outcome) return 0 if (betOutcome !== outcome) return 0
const { totalShares, totalBets, phantomShares } = contract const { totalShares, phantomShares } = contract
if (!totalShares[outcome]) return 0 if (!totalShares[outcome]) return 0
const truePool = _.sum(Object.values(totalShares)) const pool = _.sum(Object.values(totalShares))
if (totalBets[outcome] >= truePool) const total = totalShares[outcome] - phantomShares[outcome]
return (amount / totalBets[outcome]) * truePool
const total = const winnings = (shares / total) * pool
totalShares[outcome] - phantomShares[outcome] - totalBets[outcome] // profit can be negative if using phantom shares
const winningsPool = truePool - totalBets[outcome] return amount + (1 - FEES) * Math.max(0, winnings - amount)
return amount + (1 - FEES) * ((shares - amount) / total) * winningsPool
} }
export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
@ -191,35 +190,18 @@ function calculateMktPayout(contract: Contract, bet: Bet) {
? contract.resolutionProbability ? contract.resolutionProbability
: getProbability(contract.totalShares) : getProbability(contract.totalShares)
const weightedTotal = const pool = contract.pool.YES + contract.pool.NO
p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO
const truePool = contract.pool.YES + contract.pool.NO
const betP = bet.outcome === 'YES' ? p : 1 - p
if (weightedTotal >= truePool) {
return ((betP * bet.amount) / weightedTotal) * truePool
}
const winningsPool = truePool - weightedTotal
const weightedShareTotal = const weightedShareTotal =
p * p * (contract.totalShares.YES - contract.phantomShares.YES) +
(contract.totalShares.YES - (1 - p) * (contract.totalShares.NO - contract.phantomShares.NO)
contract.phantomShares.YES -
contract.totalBets.YES) +
(1 - p) *
(contract.totalShares.NO -
contract.phantomShares.NO -
contract.totalBets.NO)
return ( const { outcome, amount, shares } = bet
betP * bet.amount +
(1 - FEES) * const betP = outcome === 'YES' ? p : 1 - p
((betP * (bet.shares - bet.amount)) / weightedShareTotal) * const winnings = ((betP * shares) / weightedShareTotal) * pool
winningsPool
) return deductFees(amount, winnings)
} }
export function resolvedPayout(contract: Contract, bet: Bet) { export function resolvedPayout(contract: Contract, bet: Bet) {
@ -236,3 +218,9 @@ export function currentValue(contract: Contract, bet: Bet) {
return prob * yesPayout + (1 - prob) * noPayout return prob * yesPayout + (1 - prob) * noPayout
} }
export const deductFees = (betAmount: number, winnings: number) => {
return winnings > betAmount
? betAmount + (1 - FEES) * (winnings - betAmount)
: winnings
}

View File

@ -1,5 +1,7 @@
import * as _ from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getProbability } from './calculate' import { deductFees, getProbability } from './calculate'
import { Contract, outcome } from './contract' import { Contract, outcome } from './contract'
import { CREATOR_FEE, FEES } from './fees' import { CREATOR_FEE, FEES } from './fees'
@ -8,7 +10,7 @@ export const getCancelPayouts = (contract: Contract, bets: Bet[]) => {
const poolTotal = pool.YES + pool.NO const poolTotal = pool.YES + pool.NO
console.log('resolved N/A, pool M$', poolTotal) console.log('resolved N/A, pool M$', poolTotal)
const betSum = sumBy(bets, (b) => b.amount) const betSum = _.sumBy(bets, (b) => b.amount)
return bets.map((bet) => ({ return bets.map((bet) => ({
userId: bet.userId, userId: bet.userId,
@ -21,42 +23,38 @@ export const getStandardPayouts = (
contract: Contract, contract: Contract,
bets: Bet[] bets: Bet[]
) => { ) => {
const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === 'YES') const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
const winningBets = outcome === 'YES' ? yesBets : noBets const winningBets = outcome === 'YES' ? yesBets : noBets
const betSum = sumBy(winningBets, (b) => b.amount) const pool = contract.pool.YES + contract.pool.NO
const totalShares = _.sumBy(winningBets, (b) => b.shares)
const poolTotal = contract.pool.YES + contract.pool.NO const payouts = winningBets.map(({ userId, amount, shares }) => {
const winnings = (shares / totalShares) * pool
const profit = winnings - amount
if (betSum >= poolTotal) return getCancelPayouts(contract, winningBets) // profit can be negative if using phantom shares
const payout = amount + (1 - FEES) * Math.max(0, profit)
return { userId, profit, payout }
})
const shareDifferenceSum = sumBy(winningBets, (b) => b.shares - b.amount) const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
const creatorPayout = CREATOR_FEE * profits
const winningsPool = poolTotal - betSum
const winnerPayouts = winningBets.map((bet) => ({
userId: bet.userId,
payout:
bet.amount +
(1 - FEES) *
((bet.shares - bet.amount) / shareDifferenceSum) *
winningsPool,
}))
const creatorPayout = CREATOR_FEE * winningsPool
console.log( console.log(
'resolved', 'resolved',
outcome, outcome,
'pool: M$', 'pool',
poolTotal, pool,
'creator fee: M$', 'profits',
profits,
'creator fee',
creatorPayout creatorPayout
) )
return winnerPayouts.concat([ return payouts
{ userId: contract.creatorId, payout: creatorPayout }, .map(({ userId, payout }) => ({ userId, payout }))
]) // add creator fee .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
} }
export const getMktPayouts = ( export const getMktPayouts = (
@ -69,56 +67,37 @@ export const getMktPayouts = (
? getProbability(contract.totalShares) ? getProbability(contract.totalShares)
: resolutionProbability : resolutionProbability
const poolTotal = contract.pool.YES + contract.pool.NO const weightedShareTotal = _.sumBy(bets, (b) =>
console.log('Resolved MKT at p=', p, 'pool: $M', poolTotal) b.outcome === 'YES' ? p * b.shares : (1 - p) * b.shares
)
const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === 'YES') const pool = contract.pool.YES + contract.pool.NO
const weightedBetTotal = const payouts = bets.map(({ userId, outcome, amount, shares }) => {
p * sumBy(yesBets, (b) => b.amount) + const betP = outcome === 'YES' ? p : 1 - p
(1 - p) * sumBy(noBets, (b) => b.amount) const winnings = ((betP * shares) / weightedShareTotal) * pool
const profit = winnings - amount
const payout = deductFees(amount, winnings)
return { userId, profit, payout }
})
if (weightedBetTotal >= poolTotal) { const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
return bets.map((bet) => ({ const creatorPayout = CREATOR_FEE * profits
userId: bet.userId,
payout:
(((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) /
weightedBetTotal) *
poolTotal,
}))
}
const winningsPool = poolTotal - weightedBetTotal console.log(
'resolved MKT',
p,
'pool',
pool,
'profits',
profits,
'creator fee',
creatorPayout
)
const weightedShareTotal = return payouts
p * sumBy(yesBets, (b) => b.shares - b.amount) + .map(({ userId, payout }) => ({ userId, payout }))
(1 - p) * sumBy(noBets, (b) => b.shares - b.amount) .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
const yesPayouts = yesBets.map((bet) => ({
userId: bet.userId,
payout:
p * bet.amount +
(1 - FEES) *
((p * (bet.shares - bet.amount)) / weightedShareTotal) *
winningsPool,
}))
const noPayouts = noBets.map((bet) => ({
userId: bet.userId,
payout:
(1 - p) * bet.amount +
(1 - FEES) *
(((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) *
winningsPool,
}))
const creatorPayout = CREATOR_FEE * winningsPool
return [
...yesPayouts,
...noPayouts,
{ userId: contract.creatorId, payout: creatorPayout },
]
} }
export const getPayouts = ( export const getPayouts = (
@ -137,20 +116,3 @@ export const getPayouts = (
return getCancelPayouts(contract, bets) return getCancelPayouts(contract, bets)
} }
} }
const partition = <T>(array: T[], f: (t: T) => boolean) => {
const yes = []
const no = []
for (let t of array) {
if (f(t)) yes.push(t)
else no.push(t)
}
return [yes, no] as [T[], T[]]
}
const sumBy = <T>(array: T[], f: (t: T) => number) => {
const values = array.map(f)
return values.reduce((prev, cur) => prev + cur, 0)
}

View File

@ -1,5 +1,5 @@
import { Bet } from './bet' import { Bet } from './bet'
import { calculateShareValue, getProbability } from './calculate' import { calculateShareValue, deductFees, getProbability } from './calculate'
import { Contract } from './contract' import { Contract } from './contract'
import { CREATOR_FEE, FEES } from './fees' import { CREATOR_FEE, FEES } from './fees'
import { User } from './user' import { User } from './user'
@ -36,8 +36,9 @@ export const getSellBetInfo = (
const probBefore = getProbability(contract.totalShares) const probBefore = getProbability(contract.totalShares)
const probAfter = getProbability(newTotalShares) const probAfter = getProbability(newTotalShares)
const creatorFee = CREATOR_FEE * adjShareValue const profit = adjShareValue - amount
const saleAmount = (1 - FEES) * adjShareValue const creatorFee = CREATOR_FEE * Math.max(0, profit)
const saleAmount = deductFees(amount, adjShareValue)
console.log( console.log(
'SELL M$', 'SELL M$',

View File

@ -30,7 +30,9 @@ export const resolveMarket = functions
if ( if (
probabilityInt !== undefined && probabilityInt !== undefined &&
(probabilityInt < 1 || probabilityInt > 99 || !isFinite(probabilityInt)) (probabilityInt < 0 ||
probabilityInt > 100 ||
!isFinite(probabilityInt))
) )
return { status: 'error', message: 'Invalid probability' } return { status: 'error', message: 'Invalid probability' }

View File

@ -23,13 +23,15 @@ import {
import { Row } from './layout/row' import { Row } from './layout/row'
import { UserLink } from './user-page' import { UserLink } from './user-page'
import { import {
calculateCancelPayout,
calculatePayout, calculatePayout,
calculateSaleAmount, calculateSaleAmount,
getProbability,
resolvedPayout, resolvedPayout,
} from '../../common/calculate' } from '../../common/calculate'
import { sellBet } from '../lib/firebase/api-call' import { sellBet } from '../lib/firebase/api-call'
import { ConfirmationButton } from './confirmation-button' import { ConfirmationButton } from './confirmation-button'
import { OutcomeLabel, YesLabel, NoLabel, MarketLabel } from './outcome-label' import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
type BetSort = 'newest' | 'profit' type BetSort = 'newest' | 'profit'
@ -119,16 +121,32 @@ export function BetsList(props: { user: User }) {
(c) => contractsCurrentValue[c.id] (c) => contractsCurrentValue[c.id]
) )
const totalPortfolio = currentBetsValue + user.balance
const pnl = totalPortfolio - user.totalDeposits
const totalReturn =
(pnl > 0 ? '+' : '') + ((pnl / user.totalDeposits) * 100).toFixed() + '%'
return ( return (
<Col className="mt-6 gap-6"> <Col className="mt-6 gap-6">
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0"> <Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
<Row className="gap-8"> <Row className="gap-8">
<Col>
<div className="text-sm text-gray-500">Total portfolio</div>
<div>{formatMoney(totalPortfolio)}</div>
</Col>
<Col>
<div className="text-sm text-gray-500">Total profits & losses</div>
<div>
{formatMoney(pnl)} ({totalReturn})
</div>
</Col>
<Col> <Col>
<div className="text-sm text-gray-500">Currently invested</div> <div className="text-sm text-gray-500">Currently invested</div>
<div>{formatMoney(currentInvestment)}</div> <div>{formatMoney(currentInvestment)}</div>
</Col> </Col>
<Col> <Col>
<div className="text-sm text-gray-500">Current value</div> <div className="text-sm text-gray-500">Current market value</div>
<div>{formatMoney(currentBetsValue)}</div> <div>{formatMoney(currentBetsValue)}</div>
</Col> </Col>
</Row> </Row>
@ -231,6 +249,7 @@ export function MyBetsSummary(props: {
}) { }) {
const { bets, contract, showMKT, className } = props const { bets, contract, showMKT, className } = props
const { resolution } = contract const { resolution } = contract
calculateCancelPayout
const excludeSales = bets.filter((b) => !b.isSold && !b.sale) const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount) const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount)
@ -284,7 +303,10 @@ export function MyBetsSummary(props: {
{showMKT && ( {showMKT && (
<Col> <Col>
<div className="whitespace-nowrap text-sm text-gray-500"> <div className="whitespace-nowrap text-sm text-gray-500">
Payout if <MarketLabel /> Payout at{' '}
<span className="text-blue-400">
{formatPercent(getProbability(contract.totalShares))}
</span>
</div> </div>
<div className="whitespace-nowrap"> <div className="whitespace-nowrap">
{formatMoney(marketWinnings)} {formatMoney(marketWinnings)}

View File

@ -25,6 +25,7 @@ export function EditFoldButton(props: { fold: Fold; className?: string }) {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags) const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
const saveDisabled = const saveDisabled =
name === fold.name && name === fold.name &&
@ -38,6 +39,7 @@ export function EditFoldButton(props: { fold: Fold; className?: string }) {
name, name,
about, about,
tags, tags,
lowercaseTags,
}) })
setIsSubmitting(false) setIsSubmitting(false)

View File

@ -21,10 +21,6 @@ export function CancelLabel() {
return <span className="text-yellow-400">N/A</span> return <span className="text-yellow-400">N/A</span>
} }
export function MarketLabel() {
return <span className="text-blue-400">MKT</span>
}
export function ProbLabel() { export function ProbLabel() {
return <span className="text-blue-400">PROB</span> return <span className="text-blue-400">PROB</span>
} }

View File

@ -64,9 +64,9 @@ export function ResolutionPanel(props: {
return ( return (
<Col className={clsx('rounded-md bg-white px-8 py-6', className)}> <Col className={clsx('rounded-md bg-white px-8 py-6', className)}>
<Title className="mt-0" text="Your market" /> <Title className="mt-0" text="Resolve market" />
<div className="pt-2 pb-1 text-sm text-gray-500">Resolve outcome</div> <div className="pt-2 pb-1 text-sm text-gray-500">Outcome</div>
<YesNoCancelSelector <YesNoCancelSelector
className="mx-auto my-2" className="mx-auto my-2"
@ -80,20 +80,30 @@ export function ResolutionPanel(props: {
<div> <div>
{outcome === 'YES' ? ( {outcome === 'YES' ? (
<> <>
Winnings will be paid out to YES bettors. You earn{' '} Winnings will be paid out to YES bettors.
{CREATOR_FEE * 100}% of trader profits. <br />
<br />
You earn {CREATOR_FEE * 100}% of trader profits.
</> </>
) : outcome === 'NO' ? ( ) : outcome === 'NO' ? (
<> <>
Winnings will be paid out to NO bettors. You earn{' '} Winnings will be paid out to NO bettors.
{CREATOR_FEE * 100}% of trader profits. <br />
<br />
You earn {CREATOR_FEE * 100}% of trader profits.
</> </>
) : outcome === 'CANCEL' ? ( ) : outcome === 'CANCEL' ? (
<>The pool will be returned to traders with no fees.</> <>The pool will be returned to traders with no fees.</>
) : outcome === 'MKT' ? ( ) : outcome === 'MKT' ? (
<> <>
Traders will be paid out at the probability you specify. You earn{' '} Traders will be paid out at the probability you specify:
{CREATOR_FEE * 100}% of trader profits. <Spacer h={2} />
<ProbabilitySelector
probabilityInt={Math.round(prob)}
setProbabilityInt={setProb}
/>
<Spacer h={2} />
You earn {CREATOR_FEE * 100}% of trader profits.
</> </>
) : ( ) : (
<>Resolving this market will immediately pay out traders.</> <>Resolving this market will immediately pay out traders.</>
@ -123,20 +133,7 @@ export function ResolutionPanel(props: {
}} }}
onSubmit={resolve} onSubmit={resolve}
> >
{outcome === 'MKT' ? ( <p>Are you sure you want to resolve this market?</p>
<>
<p className="mb-4">
What probability would you like to resolve the market to?
</p>
<ProbabilitySelector
probabilityInt={Math.round(prob)}
setProbabilityInt={setProb}
/>
</>
) : (
<p>Are you sure you want to resolve this market?</p>
)}
</ConfirmationButton> </ConfirmationButton>
</Col> </Col>
) )

View File

@ -48,47 +48,44 @@ export function YesNoCancelSelector(props: {
className?: string className?: string
btnClassName?: string btnClassName?: string
}) { }) {
const { selected, onSelect, className } = props const { selected, onSelect } = props
const btnClassName = clsx('px-6 flex-1', props.btnClassName) const btnClassName = clsx('px-6 flex-1', props.btnClassName)
return ( return (
<Col> <Col className="gap-2">
<Row className={clsx('w-full space-x-3', className)}> {/* Should ideally use a radio group instead of buttons */}
<Button <Button
color={selected === 'YES' ? 'green' : 'gray'} color={selected === 'YES' ? 'green' : 'gray'}
onClick={() => onSelect('YES')} onClick={() => onSelect('YES')}
className={btnClassName} className={btnClassName}
> >
YES YES
</Button> </Button>
<Button <Button
color={selected === 'NO' ? 'red' : 'gray'} color={selected === 'NO' ? 'red' : 'gray'}
onClick={() => onSelect('NO')} onClick={() => onSelect('NO')}
className={btnClassName} className={btnClassName}
> >
NO NO
</Button> </Button>
</Row>
<Row className={clsx('w-full space-x-3', className)}> <Button
<Button color={selected === 'MKT' ? 'blue' : 'gray'}
color={selected === 'MKT' ? 'blue' : 'gray'} onClick={() => onSelect('MKT')}
onClick={() => onSelect('MKT')} className={clsx(btnClassName, 'btn-sm')}
className={clsx(btnClassName, 'btn-sm')} >
> PROB
PROB </Button>
</Button>
<Button <Button
color={selected === 'CANCEL' ? 'yellow' : 'gray'} color={selected === 'CANCEL' ? 'yellow' : 'gray'}
onClick={() => onSelect('CANCEL')} onClick={() => onSelect('CANCEL')}
className={clsx(btnClassName, 'btn-sm')} className={clsx(btnClassName, 'btn-sm')}
> >
N/A N/A
</Button> </Button>
</Row>
</Col> </Col>
) )
} }

View File

@ -1,14 +1,14 @@
import { Page } from '../components/page' import { Page } from '../components/page'
import { Grid } from 'gridjs-react' import { Grid, _ as r } from 'gridjs-react'
import 'gridjs/dist/theme/mermaid.css' import 'gridjs/dist/theme/mermaid.css'
import { html } from 'gridjs' import { html } from 'gridjs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { usePrivateUsers, useUsers } from '../hooks/use-users' import { usePrivateUsers, useUsers } from '../hooks/use-users'
import { useUser } from '../hooks/use-user'
import Custom404 from './404' import Custom404 from './404'
import { useContracts } from '../hooks/use-contracts' import { useContracts } from '../hooks/use-contracts'
import _ from 'lodash' import _ from 'lodash'
import { useAdmin } from '../hooks/use-admin' import { useAdmin } from '../hooks/use-admin'
import { contractPath } from '../lib/firebase/contracts'
function avatarHtml(avatarUrl: string) { function avatarHtml(avatarUrl: string) {
return `<img return `<img
@ -111,6 +111,20 @@ function ContractsTable() {
let contracts = useContracts() ?? [] let contracts = useContracts() ?? []
// Sort users by createdTime descending, by default // Sort users by createdTime descending, by default
contracts.sort((a, b) => b.createdTime - a.createdTime) contracts.sort((a, b) => b.createdTime - a.createdTime)
// Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
contracts.map((contract) => {
// @ts-ignore
contract.questionLink = r(
<div className="w-60">
<a
className="hover:underline hover:decoration-indigo-400 hover:decoration-2"
href={contractPath(contract)}
>
{contract.question}
</a>
</div>
)
})
return ( return (
<Grid <Grid
@ -126,9 +140,8 @@ function ContractsTable() {
href="/${cell}">@${cell}</a>`), href="/${cell}">@${cell}</a>`),
}, },
{ {
id: 'question', id: 'questionLink',
name: 'Question', name: 'Question',
formatter: (cell) => html(`<div class="w-60">${cell}</div>`),
}, },
{ {
id: 'volume24Hours', id: 'volume24Hours',