pseudo numeric market layout, quick betting
This commit is contained in:
parent
0ee6dfae94
commit
d8aaf4219e
10
common/numeric.ts
Normal file
10
common/numeric.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { PseudoNumericContract } from './contract'
|
||||
|
||||
export function formatNumericProbability(
|
||||
p: number,
|
||||
contract: PseudoNumericContract
|
||||
) {
|
||||
const { min, max } = contract
|
||||
const value = p * (max - min) + min
|
||||
return Math.round(value).toString()
|
||||
}
|
|
@ -25,7 +25,7 @@ import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
|||
import { sellShares } from 'web/lib/firebase/api-call'
|
||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||
import { InfoTooltip } from './info-tooltip'
|
||||
import { BinaryOutcomeLabel } from './outcome-label'
|
||||
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
||||
import {
|
||||
calculatePayoutAfterCorrectBet,
|
||||
calculateShares,
|
||||
|
@ -46,7 +46,7 @@ import { isIOS } from 'web/lib/util/device'
|
|||
import { track } from 'web/lib/service/analytics'
|
||||
|
||||
export function BetPanel(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
|
@ -85,7 +85,7 @@ export function BetPanel(props: {
|
|||
}
|
||||
|
||||
export function BetPanelSwitcher(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
title?: string // Set if BetPanel is on a feed modal
|
||||
selected?: 'YES' | 'NO'
|
||||
|
@ -93,7 +93,8 @@ export function BetPanelSwitcher(props: {
|
|||
}) {
|
||||
const { contract, className, title, selected, onBetSuccess } = props
|
||||
|
||||
const { mechanism } = contract
|
||||
const { mechanism, outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const user = useUser()
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
|
@ -126,7 +127,12 @@ export function BetPanelSwitcher(props: {
|
|||
<Row className="items-center justify-between gap-2">
|
||||
<div>
|
||||
You have {formatWithCommas(floorShares)}{' '}
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
||||
{isPseudoNumeric ? (
|
||||
<PseudoNumericOutcomeLabel outcome={sharesOutcome} />
|
||||
) : (
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} />
|
||||
)}{' '}
|
||||
shares
|
||||
</div>
|
||||
|
||||
{tradeType === 'BUY' && (
|
||||
|
@ -405,7 +411,7 @@ function BuyPanel(props: {
|
|||
}
|
||||
|
||||
export function SellPanel(props: {
|
||||
contract: CPMMBinaryContract
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
userBets: Bet[]
|
||||
shares: number
|
||||
sharesOutcome: 'YES' | 'NO'
|
||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx'
|
|||
|
||||
import { BetPanelSwitcher } from './bet-panel'
|
||||
import { YesNoSelector } from './yes-no-selector'
|
||||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { Modal } from './layout/modal'
|
||||
import { SellButton } from './sell-button'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
|
@ -12,7 +12,7 @@ import { useSaveShares } from './use-save-shares'
|
|||
|
||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||
export default function BetRow(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
btnClassName?: string
|
||||
betPanelClassName?: string
|
||||
|
@ -32,6 +32,7 @@ export default function BetRow(props: {
|
|||
return (
|
||||
<>
|
||||
<YesNoSelector
|
||||
isPseudoNumeric={contract.outcomeType === 'PSEUDO_NUMERIC'}
|
||||
className={clsx('justify-end', className)}
|
||||
btnClassName={clsx('btn-sm w-24', btnClassName)}
|
||||
onSelect={(choice) => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
BinaryContract,
|
||||
FreeResponseContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from 'common/contract'
|
||||
import {
|
||||
AnswerLabel,
|
||||
|
@ -16,7 +17,11 @@ import {
|
|||
CancelLabel,
|
||||
FreeResponseOutcomeLabel,
|
||||
} from '../outcome-label'
|
||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
||||
import {
|
||||
getOutcomeProbability,
|
||||
getProbability,
|
||||
getTopAnswer,
|
||||
} from 'common/calculate'
|
||||
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
|
||||
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||
import { QuickBet, ProbBar, getColor } from './quick-bet'
|
||||
|
@ -24,6 +29,7 @@ import { useContractWithPreload } from 'web/hooks/use-contract'
|
|||
import { useUser } from 'web/hooks/use-user'
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { trackCallback } from 'web/lib/service/analytics'
|
||||
import { formatNumericProbability } from 'common/numeric'
|
||||
|
||||
export function ContractCard(props: {
|
||||
contract: Contract
|
||||
|
@ -284,3 +290,37 @@ export function NumericResolutionOrExpectation(props: {
|
|||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function PseudoNumericResolutionOrExpectation(props: {
|
||||
contract: PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
const { resolution, resolutionProbability } = contract
|
||||
const textColor = `text-blue-400`
|
||||
|
||||
return (
|
||||
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||
{resolution ? (
|
||||
<>
|
||||
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
||||
|
||||
{resolution === 'CANCEL' ? (
|
||||
<CancelLabel />
|
||||
) : (
|
||||
<div className="text-blue-400">
|
||||
{formatNumericProbability(resolutionProbability ?? 0, contract)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={clsx('text-3xl', textColor)}>
|
||||
{formatNumericProbability(getProbability(contract), contract)}
|
||||
</div>
|
||||
<div className={clsx('text-base', textColor)}>expected</div>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
FreeResponseResolutionOrChance,
|
||||
BinaryResolutionOrChance,
|
||||
NumericResolutionOrExpectation,
|
||||
PseudoNumericResolutionOrExpectation,
|
||||
} from './contract-card'
|
||||
import { Bet } from 'common/bet'
|
||||
import BetRow from '../bet-row'
|
||||
|
@ -50,6 +51,13 @@ export const ContractOverview = (props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
{isPseudoNumeric && (
|
||||
<PseudoNumericResolutionOrExpectation
|
||||
contract={contract}
|
||||
className="hidden items-end xl:flex"
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericResolutionOrExpectation
|
||||
contract={contract}
|
||||
|
@ -62,6 +70,11 @@ export const ContractOverview = (props: {
|
|||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<BinaryResolutionOrChance contract={contract} />
|
||||
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
</Row>
|
||||
) : isPseudoNumeric ? (
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
</Row>
|
||||
) : (
|
||||
|
|
|
@ -81,13 +81,16 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
}
|
||||
}
|
||||
|
||||
const data = [{ id: 'Yes', data: points, color: '#11b981' }]
|
||||
const isBinary = contract.outcomeType === 'BINARY'
|
||||
|
||||
const data = [
|
||||
{ id: 'Yes', data: points, color: isBinary ? '#11b981' : '#5fa5f9' },
|
||||
]
|
||||
|
||||
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
||||
const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime)
|
||||
|
||||
const formatter =
|
||||
contract.outcomeType === 'BINARY' ? formatPercent : formatNumeric
|
||||
const formatter = isBinary ? formatPercent : formatNumeric
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx'
|
|||
import {
|
||||
getOutcomeProbability,
|
||||
getOutcomeProbabilityAfterBet,
|
||||
getProbability,
|
||||
getTopAnswer,
|
||||
} from 'common/calculate'
|
||||
import { getExpectedValue } from 'common/calculate-dpm'
|
||||
|
@ -25,18 +26,18 @@ import { useSaveShares } from '../use-save-shares'
|
|||
import { sellShares } from 'web/lib/firebase/api-call'
|
||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { formatNumericProbability } from 'common/numeric'
|
||||
|
||||
const BET_SIZE = 10
|
||||
|
||||
export function QuickBet(props: { contract: Contract; user: User }) {
|
||||
const { contract, user } = props
|
||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
||||
const { mechanism, outcomeType } = contract
|
||||
const isCpmm = mechanism === 'cpmm-1'
|
||||
|
||||
const userBets = useUserContractBets(user.id, contract.id)
|
||||
const topAnswer =
|
||||
contract.outcomeType === 'FREE_RESPONSE'
|
||||
? getTopAnswer(contract)
|
||||
: undefined
|
||||
outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
|
||||
|
||||
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||
|
@ -45,9 +46,9 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
topAnswer?.number.toString() || undefined
|
||||
)
|
||||
const hasUpShares =
|
||||
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
|
||||
yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC')
|
||||
const hasDownShares =
|
||||
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
|
||||
noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC'
|
||||
|
||||
const [upHover, setUpHover] = useState(false)
|
||||
const [downHover, setDownHover] = useState(false)
|
||||
|
@ -130,25 +131,6 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
})
|
||||
}
|
||||
|
||||
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||
if (contract.outcomeType === 'BINARY') {
|
||||
return direction === 'UP' ? 'YES' : 'NO'
|
||||
}
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
// TODO: Implement shorting of free response answers
|
||||
if (direction === 'DOWN') {
|
||||
throw new Error("Can't bet against free response answers")
|
||||
}
|
||||
return getTopAnswer(contract)?.id
|
||||
}
|
||||
if (contract.outcomeType === 'NUMERIC') {
|
||||
// TODO: Ideally an 'UP' bet would be a uniform bet between [current, max]
|
||||
throw new Error("Can't quick bet on numeric markets")
|
||||
}
|
||||
}
|
||||
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
return (
|
||||
<Col
|
||||
className={clsx(
|
||||
|
@ -173,14 +155,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
<TriangleFillIcon
|
||||
className={clsx(
|
||||
'mx-auto h-5 w-5',
|
||||
upHover ? textColor : 'text-gray-400'
|
||||
upHover ? 'text-green-500' : 'text-gray-400'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TriangleFillIcon
|
||||
className={clsx(
|
||||
'mx-auto h-5 w-5',
|
||||
upHover ? textColor : 'text-gray-200'
|
||||
upHover ? 'text-green-500' : 'text-gray-200'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
@ -189,7 +171,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
||||
|
||||
{/* Down bet triangle */}
|
||||
{contract.outcomeType !== 'BINARY' ? (
|
||||
{outcomeType !== 'BINARY' && outcomeType !== 'PSEUDO_NUMERIC' ? (
|
||||
<div>
|
||||
<div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div>
|
||||
<TriangleDownFillIcon
|
||||
|
@ -254,6 +236,25 @@ export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||
const { outcomeType } = contract
|
||||
|
||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||
return direction === 'UP' ? 'YES' : 'NO'
|
||||
}
|
||||
if (outcomeType === 'FREE_RESPONSE') {
|
||||
// TODO: Implement shorting of free response answers
|
||||
if (direction === 'DOWN') {
|
||||
throw new Error("Can't bet against free response answers")
|
||||
}
|
||||
return getTopAnswer(contract)?.id
|
||||
}
|
||||
if (outcomeType === 'NUMERIC') {
|
||||
// TODO: Ideally an 'UP' bet would be a uniform bet between [current, max]
|
||||
throw new Error("Can't quick bet on numeric markets")
|
||||
}
|
||||
}
|
||||
|
||||
function QuickOutcomeView(props: {
|
||||
contract: Contract
|
||||
previewProb?: number
|
||||
|
@ -261,9 +262,16 @@ function QuickOutcomeView(props: {
|
|||
}) {
|
||||
const { contract, previewProb, caption } = props
|
||||
const { outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
// If there's a preview prob, display that instead of the current prob
|
||||
const override =
|
||||
previewProb === undefined ? undefined : formatPercent(previewProb)
|
||||
previewProb === undefined
|
||||
? undefined
|
||||
: isPseudoNumeric
|
||||
? formatNumericProbability(previewProb, contract)
|
||||
: formatPercent(previewProb)
|
||||
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
let display: string | undefined
|
||||
|
@ -271,6 +279,9 @@ function QuickOutcomeView(props: {
|
|||
case 'BINARY':
|
||||
display = getBinaryProbPercent(contract)
|
||||
break
|
||||
case 'PSEUDO_NUMERIC':
|
||||
display = formatNumericProbability(getProbability(contract), contract)
|
||||
break
|
||||
case 'NUMERIC':
|
||||
display = formatLargeNumber(getExpectedValue(contract))
|
||||
break
|
||||
|
@ -316,7 +327,8 @@ function getNumericScale(contract: NumericContract) {
|
|||
export function getColor(contract: Contract) {
|
||||
// TODO: Try injecting a gradient here
|
||||
// return 'primary'
|
||||
const { resolution } = contract
|
||||
const { resolution, outcomeType } = contract
|
||||
|
||||
if (resolution) {
|
||||
return (
|
||||
OUTCOME_TO_COLOR[resolution as resolution] ??
|
||||
|
@ -325,6 +337,8 @@ export function getColor(contract: Contract) {
|
|||
)
|
||||
}
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC') return 'blue-400'
|
||||
|
||||
if ((contract.closeTime ?? Infinity) < Date.now()) {
|
||||
return 'gray-400'
|
||||
}
|
||||
|
|
|
@ -7,12 +7,12 @@ import { NumberCancelSelector } from './yes-no-selector'
|
|||
import { Spacer } from './layout/spacer'
|
||||
import { ResolveConfirmationButton } from './confirmation-button'
|
||||
import { resolveMarket } from 'web/lib/firebase/fn-call'
|
||||
import { NumericContract } from 'common/contract'
|
||||
import { NumericContract, PseudoNumericContract } from 'common/contract'
|
||||
import { BucketInput } from './bucket-input'
|
||||
|
||||
export function NumericResolutionPanel(props: {
|
||||
creator: User
|
||||
contract: NumericContract
|
||||
contract: NumericContract | PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||
import { useState } from 'react'
|
||||
|
@ -7,7 +7,7 @@ import clsx from 'clsx'
|
|||
import { SellSharesModal } from './sell-modal'
|
||||
|
||||
export function SellButton(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
user: User | null | undefined
|
||||
sharesOutcome: 'YES' | 'NO' | undefined
|
||||
shares: number
|
||||
|
@ -16,7 +16,8 @@ export function SellButton(props: {
|
|||
const { contract, user, sharesOutcome, shares, panelClassName } = props
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
const [showSellModal, setShowSellModal] = useState(false)
|
||||
const { mechanism } = contract
|
||||
const { mechanism, outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
||||
return (
|
||||
|
@ -32,7 +33,10 @@ export function SellButton(props: {
|
|||
)}
|
||||
onClick={() => setShowSellModal(true)}
|
||||
>
|
||||
{'Sell ' + sharesOutcome}
|
||||
Sell{' '}
|
||||
{isPseudoNumeric
|
||||
? { YES: 'HIGH', NO: 'LOW' }[sharesOutcome]
|
||||
: sharesOutcome}
|
||||
</button>
|
||||
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
||||
{'(' + Math.floor(shares) + ' shares)'}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { Bet } from 'common/bet'
|
||||
import { User } from 'common/user'
|
||||
import { Modal } from './layout/modal'
|
||||
|
@ -11,7 +11,7 @@ import clsx from 'clsx'
|
|||
|
||||
export function SellSharesModal(props: {
|
||||
className?: string
|
||||
contract: CPMMBinaryContract
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
userBets: Bet[]
|
||||
shares: number
|
||||
sharesOutcome: 'YES' | 'NO'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
import { useState } from 'react'
|
||||
import { Col } from './layout/col'
|
||||
|
@ -10,7 +10,7 @@ import { useSaveShares } from './use-save-shares'
|
|||
import { SellSharesModal } from './sell-modal'
|
||||
|
||||
export function SellRow(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
user: User | null | undefined
|
||||
className?: string
|
||||
}) {
|
||||
|
|
|
@ -142,11 +142,12 @@ export function ContractPageContent(
|
|||
const { creatorId, isResolved, question, outcomeType } = contract
|
||||
|
||||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC'
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const allowTrade = tradingAllowed(contract)
|
||||
const allowResolve = !isResolved && isCreator && !!user
|
||||
const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve)
|
||||
const hasSidePanel = (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
|
||||
|
||||
const ogCardProps = getOpenGraphProps(contract)
|
||||
|
||||
|
@ -159,7 +160,7 @@ export function ContractPageContent(
|
|||
<BetPanel className="hidden xl:flex" contract={contract} />
|
||||
))}
|
||||
{allowResolve &&
|
||||
(isNumeric ? (
|
||||
(isNumeric || isPseudoNumeric ? (
|
||||
<NumericResolutionPanel creator={user} contract={contract} />
|
||||
) : (
|
||||
<ResolutionPanel creator={user} contract={contract} />
|
||||
|
|
Loading…
Reference in New Issue
Block a user