Compare commits

...

8 Commits

Author SHA1 Message Date
Austin Chen
336eb54d72 WIP 2022-05-24 15:42:23 -07:00
Austin Chen
f761d64228 Clean up comments 2022-05-24 09:00:46 -07:00
Austin Chen
7d47a5910f Minor cleanups 2022-05-24 08:46:05 -07:00
Austin Chen
8e746ce0cb Pull out quick bet display component 2022-05-24 08:40:43 -07:00
Austin Chen
a204380f57 Animate the prob bar change too 2022-05-24 08:09:13 -07:00
Austin Chen
2373f8e009 Show amount moved in probability 2022-05-24 07:56:32 -07:00
Austin Chen
6551aa7ea4 Revert "Switch from triangle to a circle arrow"
This reverts commit 370f8eefe4.
2022-05-24 07:38:58 -07:00
Austin Chen
370f8eefe4 Switch from triangle to a circle arrow
WIP
2022-05-24 07:34:47 -07:00
2 changed files with 227 additions and 89 deletions

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: {
@ -54,7 +52,7 @@ export function ContractCard(props: {
className className
)} )}
> >
<Row className={clsx(showQuickBet ? 'divide-x' : '')}> <Row className={clsx(showQuickBet ? '' : '')}>
<Col className="relative flex-1 gap-3 pr-1"> <Col className="relative flex-1 gap-3 pr-1">
<div <div
className={clsx( className={clsx(
@ -92,11 +90,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 +124,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 +146,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 +177,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)
@ -189,7 +203,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 +215,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 +235,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

@ -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,31 +12,75 @@ 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, function previewProb(contract: Contract, amount: number) {
NumericResolutionOrExpectation, if (amount === 0) {
FreeResponseResolutionOrChance, return getProb(contract)
} from './contract-card' }
try {
if (amount > 0) {
return getOutcomeProbabilityAfterBet(
contract,
quickOutcome(contract, 'UP') || '',
amount
)
}
if (amount < 0) {
return (
1 -
getOutcomeProbabilityAfterBet(
contract,
quickOutcome(contract, 'DOWN') || '',
-amount
)
)
}
} catch (e) {
return getProb(contract)
}
}
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 short 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")
}
}
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
) )
@ -42,7 +90,30 @@ export function QuickBet(props: { contract: Contract }) {
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') || '',
10
)
: downHover
? 1 -
getOutcomeProbabilityAfterBet(
contract,
quickOutcome(contract, 'DOWN') || '',
10
)
: 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 () => {
@ -61,36 +132,20 @@ export function QuickBet(props: { contract: Contract }) {
}) })
} }
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 short 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")
}
}
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-[6rem] justify-center gap-2 pr-5 pl-3 align-middle'
// Use this for colored QuickBet panes // Use this for colored QuickBet panes
// `bg-opacity-10 bg-${color}` // upHover || downHover ? `bg-${color} bg-opacity-5` : 'bg-gray-50'
'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>
<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">
@ -109,12 +164,14 @@ export function QuickBet(props: { contract: Contract }) {
)} )}
</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 ? (
@ -135,22 +192,22 @@ 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
className={clsx( className={clsx(
'absolute right-0 top-0 w-2 rounded-tr-md transition-all', 'absolute right-0 top-0 w-4 rounded-tr-md transition-all',
'bg-gray-200' 'bg-gray-200'
)} )}
style={{ height: `${100 * (1 - prob)}%` }} style={{ height: `${100 * (1 - prob)}%` }}
></div> ></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-4 rounded-br-md transition-all',
`bg-${color}`, `bg-${color}`,
// If we're showing the full bar, also round the top // If we're showing the full bar, also round the top
prob === 1 ? 'rounded-tr-md' : '' prob === 1 ? 'rounded-tr-md' : ''
@ -161,39 +218,112 @@ export function ProbBar(props: { contract: Contract }) {
) )
} }
export function QuickOutcomeView(props: { contract: Contract }) { export function LiquidityBar(props: {
contract: Contract
previewProb?: number
}) {
const { contract } = props const { contract } = props
const { outcomeType } = contract const color = getColor(contract)
const up10 = previewProb(contract, 10)
const up100 = previewProb(contract, 100)
const down10 = previewProb(contract, -10)
const down100 = previewProb(contract, -100)
console.log('up, down', up10, down10)
return ( return (
<> <>
{outcomeType === 'BINARY' && ( {/* Gray background */}
<BinaryResolutionOrChance {/* <div
className="items-center" className={clsx(
contract={contract} 'absolute right-0 top-0 h-full w-2 rounded-r-md transition-all',
hideText 'bg-gray-100 bg-opacity-50'
/> )}
)} ></div> */}
{/* indigo-200 liquidity range for 100 */}
{outcomeType === 'NUMERIC' && ( {/* indigo-400 liquidity range for 10 */}
<NumericResolutionOrExpectation <div
className="items-center" className={clsx(
contract={contract as NumericContract} 'absolute -right-1 w-1 transition-all',
hideText `bg-blue-300 bg-opacity-100`
/> )}
)} style={{
height: `${100 * (up100 - down100)}%`,
bottom: `${100 * down100}%`,
}}
></div>
{outcomeType === 'FREE_RESPONSE' && ( {/* indigo-400 liquidity range for 10 */}
<FreeResponseResolutionOrChance <div
className="self-end text-gray-600" className={clsx(
contract={contract as FullContract<DPM, FreeResponse>} 'absolute -right-1.5 w-1.5 transition-all',
truncate="long" `bg-blue-600 bg-opacity-100`
hideText )}
/> style={{
)} height: `${100 * (up10 - down10)}%`,
bottom: `${100 * down10}%`,
}}
></div>
{/* Try a tailwind gradient for 100 */}
{/* <div
className="absolute right-0 w-2 bg-gradient-to-b from-red-50 via-red-500 to-red-500 opacity-50"
style={{
height: `${100 * (up100 - down100)}%`,
bottom: `${100 * down100}%`,
}}
></div> */}
{/* <div
className={clsx(
'absolute right-0 bottom-0 w-24 rounded-br-md transition-all',
`bg-indigo-500 bg-opacity-20`,
// If we're showing the full bar, also round the top
prob === 1 ? 'rounded-tr-md' : ''
)}
style={{ height: `${100 * prob}%` }}
></div> */}
</> </>
) )
} }
function QuickOutcomeView(props: {
contract: Contract
previewProb?: number
caption?: 'chance' | 'expected'
}) {
const { contract, previewProb, caption } = props
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 (
<Col className={clsx('items-center text-3xl', textColor)}>
{override ?? display}
{caption && <div className="text-base">{caption}</div>}
<ProbBar contract={contract} previewProb={previewProb} />
<LiquidityBar contract={contract} previewProb={previewProb} />
</Col>
)
}
// Return a number from 0 to 1 for this contract // Return a number from 0 to 1 for this contract
// Resolved contracts are set to 1, for coloring purposes (even if NO) // Resolved contracts are set to 1, for coloring purposes (even if NO)
function getProb(contract: Contract) { function getProb(contract: Contract) {
@ -215,7 +345,7 @@ 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'
@ -233,9 +363,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'
} }