2022-05-24 06:44:16 +00:00
|
|
|
import clsx from 'clsx'
|
2022-05-24 23:38:43 +00:00
|
|
|
import {
|
|
|
|
getOutcomeProbability,
|
|
|
|
getOutcomeProbabilityAfterBet,
|
|
|
|
getTopAnswer,
|
|
|
|
} from 'common/calculate'
|
2022-05-24 06:44:16 +00:00
|
|
|
import { getExpectedValue } from 'common/calculate-dpm'
|
2022-05-28 20:36:16 +00:00
|
|
|
import { User } from 'common/user'
|
2022-06-03 00:30:34 +00:00
|
|
|
import { Contract, NumericContract, resolution } from 'common/contract'
|
2022-05-24 23:38:43 +00:00
|
|
|
import {
|
|
|
|
formatLargeNumber,
|
|
|
|
formatMoney,
|
|
|
|
formatPercent,
|
|
|
|
} from 'common/util/format'
|
|
|
|
import { useState } from 'react'
|
2022-05-24 06:44:16 +00:00
|
|
|
import toast from 'react-hot-toast'
|
|
|
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
|
|
|
import { placeBet } from 'web/lib/firebase/api-call'
|
2022-05-24 23:38:43 +00:00
|
|
|
import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
2022-05-24 06:44:16 +00:00
|
|
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
|
|
|
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
|
|
|
import { Col } from '../layout/col'
|
|
|
|
import { OUTCOME_TO_COLOR } from '../outcome-label'
|
|
|
|
import { useSaveShares } from '../use-save-shares'
|
2022-06-07 20:54:58 +00:00
|
|
|
import { sellShares } from 'web/lib/firebase/api-call'
|
2022-06-02 23:22:39 +00:00
|
|
|
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
2022-06-15 03:00:36 +00:00
|
|
|
import { track } from 'web/lib/service/analytics'
|
2022-05-24 23:38:43 +00:00
|
|
|
|
|
|
|
const BET_SIZE = 10
|
2022-05-24 06:44:16 +00:00
|
|
|
|
2022-05-28 20:36:16 +00:00
|
|
|
export function QuickBet(props: { contract: Contract; user: User }) {
|
|
|
|
const { contract, user } = props
|
2022-06-02 23:22:39 +00:00
|
|
|
const isCpmm = contract.mechanism === 'cpmm-1'
|
2022-05-24 06:44:16 +00:00
|
|
|
|
2022-05-28 20:36:16 +00:00
|
|
|
const userBets = useUserContractBets(user.id, contract.id)
|
2022-05-25 22:51:33 +00:00
|
|
|
const topAnswer =
|
|
|
|
contract.outcomeType === 'FREE_RESPONSE'
|
2022-06-01 02:42:35 +00:00
|
|
|
? getTopAnswer(contract)
|
2022-05-25 22:51:33 +00:00
|
|
|
: undefined
|
|
|
|
|
|
|
|
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
2022-06-02 23:22:39 +00:00
|
|
|
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
2022-06-01 02:42:35 +00:00
|
|
|
contract,
|
2022-05-25 22:51:33 +00:00
|
|
|
userBets,
|
|
|
|
topAnswer?.number.toString() || undefined
|
2022-05-24 06:44:16 +00:00
|
|
|
)
|
2022-05-26 12:23:22 +00:00
|
|
|
const hasUpShares =
|
|
|
|
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
|
2022-05-24 06:44:16 +00:00
|
|
|
const hasDownShares =
|
2022-05-25 22:51:33 +00:00
|
|
|
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
|
2022-05-24 06:44:16 +00:00
|
|
|
|
2022-05-24 23:38:43 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-06-02 23:22:39 +00:00
|
|
|
let sharesSold: number | undefined
|
|
|
|
let sellOutcome: 'YES' | 'NO' | undefined
|
|
|
|
let saleAmount: number | undefined
|
|
|
|
if (isCpmm && (upHover || downHover)) {
|
|
|
|
const oppositeShares = upHover ? noShares : yesShares
|
|
|
|
if (oppositeShares) {
|
|
|
|
sellOutcome = upHover ? 'NO' : 'YES'
|
|
|
|
|
|
|
|
const prob = getProb(contract)
|
|
|
|
const maxSharesSold = BET_SIZE / (sellOutcome === 'YES' ? prob : 1 - prob)
|
|
|
|
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
|
|
|
|
|
|
|
const { newPool, saleValue } = calculateCpmmSale(
|
|
|
|
contract,
|
|
|
|
sharesSold,
|
|
|
|
sellOutcome
|
|
|
|
)
|
|
|
|
saleAmount = saleValue
|
|
|
|
previewProb = getCpmmProbability(newPool, contract.p)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-24 06:44:16 +00:00
|
|
|
async function placeQuickBet(direction: 'UP' | 'DOWN') {
|
|
|
|
const betPromise = async () => {
|
2022-06-02 23:22:39 +00:00
|
|
|
if (sharesSold && sellOutcome) {
|
|
|
|
return await sellShares({
|
|
|
|
shares: sharesSold,
|
|
|
|
outcome: sellOutcome,
|
|
|
|
contractId: contract.id,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-24 06:44:16 +00:00
|
|
|
const outcome = quickOutcome(contract, direction)
|
|
|
|
return await placeBet({
|
2022-05-24 23:38:43 +00:00
|
|
|
amount: BET_SIZE,
|
2022-05-24 06:44:16 +00:00
|
|
|
outcome,
|
|
|
|
contractId: contract.id,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
const shortQ = contract.question.slice(0, 20)
|
2022-06-02 23:22:39 +00:00
|
|
|
const message =
|
|
|
|
sellOutcome && saleAmount
|
|
|
|
? `${formatMoney(saleAmount)} sold of "${shortQ}"...`
|
|
|
|
: `${formatMoney(BET_SIZE)} on "${shortQ}"...`
|
|
|
|
|
2022-05-24 06:44:16 +00:00
|
|
|
toast.promise(betPromise(), {
|
2022-06-02 23:22:39 +00:00
|
|
|
loading: message,
|
|
|
|
success: message,
|
2022-05-24 06:44:16 +00:00
|
|
|
error: (err) => `${err.message}`,
|
|
|
|
})
|
2022-06-15 03:00:36 +00:00
|
|
|
|
|
|
|
track('quick bet', {
|
|
|
|
slug: contract.slug,
|
|
|
|
direction,
|
|
|
|
contractId: contract.id,
|
|
|
|
})
|
2022-05-24 06:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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') {
|
2022-05-24 23:38:43 +00:00
|
|
|
throw new Error("Can't bet against free response answers")
|
2022-05-24 06:44:16 +00:00
|
|
|
}
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-13 04:42:41 +00:00
|
|
|
const textColor = `text-${getColor(contract)}`
|
2022-06-06 14:54:43 +00:00
|
|
|
|
2022-05-24 06:44:16 +00:00
|
|
|
return (
|
|
|
|
<Col
|
|
|
|
className={clsx(
|
2022-05-25 00:25:23 +00:00
|
|
|
'relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle'
|
2022-05-24 06:44:16 +00:00
|
|
|
// Use this for colored QuickBet panes
|
|
|
|
// `bg-opacity-10 bg-${color}`
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{/* Up bet triangle */}
|
|
|
|
<div>
|
|
|
|
<div
|
|
|
|
className="peer absolute top-0 left-0 right-0 h-[50%]"
|
2022-05-24 23:38:43 +00:00
|
|
|
onMouseEnter={() => setUpHover(true)}
|
|
|
|
onMouseLeave={() => setUpHover(false)}
|
2022-05-24 06:44:16 +00:00
|
|
|
onClick={() => placeQuickBet('UP')}
|
2022-05-24 23:38:43 +00:00
|
|
|
/>
|
2022-05-24 06:44:16 +00:00
|
|
|
<div className="mt-2 text-center text-xs text-transparent peer-hover:text-gray-400">
|
|
|
|
{formatMoney(10)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{hasUpShares > 0 ? (
|
|
|
|
<TriangleFillIcon
|
|
|
|
className={clsx(
|
|
|
|
'mx-auto h-5 w-5',
|
2022-06-06 14:54:43 +00:00
|
|
|
upHover ? textColor : 'text-gray-400'
|
2022-05-24 06:44:16 +00:00
|
|
|
)}
|
|
|
|
/>
|
|
|
|
) : (
|
2022-05-24 23:38:43 +00:00
|
|
|
<TriangleFillIcon
|
|
|
|
className={clsx(
|
|
|
|
'mx-auto h-5 w-5',
|
2022-06-06 14:54:43 +00:00
|
|
|
upHover ? textColor : 'text-gray-200'
|
2022-05-24 23:38:43 +00:00
|
|
|
)}
|
|
|
|
/>
|
2022-05-24 06:44:16 +00:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
2022-05-24 23:38:43 +00:00
|
|
|
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
2022-05-24 06:44:16 +00:00
|
|
|
|
|
|
|
{/* Down bet triangle */}
|
2022-06-02 21:25:41 +00:00
|
|
|
{contract.outcomeType !== 'BINARY' ? (
|
|
|
|
<div>
|
|
|
|
<div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div>
|
2022-05-24 23:38:43 +00:00
|
|
|
<TriangleDownFillIcon
|
2022-06-02 21:25:41 +00:00
|
|
|
className={clsx('mx-auto h-5 w-5 text-gray-200')}
|
2022-05-24 23:38:43 +00:00
|
|
|
/>
|
2022-05-24 06:44:16 +00:00
|
|
|
</div>
|
2022-06-02 21:25:41 +00:00
|
|
|
) : (
|
|
|
|
<div>
|
|
|
|
<div
|
|
|
|
className="peer absolute bottom-0 left-0 right-0 h-[50%]"
|
|
|
|
onMouseEnter={() => setDownHover(true)}
|
|
|
|
onMouseLeave={() => setDownHover(false)}
|
|
|
|
onClick={() => placeQuickBet('DOWN')}
|
|
|
|
></div>
|
|
|
|
{hasDownShares > 0 ? (
|
|
|
|
<TriangleDownFillIcon
|
|
|
|
className={clsx(
|
|
|
|
'mx-auto h-5 w-5',
|
|
|
|
downHover ? 'text-red-500' : 'text-gray-400'
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<TriangleDownFillIcon
|
|
|
|
className={clsx(
|
|
|
|
'mx-auto h-5 w-5',
|
|
|
|
downHover ? 'text-red-500' : 'text-gray-200'
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<div className="mb-2 text-center text-xs text-transparent peer-hover:text-gray-400">
|
|
|
|
{formatMoney(10)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
2022-05-24 06:44:16 +00:00
|
|
|
</Col>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-05-24 23:38:43 +00:00
|
|
|
export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
|
|
|
const { contract, previewProb } = props
|
2022-06-13 04:42:41 +00:00
|
|
|
const color = getColor(contract)
|
2022-05-24 23:38:43 +00:00
|
|
|
const prob = previewProb ?? getProb(contract)
|
2022-05-24 06:44:16 +00:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div
|
|
|
|
className={clsx(
|
2022-06-06 14:54:43 +00:00
|
|
|
'absolute right-0 top-0 w-1.5 rounded-tr-md transition-all',
|
|
|
|
'bg-gray-100'
|
2022-05-24 06:44:16 +00:00
|
|
|
)}
|
|
|
|
style={{ height: `${100 * (1 - prob)}%` }}
|
2022-05-24 23:38:43 +00:00
|
|
|
/>
|
2022-05-24 06:44:16 +00:00
|
|
|
<div
|
|
|
|
className={clsx(
|
2022-06-06 14:54:43 +00:00
|
|
|
'absolute right-0 bottom-0 w-1.5 rounded-br-md transition-all',
|
2022-05-24 06:44:16 +00:00
|
|
|
`bg-${color}`,
|
|
|
|
// If we're showing the full bar, also round the top
|
|
|
|
prob === 1 ? 'rounded-tr-md' : ''
|
|
|
|
)}
|
|
|
|
style={{ height: `${100 * prob}%` }}
|
2022-05-24 23:38:43 +00:00
|
|
|
/>
|
2022-05-24 06:44:16 +00:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-05-24 23:38:43 +00:00
|
|
|
function QuickOutcomeView(props: {
|
|
|
|
contract: Contract
|
|
|
|
previewProb?: number
|
|
|
|
caption?: 'chance' | 'expected'
|
|
|
|
}) {
|
|
|
|
const { contract, previewProb, caption } = props
|
2022-05-24 06:44:16 +00:00
|
|
|
const { outcomeType } = contract
|
2022-05-24 23:38:43 +00:00
|
|
|
// If there's a preview prob, display that instead of the current prob
|
|
|
|
const override =
|
|
|
|
previewProb === undefined ? undefined : formatPercent(previewProb)
|
2022-06-13 04:42:41 +00:00
|
|
|
const textColor = `text-${getColor(contract)}`
|
2022-05-24 06:44:16 +00:00
|
|
|
|
2022-05-24 23:38:43 +00:00
|
|
|
let display: string | undefined
|
|
|
|
switch (outcomeType) {
|
|
|
|
case 'BINARY':
|
|
|
|
display = getBinaryProbPercent(contract)
|
|
|
|
break
|
|
|
|
case 'NUMERIC':
|
2022-06-01 02:42:35 +00:00
|
|
|
display = formatLargeNumber(getExpectedValue(contract))
|
2022-05-24 23:38:43 +00:00
|
|
|
break
|
2022-05-26 21:41:24 +00:00
|
|
|
case 'FREE_RESPONSE': {
|
2022-06-01 02:42:35 +00:00
|
|
|
const topAnswer = getTopAnswer(contract)
|
2022-05-24 23:38:43 +00:00
|
|
|
display =
|
|
|
|
topAnswer &&
|
|
|
|
formatPercent(getOutcomeProbability(contract, topAnswer.id))
|
|
|
|
break
|
2022-05-26 21:41:24 +00:00
|
|
|
}
|
2022-05-24 23:38:43 +00:00
|
|
|
}
|
2022-05-24 06:44:16 +00:00
|
|
|
|
2022-05-24 23:38:43 +00:00
|
|
|
return (
|
|
|
|
<Col className={clsx('items-center text-3xl', textColor)}>
|
|
|
|
{override ?? display}
|
|
|
|
{caption && <div className="text-base">{caption}</div>}
|
|
|
|
<ProbBar contract={contract} previewProb={previewProb} />
|
|
|
|
</Col>
|
2022-05-24 06:44:16 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return a number from 0 to 1 for this contract
|
|
|
|
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
|
|
|
function getProb(contract: Contract) {
|
|
|
|
const { outcomeType, resolution } = contract
|
|
|
|
return resolution
|
|
|
|
? 1
|
|
|
|
: outcomeType === 'BINARY'
|
|
|
|
? getBinaryProb(contract)
|
|
|
|
: outcomeType === 'FREE_RESPONSE'
|
|
|
|
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
|
|
|
: outcomeType === 'NUMERIC'
|
2022-06-01 02:42:35 +00:00
|
|
|
? getNumericScale(contract)
|
2022-05-24 06:44:16 +00:00
|
|
|
: 1 // Should not happen
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNumericScale(contract: NumericContract) {
|
|
|
|
const { min, max } = contract
|
|
|
|
const ev = getExpectedValue(contract)
|
|
|
|
return (ev - min) / (max - min)
|
|
|
|
}
|
|
|
|
|
2022-06-13 04:42:41 +00:00
|
|
|
export function getColor(contract: Contract) {
|
2022-05-24 06:44:16 +00:00
|
|
|
// TODO: Try injecting a gradient here
|
|
|
|
// return 'primary'
|
|
|
|
const { resolution } = contract
|
|
|
|
if (resolution) {
|
|
|
|
return (
|
2022-06-03 00:30:34 +00:00
|
|
|
OUTCOME_TO_COLOR[resolution as resolution] ??
|
2022-05-24 06:44:16 +00:00
|
|
|
// If resolved to a FR answer, use 'primary'
|
|
|
|
'primary'
|
|
|
|
)
|
|
|
|
}
|
2022-06-02 21:00:38 +00:00
|
|
|
|
2022-05-24 06:44:16 +00:00
|
|
|
if (contract.outcomeType === 'NUMERIC') {
|
|
|
|
return 'blue-400'
|
|
|
|
}
|
|
|
|
|
2022-06-02 21:00:38 +00:00
|
|
|
if (contract.outcomeType === 'FREE_RESPONSE') {
|
|
|
|
return 'blue-400'
|
|
|
|
}
|
|
|
|
|
2022-06-06 14:54:43 +00:00
|
|
|
if ((contract.closeTime ?? Infinity) < Date.now()) {
|
|
|
|
return 'gray-400'
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind
|
|
|
|
return 'primary'
|
2022-05-24 06:44:16 +00:00
|
|
|
}
|