Merge branch 'main' into automated-market-resolution
This commit is contained in:
commit
5f8031c155
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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' &&
|
||||||
<FreeResponseTopAnswer
|
(resolution ? (
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
<FreeResponseOutcomeLabel
|
||||||
truncate="long"
|
contract={contract as FreeResponseContract}
|
||||||
/>
|
resolution={resolution}
|
||||||
)}
|
truncate={'long'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FreeResponseTopAnswer
|
||||||
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
|
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')}>
|
||||||
<FreeResponseOutcomeLabel
|
Resolved
|
||||||
contract={contract}
|
</div>
|
||||||
resolution={resolution}
|
{(resolution === 'CANCEL' || resolution === 'MKT') && (
|
||||||
truncate={truncate}
|
<FreeResponseOutcomeLabel
|
||||||
answerClassName="text-xl"
|
contract={contract}
|
||||||
/>
|
resolution={resolution}
|
||||||
|
truncate={truncate}
|
||||||
|
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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
<tr>
|
<td>Pool</td>
|
||||||
<td>Liquidity</td>
|
<td>{contractPool(contract)}</td>
|
||||||
<td>{formatMoney(contract.totalLiquidity)}</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{contract.mechanism === 'dpm-2' && (
|
|
||||||
<tr>
|
|
||||||
<td>Pool</td>
|
|
||||||
<td>{formatMoney(sum(Object.values(contract.pool)))}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
16
web/components/sign-up-prompt.tsx
Normal file
16
web/components/sign-up-prompt.tsx
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user