Merge branch 'main' into automated-market-resolution

This commit is contained in:
Milli 2022-05-26 13:03:58 +02:00
commit 5f8031c155
13 changed files with 226 additions and 156 deletions

View File

@ -63,10 +63,8 @@ export function getCpmmLiquidityFee(
bet: number, bet: number,
outcome: string outcome: string
) { ) {
const probBefore = getCpmmProbability(contract.pool, contract.p) const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
const probAfter = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet) const betP = outcome === 'YES' ? 1 - prob : prob
const probMid = Math.sqrt(probBefore * probAfter)
const betP = outcome === 'YES' ? 1 - probMid : probMid
const liquidityFee = LIQUIDITY_FEE * betP * bet const liquidityFee = LIQUIDITY_FEE * betP * bet
const platformFee = PLATFORM_FEE * betP * bet const platformFee = PLATFORM_FEE * betP * bet

View File

@ -24,8 +24,9 @@ Adapted from https://firebase.google.com/docs/functions/get-started
0. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev 0. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev
1. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI 1. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
2. `$ brew install java` to install java if you don't already have it 2. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
1. `$ echo 'export PATH="/usr/local/opt/openjdk/bin:$PATH"' >> ~/.zshrc` to add java to your path 1. `$ brew install java`
2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk`
3. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud 3. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
4. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options) 4. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
5. `$ mkdir firestore_export` to create a folder to store the exported database 5. `$ mkdir firestore_export` to create a folder to store the exported database

View File

@ -14,7 +14,7 @@ import {
formatWithCommas, formatWithCommas,
} from 'common/util/format' } from 'common/util/format'
import { Title } from './title' import { Title } from './title'
import { firebaseLogin, User } from 'web/lib/firebase/users' import { User } from 'web/lib/firebase/users'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { APIError, placeBet } from 'web/lib/firebase/api-call' import { APIError, placeBet } from 'web/lib/firebase/api-call'
import { sellShares } from 'web/lib/firebase/fn-call' import { sellShares } from 'web/lib/firebase/fn-call'
@ -36,6 +36,7 @@ import {
} from 'common/calculate-cpmm' } from 'common/calculate-cpmm'
import { SellRow } from './sell-row' import { SellRow } from './sell-row'
import { useSaveShares } from './use-save-shares' import { useSaveShares } from './use-save-shares'
import { SignUpPrompt } from './sign-up-prompt'
export function BetPanel(props: { export function BetPanel(props: {
contract: FullContract<DPM | CPMM, Binary> contract: FullContract<DPM | CPMM, Binary>
@ -70,14 +71,7 @@ export function BetPanel(props: {
<BuyPanel contract={contract} user={user} /> <BuyPanel contract={contract} user={user} />
{user === null && ( <SignUpPrompt />
<button
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign up to bet!
</button>
)}
</Col> </Col>
</Col> </Col>
) )
@ -183,14 +177,7 @@ export function BetPanelSwitcher(props: {
/> />
)} )}
{user === null && ( <SignUpPrompt />
<button
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign up to bet!
</button>
)}
</Col> </Col>
</Col> </Col>
) )

View File

@ -6,7 +6,6 @@ import {
Contract, Contract,
contractPath, contractPath,
getBinaryProbPercent, getBinaryProbPercent,
listenForContract,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { import {
@ -26,8 +25,7 @@ import {
import { getOutcomeProbability, getTopAnswer } from 'common/calculate' import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
import { AvatarDetails, MiscDetails } from './contract-details' import { AvatarDetails, MiscDetails } from './contract-details'
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm' import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
import { useEffect, useState } from 'react' import { QuickBet, ProbBar, getColor } from './quick-bet'
import { QuickBet, QuickOutcomeView, ProbBar, getColor } from './quick-bet'
import { useContractWithPreload } from 'web/hooks/use-contract' import { useContractWithPreload } from 'web/hooks/use-contract'
export function ContractCard(props: { export function ContractCard(props: {
@ -39,6 +37,7 @@ export function ContractCard(props: {
const { showHotVolume, showCloseTime, className } = props const { showHotVolume, showCloseTime, className } = props
const contract = useContractWithPreload(props.contract) ?? props.contract const contract = useContractWithPreload(props.contract) ?? props.contract
const { question, outcomeType } = contract const { question, outcomeType } = contract
const { resolution } = contract
const marketClosed = (contract.closeTime || Infinity) < Date.now() const marketClosed = (contract.closeTime || Infinity) < Date.now()
const showQuickBet = !( const showQuickBet = !(
@ -54,7 +53,7 @@ export function ContractCard(props: {
className className
)} )}
> >
<Row className={clsx(showQuickBet ? 'divide-x' : '')}> <Row>
<Col className="relative flex-1 gap-3 pr-1"> <Col className="relative flex-1 gap-3 pr-1">
<div <div
className={clsx( className={clsx(
@ -69,18 +68,25 @@ export function ContractCard(props: {
</div> </div>
<AvatarDetails contract={contract} /> <AvatarDetails contract={contract} />
<p <p
className="break-words font-medium text-indigo-700 peer-hover:underline peer-hover:decoration-indigo-400 peer-hover:decoration-2" className="break-words font-semibold text-indigo-700 peer-hover:underline peer-hover:decoration-indigo-400 peer-hover:decoration-2"
style={{ /* For iOS safari */ wordBreak: 'break-word' }} style={{ /* For iOS safari */ wordBreak: 'break-word' }}
> >
{question} {question}
</p> </p>
{outcomeType === 'FREE_RESPONSE' && ( {outcomeType === 'FREE_RESPONSE' &&
(resolution ? (
<FreeResponseOutcomeLabel
contract={contract as FreeResponseContract}
resolution={resolution}
truncate={'long'}
/>
) : (
<FreeResponseTopAnswer <FreeResponseTopAnswer
contract={contract as FullContract<DPM, FreeResponse>} contract={contract as FullContract<DPM, FreeResponse>}
truncate="long" truncate="long"
/> />
)} ))}
<MiscDetails <MiscDetails
contract={contract} contract={contract}
@ -92,11 +98,31 @@ export function ContractCard(props: {
<QuickBet contract={contract} /> <QuickBet contract={contract} />
) : ( ) : (
<Col className="m-auto pl-2"> <Col className="m-auto pl-2">
<QuickOutcomeView contract={contract} /> {outcomeType === 'BINARY' && (
<BinaryResolutionOrChance
className="items-center"
contract={contract}
/>
)}
{outcomeType === 'NUMERIC' && (
<NumericResolutionOrExpectation
className="items-center"
contract={contract as NumericContract}
/>
)}
{outcomeType === 'FREE_RESPONSE' && (
<FreeResponseResolutionOrChance
className="self-end text-gray-600"
contract={contract as FullContract<DPM, FreeResponse>}
truncate="long"
/>
)}
<ProbBar contract={contract} />
</Col> </Col>
)} )}
</Row> </Row>
<ProbBar contract={contract} />
</Col> </Col>
</div> </div>
) )
@ -106,9 +132,8 @@ export function BinaryResolutionOrChance(props: {
contract: FullContract<DPM | CPMM, Binary> contract: FullContract<DPM | CPMM, Binary>
large?: boolean large?: boolean
className?: string className?: string
hideText?: boolean
}) { }) {
const { contract, large, className, hideText } = props const { contract, large, className } = props
const { resolution } = contract const { resolution } = contract
const textColor = `text-${getColor(contract)}` const textColor = `text-${getColor(contract)}`
@ -129,11 +154,9 @@ export function BinaryResolutionOrChance(props: {
) : ( ) : (
<> <>
<div className={textColor}>{getBinaryProbPercent(contract)}</div> <div className={textColor}>{getBinaryProbPercent(contract)}</div>
{!hideText && (
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}> <div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
chance chance
</div> </div>
)}
</> </>
)} )}
</Col> </Col>
@ -162,9 +185,8 @@ export function FreeResponseResolutionOrChance(props: {
contract: FreeResponseContract contract: FreeResponseContract
truncate: 'short' | 'long' | 'none' truncate: 'short' | 'long' | 'none'
className?: string className?: string
hideText?: boolean
}) { }) {
const { contract, truncate, className, hideText } = props const { contract, truncate, className } = props
const { resolution } = contract const { resolution } = contract
const topAnswer = getTopAnswer(contract) const topAnswer = getTopAnswer(contract)
@ -174,13 +196,17 @@ export function FreeResponseResolutionOrChance(props: {
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}> <Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
{resolution ? ( {resolution ? (
<> <>
<div className={clsx('text-base text-gray-500')}>Resolved</div> <div className={clsx('text-base text-gray-500 sm:hidden')}>
Resolved
</div>
{(resolution === 'CANCEL' || resolution === 'MKT') && (
<FreeResponseOutcomeLabel <FreeResponseOutcomeLabel
contract={contract} contract={contract}
resolution={resolution} resolution={resolution}
truncate={truncate} truncate={truncate}
answerClassName="text-xl" answerClassName="text-3xl uppercase text-blue-500"
/> />
)}
</> </>
) : ( ) : (
topAnswer && ( topAnswer && (
@ -189,7 +215,7 @@ export function FreeResponseResolutionOrChance(props: {
<div> <div>
{formatPercent(getOutcomeProbability(contract, topAnswer.id))} {formatPercent(getOutcomeProbability(contract, topAnswer.id))}
</div> </div>
{!hideText && <div className="text-base">chance</div>} <div className="text-base">chance</div>
</Col> </Col>
</Row> </Row>
) )
@ -201,9 +227,8 @@ export function FreeResponseResolutionOrChance(props: {
export function NumericResolutionOrExpectation(props: { export function NumericResolutionOrExpectation(props: {
contract: NumericContract contract: NumericContract
className?: string className?: string
hideText?: boolean
}) { }) {
const { contract, className, hideText } = props const { contract, className } = props
const { resolution } = contract const { resolution } = contract
const textColor = `text-${getColor(contract)}` const textColor = `text-${getColor(contract)}`
@ -222,9 +247,7 @@ export function NumericResolutionOrExpectation(props: {
<div className={clsx('text-3xl', textColor)}> <div className={clsx('text-3xl', textColor)}>
{formatLargeNumber(getExpectedValue(contract))} {formatLargeNumber(getExpectedValue(contract))}
</div> </div>
{!hideText && (
<div className={clsx('text-base', textColor)}>expected</div> <div className={clsx('text-base', textColor)}>expected</div>
)}
</> </>
)} )}
</Col> </Col>

View File

@ -14,6 +14,7 @@ import { UserLink } from '../user-page'
import { import {
Contract, Contract,
contractMetrics, contractMetrics,
contractPool,
updateContract, updateContract,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
import { Col } from '../layout/col' import { Col } from '../layout/col'
@ -43,10 +44,6 @@ export function MiscDetails(props: {
return ( return (
<Row className="items-center gap-3 text-sm text-gray-400"> <Row className="items-center gap-3 text-sm text-gray-400">
{categories.length > 0 && (
<TagsList className="text-gray-400" tags={categories} noLabel />
)}
{showHotVolume ? ( {showHotVolume ? (
<Row className="gap-0.5"> <Row className="gap-0.5">
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)} <TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
@ -58,10 +55,14 @@ export function MiscDetails(props: {
{fromNow(closeTime || 0)} {fromNow(closeTime || 0)}
</Row> </Row>
) : volume > 0 ? ( ) : volume > 0 ? (
<Row>{volumeLabel}</Row> <Row>{contractPool(contract)} pool</Row>
) : ( ) : (
<NewContractBadge /> <NewContractBadge />
)} )}
{categories.length > 0 && (
<TagsList className="text-gray-400" tags={categories} noLabel />
)}
</Row> </Row>
) )
} }
@ -71,7 +72,7 @@ export function AvatarDetails(props: { contract: Contract }) {
const { creatorName, creatorUsername } = contract const { creatorName, creatorUsername } = contract
return ( return (
<Row className="items-center gap-2 text-sm text-gray-500"> <Row className="items-center gap-2 text-sm text-gray-400">
<Avatar <Avatar
username={creatorUsername} username={creatorUsername}
avatarUrl={contract.creatorAvatarUrl} avatarUrl={contract.creatorAvatarUrl}

View File

@ -7,7 +7,12 @@ import { Bet } from 'common/bet'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts' import {
contractMetrics,
contractPath,
contractPool,
getBinaryProbPercent,
} from 'web/lib/firebase/contracts'
import { AddLiquidityPanel } from '../add-liquidity-panel' import { AddLiquidityPanel } from '../add-liquidity-panel'
import { CopyLinkButton } from '../copy-link-button' import { CopyLinkButton } from '../copy-link-button'
import { Col } from '../layout/col' import { Col } from '../layout/col'
@ -98,19 +103,10 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
<td>{tradersCount}</td> <td>{tradersCount}</td>
</tr> </tr>
{contract.mechanism === 'cpmm-1' && (
<tr>
<td>Liquidity</td>
<td>{formatMoney(contract.totalLiquidity)}</td>
</tr>
)}
{contract.mechanism === 'dpm-2' && (
<tr> <tr>
<td>Pool</td> <td>Pool</td>
<td>{formatMoney(sum(Object.values(contract.pool)))}</td> <td>{contractPool(contract)}</td>
</tr> </tr>
)}
</tbody> </tbody>
</table> </table>

View File

@ -1,5 +1,9 @@
import clsx from 'clsx' import clsx from 'clsx'
import { getOutcomeProbability, getTopAnswer } from 'common/calculate' import {
getOutcomeProbability,
getOutcomeProbabilityAfterBet,
getTopAnswer,
} from 'common/calculate'
import { getExpectedValue } from 'common/calculate-dpm' import { getExpectedValue } from 'common/calculate-dpm'
import { import {
Contract, Contract,
@ -8,55 +12,81 @@ import {
DPM, DPM,
Binary, Binary,
NumericContract, NumericContract,
FreeResponse, FreeResponseContract,
} from 'common/contract' } from 'common/contract'
import { formatMoney } from 'common/util/format' import {
formatLargeNumber,
formatMoney,
formatPercent,
} from 'common/util/format'
import { useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { useUserContractBets } from 'web/hooks/use-user-bets' import { useUserContractBets } from 'web/hooks/use-user-bets'
import { placeBet } from 'web/lib/firebase/api-call' import { placeBet } from 'web/lib/firebase/api-call'
import { getBinaryProb } from 'web/lib/firebase/contracts' import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts'
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon' import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { OUTCOME_TO_COLOR } from '../outcome-label' import { OUTCOME_TO_COLOR } from '../outcome-label'
import { useSaveShares } from '../use-save-shares' import { useSaveShares } from '../use-save-shares'
import {
BinaryResolutionOrChance, const BET_SIZE = 10
NumericResolutionOrExpectation,
FreeResponseResolutionOrChance,
} from './contract-card'
export function QuickBet(props: { contract: Contract }) { export function QuickBet(props: { contract: Contract }) {
const { contract } = props const { contract } = props
const user = useUser() const user = useUser()
const userBets = useUserContractBets(user?.id, contract.id) const userBets = useUserContractBets(user?.id, contract.id)
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( const { yesFloorShares, noFloorShares } = useSaveShares(
contract as FullContract<CPMM | DPM, Binary>, contract as FullContract<CPMM | DPM, Binary>,
userBets userBets
) )
// TODO: For some reason, Floor Shares are inverted for non-BINARY markets // TODO: This relies on a hack in useSaveShares, where noFloorShares includes
// all non-YES shares. Ideally, useSaveShares should group by all outcomes
const hasUpShares = const hasUpShares =
contract.outcomeType === 'BINARY' ? yesFloorShares : noFloorShares contract.outcomeType === 'BINARY' ? yesFloorShares : noFloorShares
const hasDownShares = const hasDownShares =
contract.outcomeType === 'BINARY' ? noFloorShares : yesFloorShares contract.outcomeType === 'BINARY' ? noFloorShares : yesFloorShares
const color = getColor(contract) const [upHover, setUpHover] = useState(false)
const [downHover, setDownHover] = useState(false)
let previewProb = undefined
try {
previewProb = upHover
? getOutcomeProbabilityAfterBet(
contract,
quickOutcome(contract, 'UP') || '',
BET_SIZE
)
: downHover
? 1 -
getOutcomeProbabilityAfterBet(
contract,
quickOutcome(contract, 'DOWN') || '',
BET_SIZE
)
: undefined
} catch (e) {
// Catch any errors from hovering on an invalid option
}
const color = getColor(contract, previewProb)
async function placeQuickBet(direction: 'UP' | 'DOWN') { async function placeQuickBet(direction: 'UP' | 'DOWN') {
const betPromise = async () => { const betPromise = async () => {
const outcome = quickOutcome(contract, direction) const outcome = quickOutcome(contract, direction)
return await placeBet({ return await placeBet({
amount: 10, amount: BET_SIZE,
outcome, outcome,
contractId: contract.id, contractId: contract.id,
}) })
} }
const shortQ = contract.question.slice(0, 20) const shortQ = contract.question.slice(0, 20)
toast.promise(betPromise(), { toast.promise(betPromise(), {
loading: `${formatMoney(10)} on "${shortQ}"...`, loading: `${formatMoney(BET_SIZE)} on "${shortQ}"...`,
success: `${formatMoney(10)} on "${shortQ}"...`, success: `${formatMoney(BET_SIZE)} on "${shortQ}"...`,
error: (err) => `${err.message}`, error: (err) => `${err.message}`,
}) })
} }
@ -68,7 +98,7 @@ export function QuickBet(props: { contract: Contract }) {
if (contract.outcomeType === 'FREE_RESPONSE') { if (contract.outcomeType === 'FREE_RESPONSE') {
// TODO: Implement shorting of free response answers // TODO: Implement shorting of free response answers
if (direction === 'DOWN') { if (direction === 'DOWN') {
throw new Error("Can't short free response answers") throw new Error("Can't bet against free response answers")
} }
return getTopAnswer(contract)?.id return getTopAnswer(contract)?.id
} }
@ -81,18 +111,19 @@ export function QuickBet(props: { contract: Contract }) {
return ( return (
<Col <Col
className={clsx( className={clsx(
'relative -my-4 -mr-5 min-w-[6rem] justify-center gap-2 pr-5 pl-3 align-middle', 'relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle'
// Use this for colored QuickBet panes // Use this for colored QuickBet panes
// `bg-opacity-10 bg-${color}` // `bg-opacity-10 bg-${color}`
'bg-gray-50'
)} )}
> >
{/* Up bet triangle */} {/* Up bet triangle */}
<div> <div>
<div <div
className="peer absolute top-0 left-0 right-0 h-[50%]" className="peer absolute top-0 left-0 right-0 h-[50%]"
onMouseEnter={() => setUpHover(true)}
onMouseLeave={() => setUpHover(false)}
onClick={() => placeQuickBet('UP')} onClick={() => placeQuickBet('UP')}
></div> />
<div className="mt-2 text-center text-xs text-transparent peer-hover:text-gray-400"> <div className="mt-2 text-center text-xs text-transparent peer-hover:text-gray-400">
{formatMoney(10)} {formatMoney(10)}
</div> </div>
@ -101,31 +132,43 @@ export function QuickBet(props: { contract: Contract }) {
<TriangleFillIcon <TriangleFillIcon
className={clsx( className={clsx(
'mx-auto h-5 w-5', 'mx-auto h-5 w-5',
`text-${color} text-opacity-70 peer-hover:text-gray-400` upHover ? `text-${color}` : 'text-gray-400'
)} )}
/> />
) : ( ) : (
<TriangleFillIcon className="mx-auto h-5 w-5 text-gray-200 peer-hover:text-gray-400" /> <TriangleFillIcon
className={clsx(
'mx-auto h-5 w-5',
upHover ? `text-${color}` : 'text-gray-200'
)}
/>
)} )}
</div> </div>
<QuickOutcomeView contract={contract} /> <QuickOutcomeView contract={contract} previewProb={previewProb} />
{/* Down bet triangle */} {/* Down bet triangle */}
<div> <div>
<div <div
className="peer absolute bottom-0 left-0 right-0 h-[50%]" className="peer absolute bottom-0 left-0 right-0 h-[50%]"
onMouseEnter={() => setDownHover(true)}
onMouseLeave={() => setDownHover(false)}
onClick={() => placeQuickBet('DOWN')} onClick={() => placeQuickBet('DOWN')}
></div> ></div>
{hasDownShares > 0 ? ( {hasDownShares > 0 ? (
<TriangleDownFillIcon <TriangleDownFillIcon
className={clsx( className={clsx(
'mx-auto h-5 w-5', 'mx-auto h-5 w-5',
`text-${color} text-opacity-70 peer-hover:text-gray-400` downHover ? `text-${color}` : 'text-gray-400'
)} )}
/> />
) : ( ) : (
<TriangleDownFillIcon className="mx-auto h-5 w-5 text-gray-200 peer-hover:text-gray-400" /> <TriangleDownFillIcon
className={clsx(
'mx-auto h-5 w-5',
downHover ? `text-${color}` : 'text-gray-200'
)}
/>
)} )}
<div className="mb-2 text-center text-xs text-transparent peer-hover:text-gray-400"> <div className="mb-2 text-center text-xs text-transparent peer-hover:text-gray-400">
{formatMoney(10)} {formatMoney(10)}
@ -135,10 +178,10 @@ export function QuickBet(props: { contract: Contract }) {
) )
} }
export function ProbBar(props: { contract: Contract }) { export function ProbBar(props: { contract: Contract; previewProb?: number }) {
const { contract } = props const { contract, previewProb } = props
const color = getColor(contract) const color = getColor(contract, previewProb)
const prob = getProb(contract) const prob = previewProb ?? getProb(contract)
return ( return (
<> <>
<div <div
@ -147,7 +190,7 @@ export function ProbBar(props: { contract: Contract }) {
'bg-gray-200' 'bg-gray-200'
)} )}
style={{ height: `${100 * (1 - prob)}%` }} style={{ height: `${100 * (1 - prob)}%` }}
></div> />
<div <div
className={clsx( className={clsx(
'absolute right-0 bottom-0 w-2 rounded-br-md transition-all', 'absolute right-0 bottom-0 w-2 rounded-br-md transition-all',
@ -156,41 +199,45 @@ export function ProbBar(props: { contract: Contract }) {
prob === 1 ? 'rounded-tr-md' : '' prob === 1 ? 'rounded-tr-md' : ''
)} )}
style={{ height: `${100 * prob}%` }} style={{ height: `${100 * prob}%` }}
></div> />
</> </>
) )
} }
export function QuickOutcomeView(props: { contract: Contract }) { function QuickOutcomeView(props: {
const { contract } = props contract: Contract
previewProb?: number
caption?: 'chance' | 'expected'
}) {
const { contract, previewProb, caption } = props
const { outcomeType } = contract const { outcomeType } = contract
// If there's a preview prob, display that instead of the current prob
const override =
previewProb === undefined ? undefined : formatPercent(previewProb)
const textColor = `text-${getColor(contract, previewProb)}`
let display: string | undefined
switch (outcomeType) {
case 'BINARY':
display = getBinaryProbPercent(contract)
break
case 'NUMERIC':
display = formatLargeNumber(getExpectedValue(contract as NumericContract))
break
case 'FREE_RESPONSE':
const topAnswer = getTopAnswer(contract as FreeResponseContract)
display =
topAnswer &&
formatPercent(getOutcomeProbability(contract, topAnswer.id))
break
}
return ( return (
<> <Col className={clsx('items-center text-3xl', textColor)}>
{outcomeType === 'BINARY' && ( {override ?? display}
<BinaryResolutionOrChance {caption && <div className="text-base">{caption}</div>}
className="items-center" <ProbBar contract={contract} previewProb={previewProb} />
contract={contract} </Col>
hideText
/>
)}
{outcomeType === 'NUMERIC' && (
<NumericResolutionOrExpectation
className="items-center"
contract={contract as NumericContract}
hideText
/>
)}
{outcomeType === 'FREE_RESPONSE' && (
<FreeResponseResolutionOrChance
className="self-end text-gray-600"
contract={contract as FullContract<DPM, FreeResponse>}
truncate="long"
hideText
/>
)}
</>
) )
} }
@ -215,15 +262,14 @@ function getNumericScale(contract: NumericContract) {
return (ev - min) / (max - min) return (ev - min) / (max - min)
} }
export function getColor(contract: Contract) { export function getColor(contract: Contract, previewProb?: number) {
// TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind // TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind
// TODO: Try injecting a gradient here // TODO: Try injecting a gradient here
// return 'primary' // return 'primary'
const { resolution } = contract const { resolution } = contract
if (resolution) { if (resolution) {
return ( return (
// @ts-ignore; TODO: Have better typing for contract.resolution? OUTCOME_TO_COLOR[resolution as 'YES' | 'NO' | 'CANCEL' | 'MKT'] ??
OUTCOME_TO_COLOR[resolution] ||
// If resolved to a FR answer, use 'primary' // If resolved to a FR answer, use 'primary'
'primary' 'primary'
) )
@ -233,9 +279,6 @@ export function getColor(contract: Contract) {
} }
const marketClosed = (contract.closeTime || Infinity) < Date.now() const marketClosed = (contract.closeTime || Infinity) < Date.now()
return marketClosed const prob = previewProb ?? getProb(contract)
? 'gray-400' return marketClosed ? 'gray-400' : prob >= 0.5 ? 'primary' : 'red-400'
: getProb(contract) >= 0.5
? 'primary'
: 'red-400'
} }

View File

@ -12,12 +12,13 @@ import { formatPercent, formatMoney } from 'common/util/format'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { APIError, placeBet } from '../lib/firebase/api-call' import { APIError, placeBet } from '../lib/firebase/api-call'
import { firebaseLogin, User } from '../lib/firebase/users' import { User } from '../lib/firebase/users'
import { BuyAmountInput } from './amount-input' import { BuyAmountInput } from './amount-input'
import { BucketInput } from './bucket-input' import { BucketInput } from './bucket-input'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Row } from './layout/row' import { Row } from './layout/row'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
import { SignUpPrompt } from './sign-up-prompt'
export function NumericBetPanel(props: { export function NumericBetPanel(props: {
contract: NumericContract contract: NumericContract
@ -32,14 +33,7 @@ export function NumericBetPanel(props: {
<NumericBuyPanel contract={contract} user={user} /> <NumericBuyPanel contract={contract} user={user} />
{user === null && ( <SignUpPrompt />
<button
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign up to trade!
</button>
)}
</Col> </Col>
) )
} }

View File

@ -0,0 +1,16 @@
import React from 'react'
import { useUser } from 'web/hooks/use-user'
import { firebaseLogin } from 'web/lib/firebase/users'
export function SignUpPrompt() {
const user = useUser()
return user === null ? (
<button
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign up to bet!
</button>
) : null
}

View File

@ -13,7 +13,7 @@ import {
updateDoc, updateDoc,
limit, limit,
} from 'firebase/firestore' } from 'firebase/firestore'
import { range, sortBy } from 'lodash' import { range, sortBy, sum } from 'lodash'
import { app } from './init' import { app } from './init'
import { getValues, listenForValue, listenForValues } from './utils' import { getValues, listenForValue, listenForValues } from './utils'
@ -56,6 +56,14 @@ export function contractMetrics(contract: Contract) {
return { volumeLabel, createdDate, automaticResolutionDate, resolvedDate } return { volumeLabel, createdDate, automaticResolutionDate, resolvedDate }
} }
export function contractPool(contract: Contract) {
return contract.mechanism === 'cpmm-1'
? formatMoney(contract.totalLiquidity)
: contract.mechanism === 'dpm-2'
? formatMoney(sum(Object.values(contract.pool)))
: 'Empty pool'
}
export function getBinaryProb(contract: FullContract<any, Binary>) { export function getBinaryProb(contract: FullContract<any, Binary>) {
const { totalShares, pool, p, resolutionProbability, mechanism } = contract const { totalShares, pool, p, resolutionProbability, mechanism } = contract

View File

@ -8,7 +8,7 @@ export default function TriangleDownFillIcon(props: { className?: string }) {
viewBox="0 0 16 16" viewBox="0 0 16 16"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
transform="rotate(-180 8 8)" transform="rotate(-180 8 8)"
d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z" d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z"
/> />

View File

@ -8,7 +8,7 @@ export default function TriangleFillIcon(props: { className?: string }) {
viewBox="0 0 16 16" viewBox="0 0 16 16"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z" d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z"
/> />
</svg> </svg>

View File

@ -20,6 +20,7 @@ export type LiteMarket = {
description: string description: string
tags: string[] tags: string[]
url: string url: string
outcomeType: string
mechanism: string mechanism: string
pool: number pool: number
@ -57,6 +58,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
tags, tags,
slug, slug,
pool, pool,
outcomeType,
mechanism, mechanism,
volume7Days, volume7Days,
volume24Hours, volume24Hours,
@ -88,6 +90,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
probability, probability,
p, p,
totalLiquidity, totalLiquidity,
outcomeType,
mechanism, mechanism,
volume7Days, volume7Days,
volume24Hours, volume24Hours,