Range limit orders (#655)

* Prototype range limit order UI

* Conditionally show YES or NO max payout

* Range bet executes both bets immediately.

* Validate lowLimitProb < highLimitProb

* Show error if low limit is higher than high limit

* Update range order UI

* Revert "Validate lowLimitProb < highLimitProb"

This reverts commit c261fc2743.

* Revert "Range bet executes both bets immediately."

This reverts commit 30b95d75d9.

* Buy panel only non-limit orders

* Bet choice => outcome

* More iterating on range UI

* betChoice => outcome

* Lighten placeholder text
This commit is contained in:
James Grugett 2022-07-22 00:57:56 -05:00 committed by GitHub
parent 23b704ffe0
commit 3b953a7c21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 487 additions and 122 deletions

View File

@ -123,6 +123,7 @@ export function calculateCpmmAmountToProb(
prob: number, prob: number,
outcome: 'YES' | 'NO' outcome: 'YES' | 'NO'
) { ) {
if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity
if (outcome === 'NO') prob = 1 - prob if (outcome === 'NO') prob = 1 - prob
// First, find an upper bound that leads to a more extreme probability than prob. // First, find an upper bound that leads to a more extreme probability than prob.

View File

@ -1,4 +1,4 @@
import { sortBy, sumBy } from 'lodash' import { sortBy, sum, sumBy } from 'lodash'
import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
import { import {
@ -239,6 +239,32 @@ export const getBinaryCpmmBetInfo = (
} }
} }
export const getBinaryBetStats = (
outcome: 'YES' | 'NO',
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number,
unfilledBets: LimitBet[]
) => {
const { newBet } = getBinaryCpmmBetInfo(
outcome,
betAmount ?? 0,
contract,
limitProb,
unfilledBets as LimitBet[]
)
const remainingMatched =
((newBet.orderAmount ?? 0) - newBet.amount) /
(outcome === 'YES' ? limitProb : 1 - limitProb)
const currentPayout = newBet.shares + remainingMatched
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const totalFees = sum(Object.values(newBet.fees))
return { currentPayout, currentReturn, totalFees, newBet }
}
export const getNewBinaryDpmBetInfo = ( export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
amount: number, amount: number,

View File

@ -41,7 +41,7 @@ export function AmountInput(props: {
<span className="bg-gray-200 text-sm">{label}</span> <span className="bg-gray-200 text-sm">{label}</span>
<input <input
className={clsx( className={clsx(
'input input-bordered max-w-[200px] text-lg', 'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400',
error && 'input-error', error && 'input-error',
inputClassName inputClassName
)} )}

View File

@ -13,32 +13,27 @@ import {
formatPercent, formatPercent,
formatWithCommas, formatWithCommas,
} from 'common/util/format' } from 'common/util/format'
import { getBinaryCpmmBetInfo } from 'common/new-bet' import { getBinaryBetStats, getBinaryCpmmBetInfo } from 'common/new-bet'
import { User } from 'web/lib/firebase/users' import { User } from 'web/lib/firebase/users'
import { Bet, LimitBet } from 'common/bet' import { Bet, LimitBet } from 'common/bet'
import { APIError, placeBet } from 'web/lib/firebase/api' import { APIError, placeBet } from 'web/lib/firebase/api'
import { sellShares } from 'web/lib/firebase/api' import { sellShares } from 'web/lib/firebase/api'
import { AmountInput, BuyAmountInput } from './amount-input' import { AmountInput, BuyAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip' import { InfoTooltip } from './info-tooltip'
import { BinaryOutcomeLabel } from './outcome-label' import { BinaryOutcomeLabel, HigherLabel, LowerLabel } from './outcome-label'
import { getProbability } from 'common/calculate' import { getProbability } from 'common/calculate'
import { useFocus } from 'web/hooks/use-focus' import { useFocus } from 'web/hooks/use-focus'
import { useUserContractBets } from 'web/hooks/use-user-bets' import { useUserContractBets } from 'web/hooks/use-user-bets'
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
import { import { getFormattedMappedValue, getMappedValue } from 'common/pseudo-numeric'
getFormattedMappedValue,
getPseudoProbability,
} from 'common/pseudo-numeric'
import { SellRow } from './sell-row' import { SellRow } from './sell-row'
import { useSaveBinaryShares } from './use-save-binary-shares' import { useSaveBinaryShares } from './use-save-binary-shares'
import { SignUpPrompt } from './sign-up-prompt' import { SignUpPrompt } from './sign-up-prompt'
import { isIOS } from 'web/lib/util/device' import { isIOS } from 'web/lib/util/device'
import { ProbabilityInput } from './probability-input' import { ProbabilityOrNumericInput } from './probability-input'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { removeUndefinedProps } from 'common/util/object'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBets } from 'web/hooks/use-bets'
import { LimitBets } from './limit-bets' import { LimitBets } from './limit-bets'
import { BucketInput } from './bucket-input'
import { PillButton } from './buttons/pill-button' import { PillButton } from './buttons/pill-button'
import { YesNoSelector } from './yes-no-selector' import { YesNoSelector } from './yes-no-selector'
@ -73,12 +68,17 @@ export function BetPanel(props: {
setIsLimitOrder={setIsLimitOrder} setIsLimitOrder={setIsLimitOrder}
/> />
<BuyPanel <BuyPanel
hidden={isLimitOrder}
contract={contract}
user={user}
unfilledBets={unfilledBets}
/>
<LimitOrderPanel
hidden={!isLimitOrder}
contract={contract} contract={contract}
user={user} user={user}
isLimitOrder={isLimitOrder}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
/> />
<SignUpPrompt /> <SignUpPrompt />
</Col> </Col>
{unfilledBets.length > 0 && ( {unfilledBets.length > 0 && (
@ -120,14 +120,20 @@ export function SimpleBetPanel(props: {
setIsLimitOrder={setIsLimitOrder} setIsLimitOrder={setIsLimitOrder}
/> />
<BuyPanel <BuyPanel
hidden={isLimitOrder}
contract={contract} contract={contract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
selected={selected} selected={selected}
onBuySuccess={onBetSuccess} onBuySuccess={onBetSuccess}
isLimitOrder={isLimitOrder}
/> />
<LimitOrderPanel
hidden={!isLimitOrder}
contract={contract}
user={user}
unfilledBets={unfilledBets}
onBuySuccess={onBetSuccess}
/>
<SignUpPrompt /> <SignUpPrompt />
</Col> </Col>
@ -142,21 +148,17 @@ function BuyPanel(props: {
contract: CPMMBinaryContract | PseudoNumericContract contract: CPMMBinaryContract | PseudoNumericContract
user: User | null | undefined user: User | null | undefined
unfilledBets: Bet[] unfilledBets: Bet[]
isLimitOrder?: boolean hidden: boolean
selected?: 'YES' | 'NO' selected?: 'YES' | 'NO'
onBuySuccess?: () => void onBuySuccess?: () => void
}) { }) {
const { contract, user, unfilledBets, isLimitOrder, selected, onBuySuccess } = const { contract, user, unfilledBets, hidden, selected, onBuySuccess } = props
props
const initialProb = getProbability(contract) const initialProb = getProbability(contract)
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected) const [outcome, setOutcome] = useState<'YES' | 'NO' | undefined>(selected)
const [betAmount, setBetAmount] = useState<number | undefined>(undefined) const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
const [limitProb, setLimitProb] = useState<number | undefined>(
Math.round(100 * initialProb)
)
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false) const [wasSubmitted, setWasSubmitted] = useState(false)
@ -171,7 +173,7 @@ function BuyPanel(props: {
}, [selected, focusAmountInput]) }, [selected, focusAmountInput])
function onBetChoice(choice: 'YES' | 'NO') { function onBetChoice(choice: 'YES' | 'NO') {
setBetChoice(choice) setOutcome(choice)
setWasSubmitted(false) setWasSubmitted(false)
focusAmountInput() focusAmountInput()
} }
@ -179,29 +181,22 @@ function BuyPanel(props: {
function onBetChange(newAmount: number | undefined) { function onBetChange(newAmount: number | undefined) {
setWasSubmitted(false) setWasSubmitted(false)
setBetAmount(newAmount) setBetAmount(newAmount)
if (!betChoice) { if (!outcome) {
setBetChoice('YES') setOutcome('YES')
} }
} }
async function submitBet() { async function submitBet() {
if (!user || !betAmount) return if (!user || !betAmount) return
if (isLimitOrder && limitProb === undefined) return
const limitProbScaled =
isLimitOrder && limitProb !== undefined ? limitProb / 100 : undefined
setError(undefined) setError(undefined)
setIsSubmitting(true) setIsSubmitting(true)
placeBet( placeBet({
removeUndefinedProps({ outcome,
amount: betAmount, amount: betAmount,
outcome: betChoice, contractId: contract.id,
contractId: contract.id, })
limitProb: limitProbScaled,
})
)
.then((r) => { .then((r) => {
console.log('placed bet. Result:', r) console.log('placed bet. Result:', r)
setIsSubmitting(false) setIsSubmitting(false)
@ -225,21 +220,18 @@ function BuyPanel(props: {
slug: contract.slug, slug: contract.slug,
contractId: contract.id, contractId: contract.id,
amount: betAmount, amount: betAmount,
outcome: betChoice, outcome,
isLimitOrder, isLimitOrder: false,
limitProb: limitProbScaled,
}) })
} }
const betDisabled = isSubmitting || !betAmount || error const betDisabled = isSubmitting || !betAmount || error
const limitProbFrac = (limitProb ?? 0) / 100
const { newPool, newP, newBet } = getBinaryCpmmBetInfo( const { newPool, newP, newBet } = getBinaryCpmmBetInfo(
betChoice ?? 'YES', outcome ?? 'YES',
betAmount ?? 0, betAmount ?? 0,
contract, contract,
isLimitOrder ? limitProbFrac : undefined, undefined,
unfilledBets as LimitBet[] unfilledBets as LimitBet[]
) )
@ -247,11 +239,7 @@ function BuyPanel(props: {
const probStayedSame = const probStayedSame =
formatPercent(resultProb) === formatPercent(initialProb) formatPercent(resultProb) === formatPercent(initialProb)
const remainingMatched = isLimitOrder const currentPayout = newBet.shares
? ((newBet.orderAmount ?? 0) - newBet.amount) /
(betChoice === 'YES' ? limitProbFrac : 1 - limitProbFrac)
: 0
const currentPayout = newBet.shares + remainingMatched
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const currentReturnPercent = formatPercent(currentReturn) const currentReturnPercent = formatPercent(currentReturn)
@ -261,14 +249,14 @@ function BuyPanel(props: {
const format = getFormattedMappedValue(contract) const format = getFormattedMappedValue(contract)
return ( return (
<> <Col className={hidden ? 'hidden' : ''}>
<div className="my-3 text-left text-sm text-gray-500"> <div className="my-3 text-left text-sm text-gray-500">
{isPseudoNumeric ? 'Direction' : 'Outcome'} {isPseudoNumeric ? 'Direction' : 'Outcome'}
</div> </div>
<YesNoSelector <YesNoSelector
className="mb-4" className="mb-4"
btnClassName="flex-1" btnClassName="flex-1"
selected={betChoice} selected={outcome}
onSelect={(choice) => onBetChoice(choice)} onSelect={(choice) => onBetChoice(choice)}
isPseudoNumeric={isPseudoNumeric} isPseudoNumeric={isPseudoNumeric}
/> />
@ -283,61 +271,21 @@ function BuyPanel(props: {
disabled={isSubmitting} disabled={isSubmitting}
inputRef={inputRef} inputRef={inputRef}
/> />
{isLimitOrder && (
<>
<Row className="my-3 items-center gap-2 text-left text-sm text-gray-500">
Limit {isPseudoNumeric ? 'value' : 'probability'}
<InfoTooltip
text={`Bet ${betChoice === 'NO' ? 'down' : 'up'} to this ${
isPseudoNumeric ? 'value' : 'probability'
} and wait to match other bets.`}
/>
</Row>
{isPseudoNumeric ? (
<BucketInput
contract={contract}
onBucketChange={(value) =>
setLimitProb(
value === undefined
? undefined
: 100 *
getPseudoProbability(
value,
contract.min,
contract.max,
contract.isLogScale
)
)
}
isSubmitting={isSubmitting}
/>
) : (
<ProbabilityInput
inputClassName="w-full max-w-none"
prob={limitProb}
onChange={setLimitProb}
disabled={isSubmitting}
/>
)}
</>
)}
<Col className="mt-3 w-full gap-3"> <Col className="mt-3 w-full gap-3">
{!isLimitOrder && ( <Row className="items-center justify-between text-sm">
<Row className="items-center justify-between text-sm"> <div className="text-gray-500">
<div className="text-gray-500"> {isPseudoNumeric ? 'Estimated value' : 'Probability'}
{isPseudoNumeric ? 'Estimated value' : 'Probability'} </div>
{probStayedSame ? (
<div>{format(initialProb)}</div>
) : (
<div>
{format(initialProb)}
<span className="mx-2"></span>
{format(resultProb)}
</div> </div>
{probStayedSame ? ( )}
<div>{format(initialProb)}</div> </Row>
) : (
<div>
{format(initialProb)}
<span className="mx-2"></span>
{format(resultProb)}
</div>
)}
</Row>
)}
<Row className="items-center justify-between gap-2 text-sm"> <Row className="items-center justify-between gap-2 text-sm">
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
@ -346,7 +294,7 @@ function BuyPanel(props: {
'Max payout' 'Max payout'
) : ( ) : (
<> <>
Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> Payout if <BinaryOutcomeLabel outcome={outcome ?? 'YES'} />
</> </>
)} )}
</div> </div>
@ -365,6 +313,353 @@ function BuyPanel(props: {
<Spacer h={8} /> <Spacer h={8} />
{user && (
<button
className={clsx(
'btn flex-1',
betDisabled
? 'btn-disabled'
: outcome === 'YES'
? 'btn-primary'
: 'border-none bg-red-400 hover:bg-red-500',
isSubmitting ? 'loading' : ''
)}
onClick={betDisabled ? undefined : submitBet}
>
{isSubmitting ? 'Submitting...' : 'Submit bet'}
</button>
)}
{wasSubmitted && <div className="mt-4">Bet submitted!</div>}
</Col>
)
}
function LimitOrderPanel(props: {
contract: CPMMBinaryContract | PseudoNumericContract
user: User | null | undefined
unfilledBets: Bet[]
hidden: boolean
onBuySuccess?: () => void
}) {
const { contract, user, unfilledBets, hidden, onBuySuccess } = props
const initialProb = getProbability(contract)
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
const [lowLimitProb, setLowLimitProb] = useState<number | undefined>()
const [highLimitProb, setHighLimitProb] = useState<number | undefined>()
const betChoice = 'YES'
const [error, setError] = useState<string | undefined>()
const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false)
const rangeError =
lowLimitProb !== undefined &&
highLimitProb !== undefined &&
lowLimitProb >= highLimitProb
const outOfRangeError =
(lowLimitProb !== undefined &&
(lowLimitProb <= 0 || lowLimitProb >= 100)) ||
(highLimitProb !== undefined &&
(highLimitProb <= 0 || highLimitProb >= 100))
const initialLow = initialProb * 0.9
const initialHigh = initialProb + (1 - initialProb) * 0.1
const lowPlaceholder = Math.round(
isPseudoNumeric ? getMappedValue(contract)(initialLow) : initialLow * 100
).toString()
const highPlaceholder = Math.round(
isPseudoNumeric ? getMappedValue(contract)(initialHigh) : initialHigh * 100
).toString()
const hasYesLimitBet = lowLimitProb !== undefined && !!betAmount
const hasNoLimitBet = highLimitProb !== undefined && !!betAmount
const hasTwoBets = hasYesLimitBet && hasNoLimitBet
const betDisabled =
isSubmitting ||
!betAmount ||
rangeError ||
outOfRangeError ||
error ||
(!hasYesLimitBet && !hasNoLimitBet)
const yesLimitProb =
lowLimitProb === undefined ? undefined : lowLimitProb / 100
const noLimitProb =
highLimitProb === undefined ? undefined : highLimitProb / 100
const shares =
yesLimitProb !== undefined && noLimitProb !== undefined
? Math.min(
(betAmount ?? 0) / yesLimitProb,
(betAmount ?? 0) / (1 - noLimitProb)
)
: (betAmount ?? 0) / (yesLimitProb ?? 1 - (noLimitProb ?? 1))
const yesAmount = shares * (yesLimitProb ?? 1)
const noAmount = shares * (1 - (noLimitProb ?? 1))
const profitIfBothFilled = shares - (yesAmount + noAmount)
function onBetChange(newAmount: number | undefined) {
setWasSubmitted(false)
setBetAmount(newAmount)
}
async function submitBet() {
if (!user || betDisabled) return
setError(undefined)
setIsSubmitting(true)
const betsPromise = hasTwoBets
? Promise.all([
placeBet({
outcome: 'YES',
amount: yesAmount,
limitProb: yesLimitProb,
contractId: contract.id,
}),
placeBet({
outcome: 'NO',
amount: noAmount,
limitProb: noLimitProb,
contractId: contract.id,
}),
])
: placeBet({
outcome: hasYesLimitBet ? 'YES' : 'NO',
amount: betAmount,
contractId: contract.id,
limitProb: hasYesLimitBet ? yesLimitProb : noLimitProb,
})
betsPromise
.catch((e) => {
if (e instanceof APIError) {
setError(e.toString())
} else {
console.error(e)
setError('Error placing bet')
}
setIsSubmitting(false)
})
.then((r) => {
console.log('placed bet. Result:', r)
setIsSubmitting(false)
setWasSubmitted(true)
setBetAmount(undefined)
if (onBuySuccess) onBuySuccess()
})
if (hasYesLimitBet) {
track('bet', {
location: 'bet panel',
outcomeType: contract.outcomeType,
slug: contract.slug,
contractId: contract.id,
amount: yesAmount,
outcome: 'YES',
limitProb: yesLimitProb,
isLimitOrder: true,
isRangeOrder: hasTwoBets,
})
}
if (hasNoLimitBet) {
track('bet', {
location: 'bet panel',
outcomeType: contract.outcomeType,
slug: contract.slug,
contractId: contract.id,
amount: noAmount,
outcome: 'NO',
limitProb: noLimitProb,
isLimitOrder: true,
isRangeOrder: hasTwoBets,
})
}
}
const {
currentPayout: yesPayout,
currentReturn: yesReturn,
totalFees: yesFees,
newBet: yesBet,
} = getBinaryBetStats(
'YES',
yesAmount,
contract,
Math.min(yesLimitProb ?? initialLow, 0.999),
unfilledBets as LimitBet[]
)
const yesReturnPercent = formatPercent(yesReturn)
const {
currentPayout: noPayout,
currentReturn: noReturn,
totalFees: noFees,
newBet: noBet,
} = getBinaryBetStats(
'NO',
noAmount,
contract,
Math.max(noLimitProb ?? initialHigh, 0.01),
unfilledBets as LimitBet[]
)
const noReturnPercent = formatPercent(noReturn)
return (
<Col className={hidden ? 'hidden' : ''}>
<div className="my-3 text-sm text-gray-500">
Bet when the {isPseudoNumeric ? 'value' : 'probability'} reaches Low
and/or High limit.
</div>
<Row className="items-center gap-4">
<Col className="gap-2">
<div className="ml-1 text-sm text-gray-500">Low</div>
<ProbabilityOrNumericInput
contract={contract}
prob={lowLimitProb}
setProb={setLowLimitProb}
isSubmitting={isSubmitting}
placeholder={lowPlaceholder}
/>
</Col>
<Col className="gap-2">
<div className="ml-1 text-sm text-gray-500">High</div>
<ProbabilityOrNumericInput
contract={contract}
prob={highLimitProb}
setProb={setHighLimitProb}
isSubmitting={isSubmitting}
placeholder={highPlaceholder}
/>
</Col>
</Row>
{rangeError && (
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
Low limit must be less than High limit
</div>
)}
{outOfRangeError && (
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
Limit is out of range
</div>
)}
<div className="my-3 text-left text-sm text-gray-500">
Max amount<span className="ml-1 text-red-500">*</span>
</div>
<BuyAmountInput
inputClassName="w-full max-w-none"
amount={betAmount}
onChange={onBetChange}
error={error}
setError={setError}
disabled={isSubmitting}
/>
<Col className="mt-3 w-full gap-3">
{(hasTwoBets || (hasYesLimitBet && yesBet.amount !== 0)) && (
<Row className="items-center justify-between gap-2 text-sm">
<div className="whitespace-nowrap text-gray-500">
{isPseudoNumeric ? (
<HigherLabel />
) : (
<BinaryOutcomeLabel outcome={'YES'} />
)}{' '}
current fill
</div>
<div className="mr-2 whitespace-nowrap">
{formatMoney(yesBet.amount)} of{' '}
{formatMoney(yesBet.orderAmount ?? 0)}
</div>
</Row>
)}
{(hasTwoBets || (hasNoLimitBet && noBet.amount !== 0)) && (
<Row className="items-center justify-between gap-2 text-sm">
<div className="whitespace-nowrap text-gray-500">
{isPseudoNumeric ? (
<LowerLabel />
) : (
<BinaryOutcomeLabel outcome={'NO'} />
)}{' '}
current fill
</div>
<div className="mr-2 whitespace-nowrap">
{formatMoney(noBet.amount)} of{' '}
{formatMoney(noBet.orderAmount ?? 0)}
</div>
</Row>
)}
{hasTwoBets && (
<Row className="items-center justify-between gap-2 text-sm">
<div className="whitespace-nowrap text-gray-500">
Profit if both orders filled
</div>
<div className="mr-2 whitespace-nowrap">
{formatMoney(profitIfBothFilled)}
</div>
</Row>
)}
{hasYesLimitBet && !hasTwoBets && (
<Row className="items-center justify-between gap-2 text-sm">
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
<div>
{isPseudoNumeric ? (
'Max payout'
) : (
<>
Max <BinaryOutcomeLabel outcome={'YES'} /> payout
</>
)}
</div>
<InfoTooltip
text={`Includes ${formatMoneyWithDecimals(yesFees)} in fees`}
/>
</Row>
<div>
<span className="mr-2 whitespace-nowrap">
{formatMoney(yesPayout)}
</span>
(+{yesReturnPercent})
</div>
</Row>
)}
{hasNoLimitBet && !hasTwoBets && (
<Row className="items-center justify-between gap-2 text-sm">
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
<div>
{isPseudoNumeric ? (
'Max payout'
) : (
<>
Max <BinaryOutcomeLabel outcome={'NO'} /> payout
</>
)}
</div>
<InfoTooltip
text={`Includes ${formatMoneyWithDecimals(noFees)} in fees`}
/>
</Row>
<div>
<span className="mr-2 whitespace-nowrap">
{formatMoney(noPayout)}
</span>
(+{noReturnPercent})
</div>
</Row>
)}
</Col>
{(hasYesLimitBet || hasNoLimitBet) && <Spacer h={8} />}
{user && ( {user && (
<button <button
className={clsx( className={clsx(
@ -380,16 +675,12 @@ function BuyPanel(props: {
> >
{isSubmitting {isSubmitting
? 'Submitting...' ? 'Submitting...'
: isLimitOrder : `Submit order${hasTwoBets ? 's' : ''}`}
? 'Submit order'
: 'Submit bet'}
</button> </button>
)} )}
{wasSubmitted && ( {wasSubmitted && <div className="mt-4">Order submitted!</div>}
<div className="mt-4">{isLimitOrder ? 'Order' : 'Bet'} submitted!</div> </Col>
)}
</>
) )
} }

View File

@ -9,8 +9,9 @@ export function BucketInput(props: {
contract: NumericContract | PseudoNumericContract contract: NumericContract | PseudoNumericContract
isSubmitting?: boolean isSubmitting?: boolean
onBucketChange: (value?: number, bucket?: string) => void onBucketChange: (value?: number, bucket?: string) => void
placeholder?: string
}) { }) {
const { contract, isSubmitting, onBucketChange } = props const { contract, isSubmitting, onBucketChange, placeholder } = props
const [numberString, setNumberString] = useState('') const [numberString, setNumberString] = useState('')
@ -39,7 +40,7 @@ export function BucketInput(props: {
error={undefined} error={undefined}
disabled={isSubmitting} disabled={isSubmitting}
numberString={numberString} numberString={numberString}
label="Value" placeholder={placeholder}
/> />
) )
} }

View File

@ -9,8 +9,8 @@ export function NumberInput(props: {
numberString: string numberString: string
onChange: (newNumberString: string) => void onChange: (newNumberString: string) => void
error: string | undefined error: string | undefined
label: string
disabled?: boolean disabled?: boolean
placeholder?: string
className?: string className?: string
inputClassName?: string inputClassName?: string
// Needed to focus the amount input // Needed to focus the amount input
@ -21,8 +21,8 @@ export function NumberInput(props: {
numberString, numberString,
onChange, onChange,
error, error,
label,
disabled, disabled,
placeholder,
className, className,
inputClassName, inputClassName,
inputRef, inputRef,
@ -32,16 +32,17 @@ export function NumberInput(props: {
return ( return (
<Col className={className}> <Col className={className}>
<label className="input-group"> <label className="input-group">
<span className="bg-gray-200 text-sm">{label}</span>
<input <input
className={clsx( className={clsx(
'input input-bordered max-w-[200px] text-lg', 'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400',
error && 'input-error', error && 'input-error',
inputClassName inputClassName
)} )}
ref={inputRef} ref={inputRef}
type="number" type="number"
placeholder="0" pattern="[0-9]*"
inputMode="numeric"
placeholder={placeholder ?? '0'}
maxLength={9} maxLength={9}
value={numberString} value={numberString}
disabled={disabled} disabled={disabled}

View File

@ -1,4 +1,7 @@
import clsx from 'clsx' import clsx from 'clsx'
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
import { getPseudoProbability } from 'common/pseudo-numeric'
import { BucketInput } from './bucket-input'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
@ -6,10 +9,12 @@ export function ProbabilityInput(props: {
prob: number | undefined prob: number | undefined
onChange: (newProb: number | undefined) => void onChange: (newProb: number | undefined) => void
disabled?: boolean disabled?: boolean
placeholder?: string
className?: string className?: string
inputClassName?: string inputClassName?: string
}) { }) {
const { prob, onChange, disabled, className, inputClassName } = props const { prob, onChange, disabled, placeholder, className, inputClassName } =
props
const onProbChange = (str: string) => { const onProbChange = (str: string) => {
let prob = parseInt(str.replace(/\D/g, '')) let prob = parseInt(str.replace(/\D/g, ''))
@ -27,7 +32,7 @@ export function ProbabilityInput(props: {
<label className="input-group"> <label className="input-group">
<input <input
className={clsx( className={clsx(
'input input-bordered max-w-[200px] text-lg', 'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400',
inputClassName inputClassName
)} )}
type="number" type="number"
@ -35,7 +40,7 @@ export function ProbabilityInput(props: {
min={1} min={1}
pattern="[0-9]*" pattern="[0-9]*"
inputMode="numeric" inputMode="numeric"
placeholder="0" placeholder={placeholder ?? '0'}
maxLength={2} maxLength={2}
value={prob ?? ''} value={prob ?? ''}
disabled={disabled} disabled={disabled}
@ -47,3 +52,43 @@ export function ProbabilityInput(props: {
</Col> </Col>
) )
} }
export function ProbabilityOrNumericInput(props: {
contract: CPMMBinaryContract | PseudoNumericContract
prob: number | undefined
setProb: (prob: number | undefined) => void
isSubmitting: boolean
placeholder?: string
}) {
const { contract, prob, setProb, isSubmitting, placeholder } = props
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
return isPseudoNumeric ? (
<BucketInput
contract={contract}
onBucketChange={(value) =>
setProb(
value === undefined
? undefined
: 100 *
getPseudoProbability(
value,
contract.min,
contract.max,
contract.isLogScale
)
)
}
isSubmitting={isSubmitting}
placeholder={placeholder}
/>
) : (
<ProbabilityInput
inputClassName="w-full max-w-none"
prob={prob}
onChange={setProb}
disabled={isSubmitting}
placeholder={placeholder}
/>
)
}