Yes and no buttons on contract page (#868)
* Yes and no buttons on contract page * Cheating by adding 0.05 to max shares but gives better quickbet UX
This commit is contained in:
parent
e17234ecce
commit
b39e0f304f
|
@ -32,6 +32,17 @@ export default function BetButton(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Col className={clsx('items-center', className)}>
|
<Col className={clsx('items-center', className)}>
|
||||||
|
{user && (
|
||||||
|
<div className={'mb-1 w-24 text-center text-sm text-gray-500'}>
|
||||||
|
{hasYesShares
|
||||||
|
? `(${Math.floor(yesShares)} ${
|
||||||
|
isPseudoNumeric ? 'HIGHER' : 'YES'
|
||||||
|
})`
|
||||||
|
: hasNoShares
|
||||||
|
? `(${Math.floor(noShares)} ${isPseudoNumeric ? 'LOWER' : 'NO'})`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{user ? (
|
{user ? (
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
|
@ -46,18 +57,6 @@ export default function BetButton(props: {
|
||||||
) : (
|
) : (
|
||||||
<BetSignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user && (
|
|
||||||
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
|
||||||
{hasYesShares
|
|
||||||
? `(${Math.floor(yesShares)} ${
|
|
||||||
isPseudoNumeric ? 'HIGHER' : 'YES'
|
|
||||||
})`
|
|
||||||
: hasNoShares
|
|
||||||
? `(${Math.floor(noShares)} ${isPseudoNumeric ? 'LOWER' : 'NO'})`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Modal open={open} setOpen={setOpen} position="center">
|
<Modal open={open} setOpen={setOpen} position="center">
|
||||||
|
|
|
@ -25,7 +25,11 @@ import {
|
||||||
} from 'common/calculate'
|
} from 'common/calculate'
|
||||||
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
|
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
|
||||||
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||||
import { getColor, ProbBar, QuickBet } from './quick-bet'
|
import {
|
||||||
|
getColor,
|
||||||
|
ProbBar,
|
||||||
|
QuickBetArrows,
|
||||||
|
} from 'web/components/contract/quick-bet-arrows'
|
||||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
@ -101,7 +105,7 @@ export function ContractCard(props: {
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
{showQuickBet ? (
|
{showQuickBet ? (
|
||||||
<QuickBet contract={contract} user={user} className="z-10" />
|
<QuickBetArrows contract={contract} user={user} className="z-10" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{outcomeType === 'BINARY' && (
|
{outcomeType === 'BINARY' && (
|
||||||
|
|
|
@ -27,21 +27,44 @@ import {
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { ContractDetails, ExtraMobileContractDetails } from './contract-details'
|
import { ContractDetails, ExtraMobileContractDetails } from './contract-details'
|
||||||
import { NumericGraph } from './numeric-graph'
|
import { NumericGraph } from './numeric-graph'
|
||||||
|
import { QuickBetButtons } from 'web/components/contract/quick-bet-button'
|
||||||
|
|
||||||
const OverviewQuestion = (props: { text: string }) => (
|
const OverviewQuestion = (props: { text: string }) => (
|
||||||
<Linkify className="text-2xl text-indigo-700 md:text-3xl" text={props.text} />
|
<Linkify className="text-2xl text-indigo-700 md:text-3xl" text={props.text} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const BetWidget = (props: { contract: CPMMContract }) => {
|
const BetWidget = (props: { contract: CPMMContract }) => {
|
||||||
|
const { contract } = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col className={'justify-center'}>
|
||||||
<BetButton contract={props.contract} />
|
<Row className={'gap-4'}>
|
||||||
{!user && (
|
{contract.outcomeType === 'BINARY' &&
|
||||||
<div className="mt-1 text-center text-sm text-gray-500">
|
user &&
|
||||||
(with play money!)
|
QuickBetButtons({
|
||||||
</div>
|
contract: contract as CPMMBinaryContract,
|
||||||
)}
|
user: user,
|
||||||
|
side: 'NO',
|
||||||
|
className: 'self-end min-w-[60px]',
|
||||||
|
})}
|
||||||
|
<BetButton contract={props.contract} />
|
||||||
|
|
||||||
|
{contract.outcomeType === 'BINARY' &&
|
||||||
|
user &&
|
||||||
|
QuickBetButtons({
|
||||||
|
contract: contract as CPMMBinaryContract,
|
||||||
|
user: user,
|
||||||
|
side: 'YES',
|
||||||
|
className: 'self-end min-w-[60px]',
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
<Row className={'items-center justify-center'}>
|
||||||
|
{!user && (
|
||||||
|
<div className="mt-1 text-center text-sm text-gray-500">
|
||||||
|
(with play money!)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -85,13 +108,13 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => {
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
<ExtraMobileContractDetails contract={contract} />
|
|
||||||
{tradingAllowed(contract) && (
|
{tradingAllowed(contract) && (
|
||||||
<BetWidget contract={contract as CPMMBinaryContract} />
|
<BetWidget contract={contract as CPMMBinaryContract} />
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<ContractProbGraph contract={contract} bets={[...bets].reverse()} />
|
<ContractProbGraph contract={contract} bets={[...bets].reverse()} />
|
||||||
|
<ExtraMobileContractDetails contract={contract} />
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -140,11 +163,11 @@ const PseudoNumericOverview = (props: {
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||||
<ExtraMobileContractDetails contract={contract} />
|
|
||||||
{tradingAllowed(contract) && <BetWidget contract={contract} />}
|
{tradingAllowed(contract) && <BetWidget contract={contract} />}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<ContractProbGraph contract={contract} bets={[...bets].reverse()} />
|
<ContractProbGraph contract={contract} bets={[...bets].reverse()} />
|
||||||
|
<ExtraMobileContractDetails contract={contract} />
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import { getBinaryProb } from 'common/contract-details'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
|
||||||
export function QuickBet(props: {
|
export function QuickBetArrows(props: {
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
user: User
|
user: User
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -243,7 +243,7 @@ export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
export function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||||
const { outcomeType } = contract
|
const { outcomeType } = contract
|
||||||
|
|
||||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
128
web/components/contract/quick-bet-button.tsx
Normal file
128
web/components/contract/quick-bet-button.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import {
|
||||||
|
getOutcomeProbability,
|
||||||
|
getProbability,
|
||||||
|
getTopAnswer,
|
||||||
|
} from 'common/calculate'
|
||||||
|
import { getExpectedValue } from 'common/calculate-dpm'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { Contract, CPMMBinaryContract, NumericContract } from 'common/contract'
|
||||||
|
import { formatMoney } from 'common/util/format'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
|
import { placeBet } from 'web/lib/firebase/api'
|
||||||
|
import { useSaveBinaryShares } from '../use-save-binary-shares'
|
||||||
|
import { sellShares } from 'web/lib/firebase/api'
|
||||||
|
import { calculateCpmmSale } from 'common/calculate-cpmm'
|
||||||
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
import { quickOutcome } from 'web/components/contract/quick-bet-arrows'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
|
||||||
|
const BET_SIZE = 10
|
||||||
|
|
||||||
|
export function QuickBetButtons(props: {
|
||||||
|
contract: CPMMBinaryContract
|
||||||
|
user: User
|
||||||
|
side: 'YES' | 'NO'
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, side, user } = props
|
||||||
|
let sharesSold: number | undefined
|
||||||
|
let sellOutcome: 'YES' | 'NO' | undefined
|
||||||
|
let saleAmount: number | undefined
|
||||||
|
|
||||||
|
const userBets = useUserContractBets(user.id, contract.id)
|
||||||
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
|
|
||||||
|
const { yesShares, noShares } = useSaveBinaryShares(contract, userBets)
|
||||||
|
const oppositeShares = side === 'YES' ? noShares : yesShares
|
||||||
|
if (oppositeShares > 0.01) {
|
||||||
|
sellOutcome = side === 'YES' ? 'NO' : 'YES'
|
||||||
|
|
||||||
|
const prob = getProb(contract)
|
||||||
|
const maxSharesSold =
|
||||||
|
(BET_SIZE + 0.05) / (sellOutcome === 'YES' ? prob : 1 - prob)
|
||||||
|
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
||||||
|
|
||||||
|
const { saleValue } = calculateCpmmSale(
|
||||||
|
contract,
|
||||||
|
sharesSold,
|
||||||
|
sellOutcome,
|
||||||
|
unfilledBets
|
||||||
|
)
|
||||||
|
saleAmount = saleValue
|
||||||
|
}
|
||||||
|
|
||||||
|
async function placeQuickBet() {
|
||||||
|
const betPromise = async () => {
|
||||||
|
if (sharesSold && sellOutcome) {
|
||||||
|
return await sellShares({
|
||||||
|
shares: sharesSold,
|
||||||
|
outcome: sellOutcome,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const outcome = quickOutcome(contract, side === 'YES' ? 'UP' : 'DOWN')
|
||||||
|
return await placeBet({
|
||||||
|
amount: BET_SIZE,
|
||||||
|
outcome,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const shortQ = contract.question.slice(0, 20)
|
||||||
|
const message =
|
||||||
|
sellOutcome && saleAmount
|
||||||
|
? `${formatMoney(saleAmount)} sold of "${shortQ}"...`
|
||||||
|
: `${formatMoney(BET_SIZE)} on "${shortQ}"...`
|
||||||
|
|
||||||
|
toast.promise(betPromise(), {
|
||||||
|
loading: message,
|
||||||
|
success: message,
|
||||||
|
error: (err) => `${err.message}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
track('quick bet button', {
|
||||||
|
slug: contract.slug,
|
||||||
|
outcome: side,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size={'lg'}
|
||||||
|
onClick={() => placeQuickBet()}
|
||||||
|
color={side === 'YES' ? 'green' : 'red'}
|
||||||
|
className={props.className}
|
||||||
|
>
|
||||||
|
{side === 'YES' ? 'Yes' : 'No'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, resolutionProbability } = contract
|
||||||
|
return resolutionProbability
|
||||||
|
? resolutionProbability
|
||||||
|
: resolution
|
||||||
|
? 1
|
||||||
|
: outcomeType === 'BINARY'
|
||||||
|
? getBinaryProb(contract)
|
||||||
|
: outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
? getProbability(contract)
|
||||||
|
: outcomeType === 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE'
|
||||||
|
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
||||||
|
: outcomeType === 'NUMERIC'
|
||||||
|
? getNumericScale(contract)
|
||||||
|
: 1 // Should not happen
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumericScale(contract: NumericContract) {
|
||||||
|
const { min, max } = contract
|
||||||
|
const ev = getExpectedValue(contract)
|
||||||
|
return (ev - min) / (max - min)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user