Pseudo numeric market (#609)
* create pseudo-numeric contracts * graph and bet panel for pseudo numeric * pseudo numeric market layout, quick betting * Estimated value * sell panel * fix graph * pseudo numeric resolution * bets tab * redemption for pseudo numeric markets * create log scale market, validation * log scale * create: initial value can't be min or max * don't allow log scale for ranges with negative values (b/c of problem with graph library) * prettier delenda est * graph: handle min value of zero * bet labeling * validation * prettier * pseudo numeric embeds * update disclaimer * validation * validation
This commit is contained in:
parent
cc52bff05e
commit
1a6afaf44f
|
@ -18,15 +18,24 @@ import {
|
|||
getDpmProbabilityAfterSale,
|
||||
} from './calculate-dpm'
|
||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
||||
import { Contract, BinaryContract, FreeResponseContract } from './contract'
|
||||
import {
|
||||
Contract,
|
||||
BinaryContract,
|
||||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
|
||||
export function getProbability(contract: BinaryContract) {
|
||||
export function getProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbability(contract.pool, contract.p)
|
||||
: getDpmProbability(contract.totalShares)
|
||||
}
|
||||
|
||||
export function getInitialProbability(contract: BinaryContract) {
|
||||
export function getInitialProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
if (contract.initialProbability) return contract.initialProbability
|
||||
|
||||
if (contract.mechanism === 'dpm-2' || (contract as any).totalShares)
|
||||
|
@ -65,7 +74,9 @@ export function calculateShares(
|
|||
}
|
||||
|
||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
@ -87,7 +98,9 @@ export function getProbabilityAfterSale(
|
|||
}
|
||||
|
||||
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
@ -96,7 +109,9 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
|||
const outcome = contract.resolution
|
||||
if (!outcome) throw new Error('Contract not resolved')
|
||||
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
@ -142,9 +157,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
const profit = payout + saleValue + redeemed - totalInvested
|
||||
const profitPercent = (profit / totalInvested) * 100
|
||||
|
||||
const hasShares = Object.values(totalShares).some(
|
||||
(shares) => shares > 0
|
||||
)
|
||||
const hasShares = Object.values(totalShares).some((shares) => shares > 0)
|
||||
|
||||
return {
|
||||
invested: Math.max(0, currentInvested),
|
||||
|
|
|
@ -2,9 +2,10 @@ import { Answer } from './answer'
|
|||
import { Fees } from './fees'
|
||||
|
||||
export type AnyMechanism = DPM | CPMM
|
||||
export type AnyOutcomeType = Binary | FreeResponse | Numeric
|
||||
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
| (DPM & Binary)
|
||||
| (DPM & FreeResponse)
|
||||
| (DPM & Numeric)
|
||||
|
@ -33,7 +34,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
isResolved: boolean
|
||||
resolutionTime?: number // When the contract creator resolved the market
|
||||
resolution?: string
|
||||
resolutionProbability?: number,
|
||||
resolutionProbability?: number
|
||||
|
||||
closeEmailsSent?: number
|
||||
|
||||
|
@ -44,7 +45,8 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
collectedFees: Fees
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
export type BinaryContract = Contract & Binary
|
||||
export type PseudoNumericContract = Contract & PseudoNumeric
|
||||
export type NumericContract = Contract & Numeric
|
||||
export type FreeResponseContract = Contract & FreeResponse
|
||||
export type DPMContract = Contract & DPM
|
||||
|
@ -75,6 +77,18 @@ export type Binary = {
|
|||
resolution?: resolution
|
||||
}
|
||||
|
||||
export type PseudoNumeric = {
|
||||
outcomeType: 'PSEUDO_NUMERIC'
|
||||
min: number
|
||||
max: number
|
||||
isLogScale: boolean
|
||||
resolutionValue?: number
|
||||
|
||||
// same as binary market; map everything to probability
|
||||
initialProbability: number
|
||||
resolutionProbability?: number
|
||||
}
|
||||
|
||||
export type FreeResponse = {
|
||||
outcomeType: 'FREE_RESPONSE'
|
||||
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
|
||||
|
@ -94,7 +108,7 @@ export type Numeric = {
|
|||
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const
|
||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC'] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { noFees } from './fees'
|
||||
import { addObjects } from './util/object'
|
||||
|
@ -32,7 +33,7 @@ export type BetInfo = {
|
|||
export const getNewBinaryCpmmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
contract: CPMMBinaryContract,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
loanAmount: number
|
||||
) => {
|
||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
FreeResponse,
|
||||
Numeric,
|
||||
outcomeType,
|
||||
PseudoNumeric,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { parseTags } from './util/parse'
|
||||
|
@ -27,7 +28,8 @@ export function getNewContract(
|
|||
// used for numeric markets
|
||||
bucketCount: number,
|
||||
min: number,
|
||||
max: number
|
||||
max: number,
|
||||
isLogScale: boolean
|
||||
) {
|
||||
const tags = parseTags(
|
||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||
|
@ -37,6 +39,8 @@ export function getNewContract(
|
|||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
@ -111,6 +115,24 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
|||
return system
|
||||
}
|
||||
|
||||
const getPseudoNumericCpmmProps = (
|
||||
initialProb: number,
|
||||
ante: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale: boolean
|
||||
) => {
|
||||
const system: CPMM & PseudoNumeric = {
|
||||
...getBinaryCpmmProps(initialProb, ante),
|
||||
outcomeType: 'PSEUDO_NUMERIC',
|
||||
min,
|
||||
max,
|
||||
isLogScale,
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
const getFreeAnswerProps = (ante: number) => {
|
||||
const system: DPM & FreeResponse = {
|
||||
mechanism: 'dpm-2',
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { sumBy, groupBy, mapValues } from 'lodash'
|
||||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { Contract, CPMMBinaryContract, DPMContract } from './contract'
|
||||
import {
|
||||
Contract,
|
||||
CPMMBinaryContract,
|
||||
DPMContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { Fees } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import {
|
||||
|
@ -56,7 +61,11 @@ export const getPayouts = (
|
|||
},
|
||||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
||||
if (
|
||||
contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
) {
|
||||
return getFixedPayouts(
|
||||
outcome,
|
||||
contract,
|
||||
|
@ -76,7 +85,7 @@ export const getPayouts = (
|
|||
|
||||
export const getFixedPayouts = (
|
||||
outcome: string | undefined,
|
||||
contract: CPMMBinaryContract,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutionProbability?: number
|
||||
|
|
45
common/pseudo-numeric.ts
Normal file
45
common/pseudo-numeric.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { BinaryContract, PseudoNumericContract } from './contract'
|
||||
import { formatLargeNumber, formatPercent } from './util/format'
|
||||
|
||||
export function formatNumericProbability(
|
||||
p: number,
|
||||
contract: PseudoNumericContract
|
||||
) {
|
||||
const value = getMappedValue(contract)(p)
|
||||
return formatLargeNumber(value)
|
||||
}
|
||||
|
||||
export const getMappedValue =
|
||||
(contract: PseudoNumericContract | BinaryContract) => (p: number) => {
|
||||
if (contract.outcomeType === 'BINARY') return p
|
||||
|
||||
const { min, max, isLogScale } = contract
|
||||
|
||||
if (isLogScale) {
|
||||
const logValue = p * Math.log10(max - min)
|
||||
return 10 ** logValue + min
|
||||
}
|
||||
|
||||
return p * (max - min) + min
|
||||
}
|
||||
|
||||
export const getFormattedMappedValue =
|
||||
(contract: PseudoNumericContract | BinaryContract) => (p: number) => {
|
||||
if (contract.outcomeType === 'BINARY') return formatPercent(p)
|
||||
|
||||
const value = getMappedValue(contract)(p)
|
||||
return formatLargeNumber(value)
|
||||
}
|
||||
|
||||
export const getPseudoProbability = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale = false
|
||||
) => {
|
||||
if (isLogScale) {
|
||||
return Math.log10(value - min) / Math.log10(max - min)
|
||||
}
|
||||
|
||||
return (value - min) / (max - min)
|
||||
}
|
|
@ -28,6 +28,7 @@ import { getNewContract } from '../../common/new-contract'
|
|||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||
import { User } from '../../common/user'
|
||||
import { Group, MAX_ID_LENGTH } from '../../common/group'
|
||||
import { getPseudoProbability } from '../../common/pseudo-numeric'
|
||||
|
||||
const bodySchema = z.object({
|
||||
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
||||
|
@ -45,19 +46,31 @@ const binarySchema = z.object({
|
|||
initialProb: z.number().min(1).max(99),
|
||||
})
|
||||
|
||||
const finite = () => z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const numericSchema = z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
min: finite(),
|
||||
max: finite(),
|
||||
initialValue: finite(),
|
||||
isLogScale: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||
const { question, description, tags, closeTime, outcomeType, groupId } =
|
||||
validate(bodySchema, req.body)
|
||||
|
||||
let min, max, initialProb
|
||||
if (outcomeType === 'NUMERIC') {
|
||||
;({ min, max } = validate(numericSchema, req.body))
|
||||
if (max - min <= 0.01) throw new APIError(400, 'Invalid range.')
|
||||
let min, max, initialProb, isLogScale
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||
let initialValue
|
||||
;({ min, max, initialValue, isLogScale } = validate(
|
||||
numericSchema,
|
||||
req.body
|
||||
))
|
||||
if (max - min <= 0.01 || initialValue < min || initialValue > max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
|
||||
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
||||
}
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, req.body))
|
||||
|
@ -121,7 +134,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
tags ?? [],
|
||||
NUMERIC_BUCKET_COUNT,
|
||||
min ?? 0,
|
||||
max ?? 0
|
||||
max ?? 0,
|
||||
isLogScale ?? false
|
||||
)
|
||||
|
||||
if (ante) await chargeUser(user.id, ante, true)
|
||||
|
@ -130,7 +144,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
|
||||
const providerId = user.id
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||
const liquidityDoc = firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.doc()
|
||||
|
|
|
@ -6,8 +6,13 @@ import { Comment } from '../../common/comment'
|
|||
import { Contract } from '../../common/contract'
|
||||
import { DPM_CREATOR_FEE } from '../../common/fees'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||
import {
|
||||
formatLargeNumber,
|
||||
formatMoney,
|
||||
formatPercent,
|
||||
} from '../../common/util/format'
|
||||
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||
|
||||
import { sendTemplateEmail } from './send-email'
|
||||
import { getPrivateUser, getUser } from './utils'
|
||||
|
@ -101,6 +106,17 @@ const toDisplayResolution = (
|
|||
return display || resolution
|
||||
}
|
||||
|
||||
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||
const { resolutionValue } = contract
|
||||
|
||||
return resolutionValue
|
||||
? formatLargeNumber(resolutionValue)
|
||||
: formatNumericProbability(
|
||||
resolutionProbability ?? getProbability(contract),
|
||||
contract
|
||||
)
|
||||
}
|
||||
|
||||
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
||||
if (resolution === 'CANCEL') return 'N/A'
|
||||
|
||||
|
|
|
@ -70,7 +70,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
||||
const { outcome } = validate(binarySchema, req.body)
|
||||
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
||||
} else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') {
|
||||
} else if (
|
||||
(outcomeType == 'BINARY' || outcomeType == 'PSEUDO_NUMERIC') &&
|
||||
mechanism == 'cpmm-1'
|
||||
) {
|
||||
const { outcome } = validate(binarySchema, req.body)
|
||||
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
||||
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||
|
|
|
@ -16,7 +16,11 @@ export const redeemShares = async (userId: string, contractId: string) => {
|
|||
return { status: 'error', message: 'Invalid contract' }
|
||||
|
||||
const contract = contractSnap.data() as Contract
|
||||
if (contract.outcomeType !== 'BINARY' || contract.mechanism !== 'cpmm-1')
|
||||
const { mechanism, outcomeType } = contract
|
||||
if (
|
||||
!(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') ||
|
||||
mechanism !== 'cpmm-1'
|
||||
)
|
||||
return { status: 'success' }
|
||||
|
||||
const betsSnap = await transaction.get(
|
||||
|
|
|
@ -27,7 +27,7 @@ const bodySchema = z.object({
|
|||
|
||||
const binarySchema = z.object({
|
||||
outcome: z.enum(RESOLUTIONS),
|
||||
probabilityInt: z.number().gte(0).lt(100).optional(),
|
||||
probabilityInt: z.number().gte(0).lte(100).optional(),
|
||||
})
|
||||
|
||||
const freeResponseSchema = z.union([
|
||||
|
@ -39,7 +39,7 @@ const freeResponseSchema = z.union([
|
|||
resolutions: z.array(
|
||||
z.object({
|
||||
answer: z.number().int().nonnegative(),
|
||||
pct: z.number().gte(0).lt(100),
|
||||
pct: z.number().gte(0).lte(100),
|
||||
})
|
||||
),
|
||||
}),
|
||||
|
@ -53,7 +53,19 @@ const numericSchema = z.object({
|
|||
value: z.number().optional(),
|
||||
})
|
||||
|
||||
const pseudoNumericSchema = z.union([
|
||||
z.object({
|
||||
outcome: z.literal('CANCEL'),
|
||||
}),
|
||||
z.object({
|
||||
outcome: z.literal('MKT'),
|
||||
value: z.number(),
|
||||
probabilityInt: z.number().gte(0).lte(100),
|
||||
}),
|
||||
])
|
||||
|
||||
const opts = { secrets: ['MAILGUN_KEY'] }
|
||||
|
||||
export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
||||
const { contractId } = validate(bodySchema, req.body)
|
||||
const userId = auth.uid
|
||||
|
@ -221,12 +233,18 @@ const sendResolutionEmails = async (
|
|||
|
||||
function getResolutionParams(contract: Contract, body: string) {
|
||||
const { outcomeType } = contract
|
||||
|
||||
if (outcomeType === 'NUMERIC') {
|
||||
return {
|
||||
...validate(numericSchema, body),
|
||||
resolutions: undefined,
|
||||
probabilityInt: undefined,
|
||||
}
|
||||
} else if (outcomeType === 'PSEUDO_NUMERIC') {
|
||||
return {
|
||||
...validate(pseudoNumericSchema, body),
|
||||
resolutions: undefined,
|
||||
}
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
const freeResponseParams = validate(freeResponseSchema, body)
|
||||
const { outcome } = freeResponseParams
|
||||
|
|
|
@ -3,7 +3,11 @@ import React, { useEffect, useState } from 'react'
|
|||
import { partition, sumBy } from 'lodash'
|
||||
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { BinaryContract, CPMMBinaryContract } from 'common/contract'
|
||||
import {
|
||||
BinaryContract,
|
||||
CPMMBinaryContract,
|
||||
PseudoNumericContract,
|
||||
} from 'common/contract'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
@ -21,7 +25,7 @@ import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
|||
import { sellShares } from 'web/lib/firebase/api-call'
|
||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||
import { InfoTooltip } from './info-tooltip'
|
||||
import { BinaryOutcomeLabel } from './outcome-label'
|
||||
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
||||
import {
|
||||
calculatePayoutAfterCorrectBet,
|
||||
calculateShares,
|
||||
|
@ -35,6 +39,7 @@ import {
|
|||
getCpmmProbability,
|
||||
getCpmmLiquidityFee,
|
||||
} from 'common/calculate-cpmm'
|
||||
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
||||
import { SellRow } from './sell-row'
|
||||
import { useSaveShares } from './use-save-shares'
|
||||
import { SignUpPrompt } from './sign-up-prompt'
|
||||
|
@ -42,7 +47,7 @@ import { isIOS } from 'web/lib/util/device'
|
|||
import { track } from 'web/lib/service/analytics'
|
||||
|
||||
export function BetPanel(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
|
@ -81,7 +86,7 @@ export function BetPanel(props: {
|
|||
}
|
||||
|
||||
export function BetPanelSwitcher(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
title?: string // Set if BetPanel is on a feed modal
|
||||
selected?: 'YES' | 'NO'
|
||||
|
@ -89,7 +94,8 @@ export function BetPanelSwitcher(props: {
|
|||
}) {
|
||||
const { contract, className, title, selected, onBetSuccess } = props
|
||||
|
||||
const { mechanism } = contract
|
||||
const { mechanism, outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const user = useUser()
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
|
@ -122,7 +128,12 @@ export function BetPanelSwitcher(props: {
|
|||
<Row className="items-center justify-between gap-2">
|
||||
<div>
|
||||
You have {formatWithCommas(floorShares)}{' '}
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
||||
{isPseudoNumeric ? (
|
||||
<PseudoNumericOutcomeLabel outcome={sharesOutcome} />
|
||||
) : (
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} />
|
||||
)}{' '}
|
||||
shares
|
||||
</div>
|
||||
|
||||
{tradeType === 'BUY' && (
|
||||
|
@ -190,12 +201,13 @@ export function BetPanelSwitcher(props: {
|
|||
}
|
||||
|
||||
function BuyPanel(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
user: User | null | undefined
|
||||
selected?: 'YES' | 'NO'
|
||||
onBuySuccess?: () => void
|
||||
}) {
|
||||
const { contract, user, selected, onBuySuccess } = props
|
||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||
|
@ -302,6 +314,9 @@ function BuyPanel(props: {
|
|||
: 0)
|
||||
)} ${betChoice ?? 'YES'} shares`
|
||||
: undefined
|
||||
|
||||
const format = getFormattedMappedValue(contract)
|
||||
|
||||
return (
|
||||
<>
|
||||
<YesNoSelector
|
||||
|
@ -309,6 +324,7 @@ function BuyPanel(props: {
|
|||
btnClassName="flex-1"
|
||||
selected={betChoice}
|
||||
onSelect={(choice) => onBetChoice(choice)}
|
||||
isPseudoNumeric={isPseudoNumeric}
|
||||
/>
|
||||
<div className="my-3 text-left text-sm text-gray-500">Amount</div>
|
||||
<BuyAmountInput
|
||||
|
@ -323,11 +339,13 @@ function BuyPanel(props: {
|
|||
|
||||
<Col className="mt-3 w-full gap-3">
|
||||
<Row className="items-center justify-between text-sm">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
<div className="text-gray-500">
|
||||
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||
</div>
|
||||
<div>
|
||||
{formatPercent(initialProb)}
|
||||
{format(initialProb)}
|
||||
<span className="mx-2">→</span>
|
||||
{formatPercent(resultProb)}
|
||||
{format(resultProb)}
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
|
@ -340,6 +358,8 @@ function BuyPanel(props: {
|
|||
<br /> payout if{' '}
|
||||
<BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||
</>
|
||||
) : isPseudoNumeric ? (
|
||||
'Max payout'
|
||||
) : (
|
||||
<>
|
||||
Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||
|
@ -389,7 +409,7 @@ function BuyPanel(props: {
|
|||
}
|
||||
|
||||
export function SellPanel(props: {
|
||||
contract: CPMMBinaryContract
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
userBets: Bet[]
|
||||
shares: number
|
||||
sharesOutcome: 'YES' | 'NO'
|
||||
|
@ -488,6 +508,10 @@ export function SellPanel(props: {
|
|||
}
|
||||
}
|
||||
|
||||
const { outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
const format = getFormattedMappedValue(contract)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AmountInput
|
||||
|
@ -511,11 +535,13 @@ export function SellPanel(props: {
|
|||
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
||||
</Row>
|
||||
<Row className="items-center justify-between">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
<div className="text-gray-500">
|
||||
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||
</div>
|
||||
<div>
|
||||
{formatPercent(initialProb)}
|
||||
{format(initialProb)}
|
||||
<span className="mx-2">→</span>
|
||||
{formatPercent(resultProb)}
|
||||
{format(resultProb)}
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx'
|
|||
|
||||
import { BetPanelSwitcher } from './bet-panel'
|
||||
import { YesNoSelector } from './yes-no-selector'
|
||||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { Modal } from './layout/modal'
|
||||
import { SellButton } from './sell-button'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
|
@ -12,7 +12,7 @@ import { useSaveShares } from './use-save-shares'
|
|||
|
||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||
export default function BetRow(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
btnClassName?: string
|
||||
betPanelClassName?: string
|
||||
|
@ -32,6 +32,7 @@ export default function BetRow(props: {
|
|||
return (
|
||||
<>
|
||||
<YesNoSelector
|
||||
isPseudoNumeric={contract.outcomeType === 'PSEUDO_NUMERIC'}
|
||||
className={clsx('justify-end', className)}
|
||||
btnClassName={clsx('btn-sm w-24', btnClassName)}
|
||||
onSelect={(choice) => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useUserBets } from 'web/hooks/use-user-bets'
|
|||
import { Bet } from 'web/lib/firebase/bets'
|
||||
import { User } from 'web/lib/firebase/users'
|
||||
import {
|
||||
formatLargeNumber,
|
||||
formatMoney,
|
||||
formatPercent,
|
||||
formatWithCommas,
|
||||
|
@ -40,6 +41,7 @@ import {
|
|||
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
|
||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
||||
import { NumericContract } from 'common/contract'
|
||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { SellSharesModal } from './sell-modal'
|
||||
|
||||
|
@ -366,6 +368,7 @@ export function BetsSummary(props: {
|
|||
const { contract, isYourBets, className } = props
|
||||
const { resolution, closeTime, outcomeType, mechanism } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
const isCpmm = mechanism === 'cpmm-1'
|
||||
const isClosed = closeTime && Date.now() > closeTime
|
||||
|
||||
|
@ -427,6 +430,25 @@ export function BetsSummary(props: {
|
|||
</div>
|
||||
</Col>
|
||||
</>
|
||||
) : isPseudoNumeric ? (
|
||||
<>
|
||||
<Col>
|
||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||
Payout if {'>='} {formatLargeNumber(contract.max)}
|
||||
</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{formatMoney(yesWinnings)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||
Payout if {'<='} {formatLargeNumber(contract.min)}
|
||||
</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{formatMoney(noWinnings)}
|
||||
</div>
|
||||
</Col>
|
||||
</>
|
||||
) : (
|
||||
<Col>
|
||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||
|
@ -507,13 +529,15 @@ export function ContractBetsTable(props: {
|
|||
const { isResolved, mechanism, outcomeType } = contract
|
||||
const isCPMM = mechanism === 'cpmm-1'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
return (
|
||||
<div className={clsx('overflow-x-auto', className)}>
|
||||
{amountRedeemed > 0 && (
|
||||
<>
|
||||
<div className="pl-2 text-sm text-gray-500">
|
||||
{amountRedeemed} YES shares and {amountRedeemed} NO shares
|
||||
{amountRedeemed} {isPseudoNumeric ? 'HIGHER' : 'YES'} shares and{' '}
|
||||
{amountRedeemed} {isPseudoNumeric ? 'LOWER' : 'NO'} shares
|
||||
automatically redeemed for {formatMoney(amountRedeemed)}.
|
||||
</div>
|
||||
<Spacer h={4} />
|
||||
|
@ -541,7 +565,7 @@ export function ContractBetsTable(props: {
|
|||
)}
|
||||
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
||||
<th>Shares</th>
|
||||
<th>Probability</th>
|
||||
{!isPseudoNumeric && <th>Probability</th>}
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -585,6 +609,7 @@ function BetRow(props: {
|
|||
|
||||
const isCPMM = mechanism === 'cpmm-1'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const saleAmount = saleBet?.sale?.amount
|
||||
|
||||
|
@ -628,14 +653,18 @@ function BetRow(props: {
|
|||
truncate="short"
|
||||
/>
|
||||
)}
|
||||
{isPseudoNumeric &&
|
||||
' than ' + formatNumericProbability(bet.probAfter, contract)}
|
||||
</td>
|
||||
<td>{formatMoney(Math.abs(amount))}</td>
|
||||
{!isCPMM && !isNumeric && <td>{saleDisplay}</td>}
|
||||
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||
<td>{formatWithCommas(Math.abs(shares))}</td>
|
||||
<td>
|
||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||
</td>
|
||||
{!isPseudoNumeric && (
|
||||
<td>
|
||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||
</td>
|
||||
)}
|
||||
<td>{dayjs(createdTime).format('MMM D, h:mma')}</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
BinaryContract,
|
||||
FreeResponseContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from 'common/contract'
|
||||
import {
|
||||
AnswerLabel,
|
||||
|
@ -16,7 +17,11 @@ import {
|
|||
CancelLabel,
|
||||
FreeResponseOutcomeLabel,
|
||||
} from '../outcome-label'
|
||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
||||
import {
|
||||
getOutcomeProbability,
|
||||
getProbability,
|
||||
getTopAnswer,
|
||||
} from 'common/calculate'
|
||||
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
|
||||
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||
import { QuickBet, ProbBar, getColor } from './quick-bet'
|
||||
|
@ -24,6 +29,7 @@ import { useContractWithPreload } from 'web/hooks/use-contract'
|
|||
import { useUser } from 'web/hooks/use-user'
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { trackCallback } from 'web/lib/service/analytics'
|
||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
|
||||
export function ContractCard(props: {
|
||||
contract: Contract
|
||||
|
@ -131,6 +137,13 @@ export function ContractCard(props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||
<PseudoNumericResolutionOrExpectation
|
||||
className="items-center"
|
||||
contract={contract}
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericResolutionOrExpectation
|
||||
className="items-center"
|
||||
|
@ -270,7 +283,9 @@ export function NumericResolutionOrExpectation(props: {
|
|||
{resolution === 'CANCEL' ? (
|
||||
<CancelLabel />
|
||||
) : (
|
||||
<div className="text-blue-400">{resolutionValue}</div>
|
||||
<div className="text-blue-400">
|
||||
{formatLargeNumber(resolutionValue)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
@ -284,3 +299,42 @@ export function NumericResolutionOrExpectation(props: {
|
|||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function PseudoNumericResolutionOrExpectation(props: {
|
||||
contract: PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
const { resolution, resolutionValue, resolutionProbability } = contract
|
||||
const textColor = `text-blue-400`
|
||||
|
||||
return (
|
||||
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||
{resolution ? (
|
||||
<>
|
||||
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
||||
|
||||
{resolution === 'CANCEL' ? (
|
||||
<CancelLabel />
|
||||
) : (
|
||||
<div className="text-blue-400">
|
||||
{resolutionValue
|
||||
? formatLargeNumber(resolutionValue)
|
||||
: formatNumericProbability(
|
||||
resolutionProbability ?? 0,
|
||||
contract
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={clsx('text-3xl', textColor)}>
|
||||
{formatNumericProbability(getProbability(contract), contract)}
|
||||
</div>
|
||||
<div className={clsx('text-base', textColor)}>expected</div>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
FreeResponseResolutionOrChance,
|
||||
BinaryResolutionOrChance,
|
||||
NumericResolutionOrExpectation,
|
||||
PseudoNumericResolutionOrExpectation,
|
||||
} from './contract-card'
|
||||
import { Bet } from 'common/bet'
|
||||
import BetRow from '../bet-row'
|
||||
|
@ -32,6 +33,7 @@ export const ContractOverview = (props: {
|
|||
const user = useUser()
|
||||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
return (
|
||||
<Col className={clsx('mb-6', className)}>
|
||||
|
@ -49,6 +51,13 @@ export const ContractOverview = (props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
{isPseudoNumeric && (
|
||||
<PseudoNumericResolutionOrExpectation
|
||||
contract={contract}
|
||||
className="hidden items-end xl:flex"
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericResolutionOrExpectation
|
||||
contract={contract}
|
||||
|
@ -61,6 +70,11 @@ export const ContractOverview = (props: {
|
|||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<BinaryResolutionOrChance contract={contract} />
|
||||
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
</Row>
|
||||
) : isPseudoNumeric ? (
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
</Row>
|
||||
) : (
|
||||
|
@ -86,7 +100,9 @@ export const ContractOverview = (props: {
|
|||
/>
|
||||
</Col>
|
||||
<Spacer h={4} />
|
||||
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '}
|
||||
{(isBinary || isPseudoNumeric) && (
|
||||
<ContractProbGraph contract={contract} bets={bets} />
|
||||
)}{' '}
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
<AnswersGraph contract={contract} bets={bets} />
|
||||
)}
|
||||
|
|
|
@ -5,16 +5,20 @@ import dayjs from 'dayjs'
|
|||
import { memo } from 'react'
|
||||
import { Bet } from 'common/bet'
|
||||
import { getInitialProbability } from 'common/calculate'
|
||||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
import { getMappedValue } from 'common/pseudo-numeric'
|
||||
import { formatLargeNumber } from 'common/util/format'
|
||||
|
||||
export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
}) {
|
||||
const { contract, height } = props
|
||||
const { resolutionTime, closeTime } = contract
|
||||
const { resolutionTime, closeTime, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isLogScale = outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale
|
||||
|
||||
const bets = props.bets.filter((bet) => !bet.isAnte && !bet.isRedemption)
|
||||
|
||||
|
@ -24,7 +28,10 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
contract.createdTime,
|
||||
...bets.map((bet) => bet.createdTime),
|
||||
].map((time) => new Date(time))
|
||||
const probs = [startProb, ...bets.map((bet) => bet.probAfter)]
|
||||
|
||||
const f = getMappedValue(contract)
|
||||
|
||||
const probs = [startProb, ...bets.map((bet) => bet.probAfter)].map(f)
|
||||
|
||||
const isClosed = !!closeTime && Date.now() > closeTime
|
||||
const latestTime = dayjs(
|
||||
|
@ -39,7 +46,11 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
times.push(latestTime.toDate())
|
||||
probs.push(probs[probs.length - 1])
|
||||
|
||||
const yTickValues = [0, 25, 50, 75, 100]
|
||||
const quartiles = [0, 25, 50, 75, 100]
|
||||
|
||||
const yTickValues = isBinary
|
||||
? quartiles
|
||||
: quartiles.map((x) => x / 100).map(f)
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
|
@ -55,9 +66,13 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
const totalPoints = width ? (width > 800 ? 300 : 50) : 1
|
||||
|
||||
const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints
|
||||
|
||||
const points: { x: Date; y: number }[] = []
|
||||
const s = isBinary ? 100 : 1
|
||||
const c = isLogScale && contract.min === 0 ? 1 : 0
|
||||
|
||||
for (let i = 0; i < times.length - 1; i++) {
|
||||
points[points.length] = { x: times[i], y: probs[i] * 100 }
|
||||
points[points.length] = { x: times[i], y: s * probs[i] + c }
|
||||
const numPoints: number = Math.floor(
|
||||
dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep
|
||||
)
|
||||
|
@ -69,17 +84,23 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
x: dayjs(times[i])
|
||||
.add(thisTimeStep * n, 'ms')
|
||||
.toDate(),
|
||||
y: probs[i] * 100,
|
||||
y: s * probs[i] + c,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data = [{ id: 'Yes', data: points, color: '#11b981' }]
|
||||
const data = [
|
||||
{ id: 'Yes', data: points, color: isBinary ? '#11b981' : '#5fa5f9' },
|
||||
]
|
||||
|
||||
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
||||
const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime)
|
||||
|
||||
const formatter = isBinary
|
||||
? formatPercent
|
||||
: (x: DatumValue) => formatLargeNumber(+x.valueOf())
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-visible"
|
||||
|
@ -87,12 +108,20 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
yScale={{ min: 0, max: 100, type: 'linear' }}
|
||||
yFormat={formatPercent}
|
||||
yScale={
|
||||
isBinary
|
||||
? { min: 0, max: 100, type: 'linear' }
|
||||
: {
|
||||
min: contract.min + c,
|
||||
max: contract.max + c,
|
||||
type: contract.isLogScale ? 'log' : 'linear',
|
||||
}
|
||||
}
|
||||
yFormat={formatter}
|
||||
gridYValues={yTickValues}
|
||||
axisLeft={{
|
||||
tickValues: yTickValues,
|
||||
format: formatPercent,
|
||||
format: formatter,
|
||||
}}
|
||||
xScale={{
|
||||
type: 'time',
|
||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx'
|
|||
import {
|
||||
getOutcomeProbability,
|
||||
getOutcomeProbabilityAfterBet,
|
||||
getProbability,
|
||||
getTopAnswer,
|
||||
} from 'common/calculate'
|
||||
import { getExpectedValue } from 'common/calculate-dpm'
|
||||
|
@ -25,18 +26,18 @@ import { useSaveShares } from '../use-save-shares'
|
|||
import { sellShares } from 'web/lib/firebase/api-call'
|
||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
|
||||
const BET_SIZE = 10
|
||||
|
||||
export function QuickBet(props: { contract: Contract; user: User }) {
|
||||
const { contract, user } = props
|
||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
||||
const { mechanism, outcomeType } = contract
|
||||
const isCpmm = mechanism === 'cpmm-1'
|
||||
|
||||
const userBets = useUserContractBets(user.id, contract.id)
|
||||
const topAnswer =
|
||||
contract.outcomeType === 'FREE_RESPONSE'
|
||||
? getTopAnswer(contract)
|
||||
: undefined
|
||||
outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
|
||||
|
||||
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||
|
@ -45,9 +46,9 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
topAnswer?.number.toString() || undefined
|
||||
)
|
||||
const hasUpShares =
|
||||
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
|
||||
yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC')
|
||||
const hasDownShares =
|
||||
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
|
||||
noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC'
|
||||
|
||||
const [upHover, setUpHover] = useState(false)
|
||||
const [downHover, setDownHover] = useState(false)
|
||||
|
@ -130,25 +131,6 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
})
|
||||
}
|
||||
|
||||
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||
if (contract.outcomeType === 'BINARY') {
|
||||
return direction === 'UP' ? 'YES' : 'NO'
|
||||
}
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
// TODO: Implement shorting of free response answers
|
||||
if (direction === 'DOWN') {
|
||||
throw new Error("Can't bet against free response answers")
|
||||
}
|
||||
return getTopAnswer(contract)?.id
|
||||
}
|
||||
if (contract.outcomeType === 'NUMERIC') {
|
||||
// TODO: Ideally an 'UP' bet would be a uniform bet between [current, max]
|
||||
throw new Error("Can't quick bet on numeric markets")
|
||||
}
|
||||
}
|
||||
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
return (
|
||||
<Col
|
||||
className={clsx(
|
||||
|
@ -173,14 +155,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
<TriangleFillIcon
|
||||
className={clsx(
|
||||
'mx-auto h-5 w-5',
|
||||
upHover ? textColor : 'text-gray-400'
|
||||
upHover ? 'text-green-500' : 'text-gray-400'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TriangleFillIcon
|
||||
className={clsx(
|
||||
'mx-auto h-5 w-5',
|
||||
upHover ? textColor : 'text-gray-200'
|
||||
upHover ? 'text-green-500' : 'text-gray-200'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
@ -189,7 +171,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
||||
|
||||
{/* Down bet triangle */}
|
||||
{contract.outcomeType !== 'BINARY' ? (
|
||||
{outcomeType !== 'BINARY' && outcomeType !== 'PSEUDO_NUMERIC' ? (
|
||||
<div>
|
||||
<div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div>
|
||||
<TriangleDownFillIcon
|
||||
|
@ -254,6 +236,25 @@ export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||
const { outcomeType } = contract
|
||||
|
||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||
return direction === 'UP' ? 'YES' : 'NO'
|
||||
}
|
||||
if (outcomeType === 'FREE_RESPONSE') {
|
||||
// TODO: Implement shorting of free response answers
|
||||
if (direction === 'DOWN') {
|
||||
throw new Error("Can't bet against free response answers")
|
||||
}
|
||||
return getTopAnswer(contract)?.id
|
||||
}
|
||||
if (outcomeType === 'NUMERIC') {
|
||||
// TODO: Ideally an 'UP' bet would be a uniform bet between [current, max]
|
||||
throw new Error("Can't quick bet on numeric markets")
|
||||
}
|
||||
}
|
||||
|
||||
function QuickOutcomeView(props: {
|
||||
contract: Contract
|
||||
previewProb?: number
|
||||
|
@ -261,9 +262,16 @@ function QuickOutcomeView(props: {
|
|||
}) {
|
||||
const { contract, previewProb, caption } = props
|
||||
const { outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
// If there's a preview prob, display that instead of the current prob
|
||||
const override =
|
||||
previewProb === undefined ? undefined : formatPercent(previewProb)
|
||||
previewProb === undefined
|
||||
? undefined
|
||||
: isPseudoNumeric
|
||||
? formatNumericProbability(previewProb, contract)
|
||||
: formatPercent(previewProb)
|
||||
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
let display: string | undefined
|
||||
|
@ -271,6 +279,9 @@ function QuickOutcomeView(props: {
|
|||
case 'BINARY':
|
||||
display = getBinaryProbPercent(contract)
|
||||
break
|
||||
case 'PSEUDO_NUMERIC':
|
||||
display = formatNumericProbability(getProbability(contract), contract)
|
||||
break
|
||||
case 'NUMERIC':
|
||||
display = formatLargeNumber(getExpectedValue(contract))
|
||||
break
|
||||
|
@ -295,11 +306,15 @@ function QuickOutcomeView(props: {
|
|||
// 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 } = contract
|
||||
return resolution
|
||||
const { outcomeType, resolution, resolutionProbability } = contract
|
||||
return resolutionProbability
|
||||
? resolutionProbability
|
||||
: resolution
|
||||
? 1
|
||||
: outcomeType === 'BINARY'
|
||||
? getBinaryProb(contract)
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getProbability(contract)
|
||||
: outcomeType === 'FREE_RESPONSE'
|
||||
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
||||
: outcomeType === 'NUMERIC'
|
||||
|
@ -316,7 +331,8 @@ function getNumericScale(contract: NumericContract) {
|
|||
export function getColor(contract: Contract) {
|
||||
// TODO: Try injecting a gradient here
|
||||
// return 'primary'
|
||||
const { resolution } = contract
|
||||
const { resolution, outcomeType } = contract
|
||||
|
||||
if (resolution) {
|
||||
return (
|
||||
OUTCOME_TO_COLOR[resolution as resolution] ??
|
||||
|
@ -325,6 +341,8 @@ export function getColor(contract: Contract) {
|
|||
)
|
||||
}
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC') return 'blue-400'
|
||||
|
||||
if ((contract.closeTime ?? Infinity) < Date.now()) {
|
||||
return 'gray-400'
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ import { Row } from 'web/components/layout/row'
|
|||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||
import clsx from 'clsx'
|
||||
import { UsersIcon } from '@heroicons/react/solid'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { formatMoney, formatPercent } from 'common/util/format'
|
||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||
import React, { Fragment } from 'react'
|
||||
import { uniqBy, partition, sumBy, groupBy } from 'lodash'
|
||||
import { JoinSpans } from 'web/components/join-spans'
|
||||
import { UserLink } from '../user-page'
|
||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
|
||||
export function FeedBet(props: {
|
||||
contract: Contract
|
||||
|
@ -75,6 +76,8 @@ export function BetStatusText(props: {
|
|||
hideOutcome?: boolean
|
||||
}) {
|
||||
const { bet, contract, bettor, isSelf, hideOutcome } = props
|
||||
const { outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
const { amount, outcome, createdTime } = bet
|
||||
|
||||
const bought = amount >= 0 ? 'bought' : 'sold'
|
||||
|
@ -97,7 +100,10 @@ export function BetStatusText(props: {
|
|||
value={(bet as any).value}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
/>{' '}
|
||||
{isPseudoNumeric
|
||||
? ' than ' + formatNumericProbability(bet.probAfter, contract)
|
||||
: ' at ' + formatPercent(bet.probAfter)}
|
||||
</>
|
||||
)}
|
||||
<RelativeTimestamp time={createdTime} />
|
||||
|
|
|
@ -6,13 +6,14 @@ import { User } from 'web/lib/firebase/users'
|
|||
import { NumberCancelSelector } from './yes-no-selector'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { ResolveConfirmationButton } from './confirmation-button'
|
||||
import { NumericContract, PseudoNumericContract } from 'common/contract'
|
||||
import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
|
||||
import { NumericContract } from 'common/contract'
|
||||
import { BucketInput } from './bucket-input'
|
||||
import { getPseudoProbability } from 'common/pseudo-numeric'
|
||||
|
||||
export function NumericResolutionPanel(props: {
|
||||
creator: User
|
||||
contract: NumericContract
|
||||
contract: NumericContract | PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
useEffect(() => {
|
||||
|
@ -21,6 +22,7 @@ export function NumericResolutionPanel(props: {
|
|||
}, [])
|
||||
|
||||
const { contract, className } = props
|
||||
const { min, max, outcomeType } = contract
|
||||
|
||||
const [outcomeMode, setOutcomeMode] = useState<
|
||||
'NUMBER' | 'CANCEL' | undefined
|
||||
|
@ -32,15 +34,32 @@ export function NumericResolutionPanel(props: {
|
|||
const [error, setError] = useState<string | undefined>(undefined)
|
||||
|
||||
const resolve = async () => {
|
||||
const finalOutcome = outcomeMode === 'NUMBER' ? outcome : 'CANCEL'
|
||||
const finalOutcome =
|
||||
outcomeMode === 'CANCEL'
|
||||
? 'CANCEL'
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? 'MKT'
|
||||
: 'NUMBER'
|
||||
if (outcomeMode === undefined || finalOutcome === undefined) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
const boundedValue = Math.max(Math.min(max, value ?? 0), min)
|
||||
|
||||
const probabilityInt =
|
||||
100 *
|
||||
getPseudoProbability(
|
||||
boundedValue,
|
||||
min,
|
||||
max,
|
||||
outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale
|
||||
)
|
||||
|
||||
try {
|
||||
const result = await resolveMarket({
|
||||
outcome: finalOutcome,
|
||||
value,
|
||||
probabilityInt,
|
||||
contractId: contract.id,
|
||||
})
|
||||
console.log('resolved', outcome, 'result:', result)
|
||||
|
@ -77,7 +96,7 @@ export function NumericResolutionPanel(props: {
|
|||
|
||||
{outcomeMode === 'NUMBER' && (
|
||||
<BucketInput
|
||||
contract={contract}
|
||||
contract={contract as any}
|
||||
isSubmitting={isSubmitting}
|
||||
onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
|
||||
/>
|
||||
|
|
|
@ -19,11 +19,15 @@ export function OutcomeLabel(props: {
|
|||
value?: number
|
||||
}) {
|
||||
const { outcome, contract, truncate, value } = props
|
||||
const { outcomeType } = contract
|
||||
|
||||
if (contract.outcomeType === 'BINARY')
|
||||
if (outcomeType === 'PSEUDO_NUMERIC')
|
||||
return <PseudoNumericOutcomeLabel outcome={outcome as any} />
|
||||
|
||||
if (outcomeType === 'BINARY')
|
||||
return <BinaryOutcomeLabel outcome={outcome as any} />
|
||||
|
||||
if (contract.outcomeType === 'NUMERIC')
|
||||
if (outcomeType === 'NUMERIC')
|
||||
return (
|
||||
<span className="text-blue-500">
|
||||
{value ?? getValueFromBucket(outcome, contract)}
|
||||
|
@ -49,6 +53,15 @@ export function BinaryOutcomeLabel(props: { outcome: resolution }) {
|
|||
return <CancelLabel />
|
||||
}
|
||||
|
||||
export function PseudoNumericOutcomeLabel(props: { outcome: resolution }) {
|
||||
const { outcome } = props
|
||||
|
||||
if (outcome === 'YES') return <HigherLabel />
|
||||
if (outcome === 'NO') return <LowerLabel />
|
||||
if (outcome === 'MKT') return <ProbLabel />
|
||||
return <CancelLabel />
|
||||
}
|
||||
|
||||
export function BinaryContractOutcomeLabel(props: {
|
||||
contract: BinaryContract
|
||||
resolution: resolution
|
||||
|
@ -98,6 +111,14 @@ export function YesLabel() {
|
|||
return <span className="text-primary">YES</span>
|
||||
}
|
||||
|
||||
export function HigherLabel() {
|
||||
return <span className="text-primary">HIGHER</span>
|
||||
}
|
||||
|
||||
export function LowerLabel() {
|
||||
return <span className="text-red-400">LOWER</span>
|
||||
}
|
||||
|
||||
export function NoLabel() {
|
||||
return <span className="text-red-400">NO</span>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||
import { useState } from 'react'
|
||||
|
@ -7,7 +7,7 @@ import clsx from 'clsx'
|
|||
import { SellSharesModal } from './sell-modal'
|
||||
|
||||
export function SellButton(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
user: User | null | undefined
|
||||
sharesOutcome: 'YES' | 'NO' | undefined
|
||||
shares: number
|
||||
|
@ -16,7 +16,8 @@ export function SellButton(props: {
|
|||
const { contract, user, sharesOutcome, shares, panelClassName } = props
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
const [showSellModal, setShowSellModal] = useState(false)
|
||||
const { mechanism } = contract
|
||||
const { mechanism, outcomeType } = contract
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
||||
return (
|
||||
|
@ -32,7 +33,10 @@ export function SellButton(props: {
|
|||
)}
|
||||
onClick={() => setShowSellModal(true)}
|
||||
>
|
||||
{'Sell ' + sharesOutcome}
|
||||
Sell{' '}
|
||||
{isPseudoNumeric
|
||||
? { YES: 'HIGH', NO: 'LOW' }[sharesOutcome]
|
||||
: sharesOutcome}
|
||||
</button>
|
||||
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
||||
{'(' + Math.floor(shares) + ' shares)'}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { Bet } from 'common/bet'
|
||||
import { User } from 'common/user'
|
||||
import { Modal } from './layout/modal'
|
||||
|
@ -11,7 +11,7 @@ import clsx from 'clsx'
|
|||
|
||||
export function SellSharesModal(props: {
|
||||
className?: string
|
||||
contract: CPMMBinaryContract
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
userBets: Bet[]
|
||||
shares: number
|
||||
sharesOutcome: 'YES' | 'NO'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BinaryContract } from 'common/contract'
|
||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
import { useState } from 'react'
|
||||
import { Col } from './layout/col'
|
||||
|
@ -10,7 +10,7 @@ import { useSaveShares } from './use-save-shares'
|
|||
import { SellSharesModal } from './sell-modal'
|
||||
|
||||
export function SellRow(props: {
|
||||
contract: BinaryContract
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
user: User | null | undefined
|
||||
className?: string
|
||||
}) {
|
||||
|
|
|
@ -12,6 +12,7 @@ export function YesNoSelector(props: {
|
|||
btnClassName?: string
|
||||
replaceYesButton?: React.ReactNode
|
||||
replaceNoButton?: React.ReactNode
|
||||
isPseudoNumeric?: boolean
|
||||
}) {
|
||||
const {
|
||||
selected,
|
||||
|
@ -20,6 +21,7 @@ export function YesNoSelector(props: {
|
|||
btnClassName,
|
||||
replaceNoButton,
|
||||
replaceYesButton,
|
||||
isPseudoNumeric,
|
||||
} = props
|
||||
|
||||
const commonClassNames =
|
||||
|
@ -41,7 +43,7 @@ export function YesNoSelector(props: {
|
|||
)}
|
||||
onClick={() => onSelect('YES')}
|
||||
>
|
||||
Bet YES
|
||||
{isPseudoNumeric ? 'HIGHER' : 'Bet YES'}
|
||||
</button>
|
||||
)}
|
||||
{replaceNoButton ? (
|
||||
|
@ -58,7 +60,7 @@ export function YesNoSelector(props: {
|
|||
)}
|
||||
onClick={() => onSelect('NO')}
|
||||
>
|
||||
Bet NO
|
||||
{isPseudoNumeric ? 'LOWER' : 'Bet NO'}
|
||||
</button>
|
||||
)}
|
||||
</Row>
|
||||
|
|
|
@ -144,10 +144,12 @@ export function ContractPageContent(
|
|||
|
||||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const allowTrade = tradingAllowed(contract)
|
||||
const allowResolve = !isResolved && isCreator && !!user
|
||||
const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve)
|
||||
const hasSidePanel =
|
||||
(isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
|
||||
|
||||
const ogCardProps = getOpenGraphProps(contract)
|
||||
|
||||
|
@ -170,7 +172,7 @@ export function ContractPageContent(
|
|||
<BetPanel className="hidden xl:flex" contract={contract} />
|
||||
))}
|
||||
{allowResolve &&
|
||||
(isNumeric ? (
|
||||
(isNumeric || isPseudoNumeric ? (
|
||||
<NumericResolutionPanel creator={user} contract={contract} />
|
||||
) : (
|
||||
<ResolutionPanel creator={user} contract={contract} />
|
||||
|
@ -210,10 +212,11 @@ export function ContractPageContent(
|
|||
)}
|
||||
|
||||
<ContractOverview contract={contract} bets={bets} />
|
||||
|
||||
{isNumeric && (
|
||||
<AlertBox
|
||||
title="Warning"
|
||||
text="Numeric markets were introduced as an experimental feature and are now deprecated."
|
||||
text="Distributional numeric markets were introduced as an experimental feature and are now deprecated."
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -85,8 +85,12 @@ export function NewContract(props: {
|
|||
const { creator, question, groupId } = props
|
||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||
const [initialProb] = useState(50)
|
||||
|
||||
const [minString, setMinString] = useState('')
|
||||
const [maxString, setMaxString] = useState('')
|
||||
const [isLogScale, setIsLogScale] = useState(false)
|
||||
const [initialValueString, setInitialValueString] = useState('')
|
||||
|
||||
const [description, setDescription] = useState('')
|
||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||
// const tags = parseWordsAsTags(tagText)
|
||||
|
@ -129,6 +133,18 @@ export function NewContract(props: {
|
|||
|
||||
const min = minString ? parseFloat(minString) : undefined
|
||||
const max = maxString ? parseFloat(maxString) : undefined
|
||||
const initialValue = initialValueString
|
||||
? parseFloat(initialValueString)
|
||||
: undefined
|
||||
|
||||
const adjustIsLog = () => {
|
||||
if (min === undefined || max === undefined) return
|
||||
const lengthDiff = Math.log10(max - min)
|
||||
if (lengthDiff > 2) {
|
||||
setIsLogScale(true)
|
||||
}
|
||||
}
|
||||
|
||||
// get days from today until the end of this year:
|
||||
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
|
||||
|
||||
|
@ -145,13 +161,16 @@ export function NewContract(props: {
|
|||
// closeTime must be in the future
|
||||
closeTime &&
|
||||
closeTime > Date.now() &&
|
||||
(outcomeType !== 'NUMERIC' ||
|
||||
(outcomeType !== 'PSEUDO_NUMERIC' ||
|
||||
(min !== undefined &&
|
||||
max !== undefined &&
|
||||
initialValue !== undefined &&
|
||||
isFinite(min) &&
|
||||
isFinite(max) &&
|
||||
min < max &&
|
||||
max - min > 0.01))
|
||||
max - min > 0.01 &&
|
||||
min < initialValue &&
|
||||
initialValue < max))
|
||||
|
||||
function setCloseDateInDays(days: number) {
|
||||
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
||||
|
@ -175,6 +194,8 @@ export function NewContract(props: {
|
|||
closeTime,
|
||||
min,
|
||||
max,
|
||||
initialValue,
|
||||
isLogScale: (min ?? 0) < 0 ? false : isLogScale,
|
||||
groupId: selectedGroup?.id,
|
||||
tags: category ? [category] : undefined,
|
||||
})
|
||||
|
@ -220,6 +241,7 @@ export function NewContract(props: {
|
|||
choicesMap={{
|
||||
'Yes / No': 'BINARY',
|
||||
'Free response': 'FREE_RESPONSE',
|
||||
Numeric: 'PSEUDO_NUMERIC',
|
||||
}}
|
||||
isSubmitting={isSubmitting}
|
||||
className={'col-span-4'}
|
||||
|
@ -232,38 +254,89 @@ export function NewContract(props: {
|
|||
|
||||
<Spacer h={6} />
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<div className="form-control items-start">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Range</span>
|
||||
<InfoTooltip text="The minimum and maximum numbers across the numeric range." />
|
||||
</label>
|
||||
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||
<>
|
||||
<div className="form-control mb-2 items-start">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Range</span>
|
||||
<InfoTooltip text="The minimum and maximum numbers across the numeric range." />
|
||||
</label>
|
||||
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MIN"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMinString(e.target.value)}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={minString ?? ''}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MAX"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMaxString(e.target.value)}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={maxString}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MIN"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMinString(e.target.value)}
|
||||
onBlur={adjustIsLog}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={minString ?? ''}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MAX"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMaxString(e.target.value)}
|
||||
onBlur={adjustIsLog}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={maxString}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{!(min !== undefined && min < 0) && (
|
||||
<Row className="mt-1 ml-2 mb-2 items-center">
|
||||
<span className="mr-2 text-sm">Log scale</span>{' '}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isLogScale}
|
||||
onChange={() => setIsLogScale(!isLogScale)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{min !== undefined && max !== undefined && min >= max && (
|
||||
<div className="mt-2 mb-2 text-sm text-red-500">
|
||||
The maximum value must be greater than the minimum.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-control mb-2 items-start">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Initial value</span>
|
||||
<InfoTooltip text="The starting value for this market. Should be in between min and max values." />
|
||||
</label>
|
||||
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="Initial value"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setInitialValueString(e.target.value)}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={initialValueString ?? ''}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{initialValue !== undefined &&
|
||||
min !== undefined &&
|
||||
max !== undefined &&
|
||||
min < max &&
|
||||
(initialValue <= min || initialValue >= max) && (
|
||||
<div className="mt-2 mb-2 text-sm text-red-500">
|
||||
Initial value must be in between {min} and {max}.{' '}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-control max-w-[265px] items-start">
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
BinaryResolutionOrChance,
|
||||
FreeResponseResolutionOrChance,
|
||||
NumericResolutionOrExpectation,
|
||||
PseudoNumericResolutionOrExpectation,
|
||||
} from 'web/components/contract/contract-card'
|
||||
import { ContractDetails } from 'web/components/contract/contract-details'
|
||||
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph'
|
||||
|
@ -79,6 +80,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
const { question, outcomeType } = contract
|
||||
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const href = `https://${DOMAIN}${contractPath(contract)}`
|
||||
|
||||
|
@ -110,13 +112,18 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
|
||||
{isBinary && (
|
||||
<Row className="items-center gap-4">
|
||||
{/* this fails typechecking, but it doesn't explode because we will
|
||||
never */}
|
||||
<BetRow contract={contract as any} betPanelClassName="scale-75" />
|
||||
<BetRow contract={contract} betPanelClassName="scale-75" />
|
||||
<BinaryResolutionOrChance contract={contract} />
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{isPseudoNumeric && (
|
||||
<Row className="items-center gap-4">
|
||||
<BetRow contract={contract} betPanelClassName="scale-75" />
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
<FreeResponseResolutionOrChance
|
||||
contract={contract}
|
||||
|
@ -133,7 +140,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
</div>
|
||||
|
||||
<div className="mx-1" style={{ paddingBottom }}>
|
||||
{isBinary && (
|
||||
{(isBinary || isPseudoNumeric) && (
|
||||
<ContractProbGraph
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
|
|
Loading…
Reference in New Issue
Block a user