diff --git a/common/new-bet.ts b/common/new-bet.ts index f484b9f7..9b5e591b 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -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, diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 351b012e..e78a9bdc 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -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} /> - + {isLimitOrder ? ( + + ) : ( + + )} @@ -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(undefined) + const [lowLimitProb, setLowLimitProb] = useState() + const [highLimitProb, setHighLimitProb] = useState() + const betChoice = 'YES' + const [error, setError] = useState() + 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 ( + <> +
+ Trigger when the {isPseudoNumeric ? 'value' : 'probability'} reaches low + or high limit. +
+ + + +
Low
+ + + +
High
+ + +
+ +
+ Max amount* +
+ + + + + +
+ {isPseudoNumeric ? ( + 'Max payout' + ) : ( + <> + Max payout + + )} +
+ +
+
+ + {formatMoney(lowPayout)} + + (+{lowReturnPercent}) +
+
+ + +
+ {isPseudoNumeric ? ( + 'Max payout' + ) : ( + <> + Max payout + + )} +
+ +
+
+ + {formatMoney(highPayout)} + + (+{highReturnPercent}) +
+
+ + + + + {user && ( + + )} + + {wasSubmitted &&
Order submitted!
} + + ) +} + function QuickOrLimitBet(props: { isLimitOrder: boolean setIsLimitOrder: (isLimitOrder: boolean) => void diff --git a/web/components/probability-input.tsx b/web/components/probability-input.tsx index 15f73799..77d0fe4c 100644 --- a/web/components/probability-input.tsx +++ b/web/components/probability-input.tsx @@ -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: { ) } + +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 ? ( + + setProb( + value === undefined + ? undefined + : 100 * + getPseudoProbability( + value, + contract.min, + contract.max, + contract.isLogScale + ) + ) + } + isSubmitting={isSubmitting} + /> + ) : ( + + ) +}