pseudo numeric market layout, quick betting

This commit is contained in:
mantikoros 2022-06-27 19:14:23 -05:00
parent 0ee6dfae94
commit d8aaf4219e
12 changed files with 147 additions and 55 deletions

10
common/numeric.ts Normal file
View 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()
}

View File

@ -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'

View File

@ -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) => {

View File

@ -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>
)
}

View File

@ -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>
) : (

View File

@ -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

View File

@ -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'
}

View File

@ -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(() => {

View File

@ -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)'}

View File

@ -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'

View File

@ -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
}) {

View File

@ -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} />