Prototype range limit order UI

This commit is contained in:
James Grugett 2022-07-17 15:32:43 -05:00
parent 1edc1993e1
commit bc33f9572f
3 changed files with 305 additions and 11 deletions

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 {
@ -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 }
}
export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO',
amount: number,

View File

@ -13,7 +13,7 @@ import {
formatPercent,
formatWithCommas,
} 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 { Bet, LimitBet } from 'common/bet'
import { APIError, placeBet } from 'web/lib/firebase/api'
@ -33,7 +33,10 @@ import { SellRow } from './sell-row'
import { useSaveBinaryShares } from './use-save-binary-shares'
import { SignUpPrompt } from './sign-up-prompt'
import { isIOS } from 'web/lib/util/device'
import { ProbabilityInput } from './probability-input'
import {
ProbabilityInput,
ProbabilityOrNumericInput,
} from './probability-input'
import { track } from 'web/lib/service/analytics'
import { removeUndefinedProps } from 'common/util/object'
import { useUnfilledBets } from 'web/hooks/use-bets'
@ -76,12 +79,20 @@ export function BetPanel(props: {
isLimitOrder={isLimitOrder}
setIsLimitOrder={setIsLimitOrder}
/>
<BuyPanel
contract={contract}
user={user}
isLimitOrder={isLimitOrder}
unfilledBets={unfilledBets}
/>
{isLimitOrder ? (
<RangeOrderPanel
contract={contract}
user={user}
unfilledBets={unfilledBets}
/>
) : (
<BuyPanel
contract={contract}
user={user}
isLimitOrder={isLimitOrder}
unfilledBets={unfilledBets}
/>
)}
<SignUpPrompt />
</Col>
@ -400,6 +411,219 @@ function BuyPanel(props: {
)
}
function RangeOrderPanel(props: {
contract: CPMMBinaryContract | PseudoNumericContract
user: User | null | undefined
unfilledBets: Bet[]
onBuySuccess?: () => void
}) {
const { contract, user, unfilledBets, 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)
function onBetChange(newAmount: number | undefined) {
setWasSubmitted(false)
setBetAmount(newAmount)
}
async function submitBet() {
if (!user || !betAmount) return
const limitProbScaled =
lowLimitProb !== undefined ? lowLimitProb / 100 : undefined
setError(undefined)
setIsSubmitting(true)
placeBet(
removeUndefinedProps({
amount: betAmount,
outcome: betChoice,
contractId: contract.id,
limitProb: limitProbScaled,
})
)
.then((r) => {
console.log('placed bet. Result:', r)
setIsSubmitting(false)
setWasSubmitted(true)
setBetAmount(undefined)
if (onBuySuccess) onBuySuccess()
})
.catch((e) => {
if (e instanceof APIError) {
setError(e.toString())
} else {
console.error(e)
setError('Error placing bet')
}
setIsSubmitting(false)
})
track('bet', {
location: 'bet panel',
outcomeType: contract.outcomeType,
slug: contract.slug,
contractId: contract.id,
amount: betAmount,
outcome: betChoice,
isLimitOrder: true,
limitProb: limitProbScaled,
})
}
const betDisabled = isSubmitting || !betAmount || error
const lowProbFrac = (lowLimitProb ?? initialProb * 100) / 100
const {
currentPayout: lowPayout,
currentReturn: lowReturn,
totalFees: lowFees,
} = getBinaryBetStats(
'YES',
betAmount ?? 0,
contract,
lowProbFrac,
unfilledBets as LimitBet[]
)
const lowReturnPercent = formatPercent(lowReturn)
const highProbFrac = (highLimitProb ?? initialProb * 100) / 100
const {
currentPayout: highPayout,
currentReturn: highReturn,
totalFees: highFees,
} = getBinaryBetStats(
'NO',
betAmount ?? 0,
contract,
highProbFrac,
unfilledBets as LimitBet[]
)
const highReturnPercent = formatPercent(highReturn)
return (
<>
<div className="my-3 text-sm text-gray-500">
Trigger when the {isPseudoNumeric ? 'value' : 'probability'} reaches low
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={''}
/>
</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={''}
/>
</Col>
</Row>
<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">
<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(lowFees)} in fees`}
/>
</Row>
<div>
<span className="mr-2 whitespace-nowrap">
{formatMoney(lowPayout)}
</span>
(+{lowReturnPercent})
</div>
</Row>
<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(highFees)} in fees`}
/>
</Row>
<div>
<span className="mr-2 whitespace-nowrap">
{formatMoney(highPayout)}
</span>
(+{highReturnPercent})
</div>
</Row>
</Col>
<Spacer h={8} />
{user && (
<button
className={clsx(
'btn flex-1',
betDisabled
? 'btn-disabled'
: betChoice === 'YES'
? 'btn-primary'
: 'border-none bg-red-400 hover:bg-red-500',
isSubmitting ? 'loading' : ''
)}
onClick={betDisabled ? undefined : submitBet}
>
{isSubmitting ? 'Submitting...' : 'Submit order'}
</button>
)}
{wasSubmitted && <div className="mt-4">Order submitted!</div>}
</>
)
}
function QuickOrLimitBet(props: {
isLimitOrder: boolean
setIsLimitOrder: (isLimitOrder: boolean) => void

View File

@ -1,4 +1,7 @@
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 { Spacer } from './layout/spacer'
@ -6,10 +9,12 @@ export function ProbabilityInput(props: {
prob: number | undefined
onChange: (newProb: number | undefined) => void
disabled?: boolean
placeholder?: string
className?: string
inputClassName?: string
}) {
const { prob, onChange, disabled, className, inputClassName } = props
const { prob, onChange, disabled, placeholder, className, inputClassName } =
props
const onProbChange = (str: string) => {
let prob = parseInt(str.replace(/\D/g, ''))
@ -35,7 +40,7 @@ export function ProbabilityInput(props: {
min={1}
pattern="[0-9]*"
inputMode="numeric"
placeholder="0"
placeholder={placeholder ?? '0'}
maxLength={2}
value={prob ?? ''}
disabled={disabled}
@ -47,3 +52,42 @@ export function ProbabilityInput(props: {
</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}
/>
) : (
<ProbabilityInput
inputClassName="w-full max-w-none"
prob={prob}
onChange={setProb}
disabled={isSubmitting}
placeholder={placeholder}
/>
)
}