Buy and sell tabs. Compute some sell info
This commit is contained in:
parent
49bb74cc0d
commit
ce7e8900a1
|
@ -4,15 +4,17 @@ import { useUser } from '../hooks/use-user'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { useUserContractBets } from '../hooks/use-user-bets'
|
import { Bet, MAX_LOAN_PER_CONTRACT } from '../../common/bet'
|
||||||
import { MAX_LOAN_PER_CONTRACT } from '../../common/bet'
|
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
import { calculateCpmmSale } from '../../common/calculate-cpmm'
|
||||||
|
import { Binary, CPMM, FullContract } from '../../common/contract'
|
||||||
|
|
||||||
export function AmountInput(props: {
|
export function AmountInput(props: {
|
||||||
amount: number | undefined
|
amount: number | undefined
|
||||||
onChange: (newAmount: number | undefined) => void
|
onChange: (newAmount: number | undefined) => void
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
|
label: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
inputClassName?: string
|
inputClassName?: string
|
||||||
|
@ -24,6 +26,7 @@ export function AmountInput(props: {
|
||||||
amount,
|
amount,
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
|
label,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
@ -47,7 +50,7 @@ export function AmountInput(props: {
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
<label className="input-group">
|
<label className="input-group">
|
||||||
<span className="bg-gray-200 text-sm">M$</span>
|
<span className="bg-gray-200 text-sm">{label}</span>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'input input-bordered',
|
'input input-bordered',
|
||||||
|
@ -83,6 +86,7 @@ export function BuyAmountInput(props: {
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
setError: (error: string | undefined) => void
|
setError: (error: string | undefined) => void
|
||||||
contractIdForLoan: string | undefined
|
contractIdForLoan: string | undefined
|
||||||
|
userBets: Bet[]
|
||||||
minimumAmount?: number
|
minimumAmount?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -93,6 +97,7 @@ export function BuyAmountInput(props: {
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
onChange,
|
onChange,
|
||||||
|
userBets,
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
contractIdForLoan,
|
contractIdForLoan,
|
||||||
|
@ -105,7 +110,6 @@ export function BuyAmountInput(props: {
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const userBets = useUserContractBets(user?.id, contractIdForLoan) ?? []
|
|
||||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
||||||
|
@ -137,6 +141,7 @@ export function BuyAmountInput(props: {
|
||||||
<AmountInput
|
<AmountInput
|
||||||
amount={amount}
|
amount={amount}
|
||||||
onChange={onAmountChange}
|
onChange={onAmountChange}
|
||||||
|
label="M$"
|
||||||
error={error}
|
error={error}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -145,6 +150,12 @@ export function BuyAmountInput(props: {
|
||||||
>
|
>
|
||||||
{user && (
|
{user && (
|
||||||
<Col className="gap-3 text-sm">
|
<Col className="gap-3 text-sm">
|
||||||
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
|
Remaining balance{' '}
|
||||||
|
<span className="text-neutral">
|
||||||
|
{formatMoney(Math.floor(remainingBalance))}
|
||||||
|
</span>
|
||||||
|
</Row>
|
||||||
{contractIdForLoan && (
|
{contractIdForLoan && (
|
||||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
|
@ -158,12 +169,92 @@ export function BuyAmountInput(props: {
|
||||||
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
|
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
</Col>
|
||||||
Remaining balance{' '}
|
)}
|
||||||
<span className="text-neutral">
|
</AmountInput>
|
||||||
{formatMoney(Math.floor(remainingBalance))}
|
)
|
||||||
</span>
|
}
|
||||||
</Row>
|
|
||||||
|
export function SellAmountInput(props: {
|
||||||
|
contract: FullContract<CPMM, Binary>
|
||||||
|
amount: number | undefined
|
||||||
|
onChange: (newAmount: number | undefined) => void
|
||||||
|
userBets: Bet[]
|
||||||
|
error: string | undefined
|
||||||
|
setError: (error: string | undefined) => void
|
||||||
|
minimumAmount?: number
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
inputClassName?: string
|
||||||
|
// Needed to focus the amount input
|
||||||
|
inputRef?: React.MutableRefObject<any>
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
contract,
|
||||||
|
amount,
|
||||||
|
onChange,
|
||||||
|
userBets,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
|
minimumAmount,
|
||||||
|
inputRef,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
openUserBets,
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = [
|
||||||
|
_.sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
_.sumBy(noBets, (bet) => bet.shares),
|
||||||
|
]
|
||||||
|
|
||||||
|
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||||
|
|
||||||
|
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
||||||
|
const sharesSold = Math.min(amount ?? 0, yesShares || noShares)
|
||||||
|
const sellAmount = calculateCpmmSale(contract, {
|
||||||
|
shares: sharesSold,
|
||||||
|
outcome: sellOutcome,
|
||||||
|
} as Bet).saleValue
|
||||||
|
|
||||||
|
const loanRepaid = Math.min(prevLoanAmount, sellAmount)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AmountInput
|
||||||
|
amount={amount}
|
||||||
|
onChange={onChange}
|
||||||
|
label="Shares"
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
inputClassName={inputClassName}
|
||||||
|
inputRef={inputRef}
|
||||||
|
>
|
||||||
|
{user && (
|
||||||
|
<Col className="gap-3 text-sm">
|
||||||
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
|
Sale amount{' '}
|
||||||
|
<span className="text-neutral">{formatMoney(sellAmount)}</span>
|
||||||
|
</Row>
|
||||||
|
{prevLoanAmount && (
|
||||||
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
|
<Row className="items-center gap-2">
|
||||||
|
Loan repaid{' '}
|
||||||
|
<InfoTooltip
|
||||||
|
text={`Sold shares go toward paying off loans for this market first.`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<span className="text-neutral">{formatMoney(loanRepaid)}</span>{' '}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</AmountInput>
|
</AmountInput>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
@ -16,7 +17,7 @@ import { Title } from './title'
|
||||||
import { firebaseLogin } from '../lib/firebase/users'
|
import { firebaseLogin } from '../lib/firebase/users'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { placeBet } from '../lib/firebase/api-call'
|
import { placeBet } from '../lib/firebase/api-call'
|
||||||
import { BuyAmountInput } from './amount-input'
|
import { BuyAmountInput, SellAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
|
@ -26,6 +27,7 @@ import {
|
||||||
getOutcomeProbabilityAfterBet,
|
getOutcomeProbabilityAfterBet,
|
||||||
} from '../../common/calculate'
|
} from '../../common/calculate'
|
||||||
import { useFocus } from '../hooks/use-focus'
|
import { useFocus } from '../hooks/use-focus'
|
||||||
|
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||||
|
|
||||||
export function BetPanel(props: {
|
export function BetPanel(props: {
|
||||||
contract: FullContract<DPM | CPMM, Binary>
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
|
@ -42,7 +44,19 @@ export function BetPanel(props: {
|
||||||
const { contract, className, title, selected, onBetSuccess } = props
|
const { contract, className, title, selected, onBetSuccess } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id) ?? []
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
userBets,
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = [
|
||||||
|
_.sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
_.sumBy(noBets, (bet) => bet.shares),
|
||||||
|
]
|
||||||
|
|
||||||
|
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||||
|
|
||||||
|
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY')
|
||||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
||||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
const [inputRef, focusAmountInput] = useFocus()
|
const [inputRef, focusAmountInput] = useFocus()
|
||||||
|
@ -137,23 +151,76 @@ export function BetPanel(props: {
|
||||||
text={panelTitle}
|
text={panelTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YesNoSelector
|
{contract.mechanism === 'cpmm-1' && (
|
||||||
className="mb-4"
|
<Row className="gap-2 w-full tabs mb-6">
|
||||||
selected={betChoice}
|
<div
|
||||||
onSelect={(choice) => onBetChoice(choice)}
|
className={clsx(
|
||||||
/>
|
'tab gap-2 tab-bordered flex-1',
|
||||||
|
tradeType === 'BUY' && 'tab-active'
|
||||||
|
)}
|
||||||
|
onClick={() => setTradeType('BUY')}
|
||||||
|
>
|
||||||
|
BUY
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'tab gap-2 tab-bordered flex-1',
|
||||||
|
tradeType === 'SELL' && 'tab-active'
|
||||||
|
)}
|
||||||
|
onClick={() => setTradeType('SELL')}
|
||||||
|
>
|
||||||
|
SELL
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
{tradeType === 'BUY' ? (
|
||||||
<BuyAmountInput
|
<>
|
||||||
inputClassName="w-full"
|
<YesNoSelector
|
||||||
amount={betAmount}
|
className="mb-4"
|
||||||
onChange={onBetChange}
|
selected={betChoice}
|
||||||
error={error}
|
onSelect={(choice) => onBetChoice(choice)}
|
||||||
setError={setError}
|
/>
|
||||||
disabled={isSubmitting}
|
<div className="my-3 text-left text-sm text-gray-500">Buy amount</div>
|
||||||
inputRef={inputRef}
|
<BuyAmountInput
|
||||||
contractIdForLoan={contract.id}
|
inputClassName="w-full"
|
||||||
/>
|
amount={betAmount}
|
||||||
|
onChange={onBetChange}
|
||||||
|
userBets={userBets}
|
||||||
|
error={error}
|
||||||
|
setError={setError}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
inputRef={inputRef}
|
||||||
|
contractIdForLoan={contract.id}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : sellOutcome ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-3 text-left ">
|
||||||
|
You have {Math.round(yesShares || noShares)}{' '}
|
||||||
|
<OutcomeLabel outcome={sellOutcome} /> shares
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-3 text-left text-sm text-gray-500">
|
||||||
|
Sell quantity
|
||||||
|
</div>
|
||||||
|
<SellAmountInput
|
||||||
|
inputClassName="w-full"
|
||||||
|
contract={contract as FullContract<CPMM, Binary>}
|
||||||
|
amount={betAmount}
|
||||||
|
onChange={onBetChange}
|
||||||
|
userBets={userBets}
|
||||||
|
error={error}
|
||||||
|
setError={setError}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="mb-3 text-left text-gray-500">
|
||||||
|
You have don't have any shares to sell.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Col className="mt-3 w-full gap-3">
|
<Col className="mt-3 w-full gap-3">
|
||||||
<Row className="items-center justify-between text-sm">
|
<Row className="items-center justify-between text-sm">
|
||||||
|
@ -165,21 +232,23 @@ export function BetPanel(props: {
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row className="items-start justify-between gap-2 text-sm">
|
{tradeType === 'BUY' && (
|
||||||
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
<Row className="items-start justify-between gap-2 text-sm">
|
||||||
<div>
|
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||||
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
<div>
|
||||||
</div>
|
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{tooltip && <InfoTooltip text={tooltip} />}
|
{tooltip && <InfoTooltip text={tooltip} />}
|
||||||
|
</Row>
|
||||||
|
<Row className="flex-wrap items-end justify-end gap-2">
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
{formatMoney(currentPayout)}
|
||||||
|
</span>
|
||||||
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="flex-wrap items-end justify-end gap-2">
|
)}
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{formatMoney(currentPayout)}
|
|
||||||
</span>
|
|
||||||
<span>(+{currentReturnPercent})</span>
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
|
|
|
@ -12,11 +12,15 @@ export function YesNoSelector(props: {
|
||||||
}) {
|
}) {
|
||||||
const { selected, onSelect, className, btnClassName } = props
|
const { selected, onSelect, className, btnClassName } = props
|
||||||
|
|
||||||
|
const commonClassNames =
|
||||||
|
'inline-flex flex-1 items-center justify-center rounded-3xl border-2 p-2'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('space-x-3', className)}>
|
<Row className={clsx('space-x-3', className)}>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'hover:bg-primary-focus border-primary hover:border-primary-focus inline-flex flex-1 items-center justify-center rounded-lg border-2 p-2 hover:text-white',
|
commonClassNames,
|
||||||
|
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
||||||
selected == 'YES'
|
selected == 'YES'
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
: 'text-primary bg-transparent',
|
: 'text-primary bg-transparent',
|
||||||
|
@ -24,11 +28,12 @@ export function YesNoSelector(props: {
|
||||||
)}
|
)}
|
||||||
onClick={() => onSelect('YES')}
|
onClick={() => onSelect('YES')}
|
||||||
>
|
>
|
||||||
Buy YES
|
YES
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex flex-1 items-center justify-center rounded-lg border-2 border-red-400 p-2 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
commonClassNames,
|
||||||
|
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
||||||
selected == 'NO'
|
selected == 'NO'
|
||||||
? 'bg-red-400 text-white'
|
? 'bg-red-400 text-white'
|
||||||
: 'bg-transparent text-red-400',
|
: 'bg-transparent text-red-400',
|
||||||
|
@ -36,7 +41,7 @@ export function YesNoSelector(props: {
|
||||||
)}
|
)}
|
||||||
onClick={() => onSelect('NO')}
|
onClick={() => onSelect('NO')}
|
||||||
>
|
>
|
||||||
Buy NO
|
NO
|
||||||
</button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
|
@ -50,7 +55,7 @@ export function YesNoCancelSelector(props: {
|
||||||
}) {
|
}) {
|
||||||
const { selected, onSelect } = props
|
const { selected, onSelect } = props
|
||||||
|
|
||||||
const btnClassName = clsx('px-6 flex-1', props.btnClassName)
|
const btnClassName = clsx('px-6 flex-1 rounded-3xl', props.btnClassName)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user