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:
mantikoros 2022-07-02 14:37:59 -05:00 committed by GitHub
parent cc52bff05e
commit 1a6afaf44f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 623 additions and 156 deletions

View File

@ -18,15 +18,24 @@ import {
getDpmProbabilityAfterSale, getDpmProbabilityAfterSale,
} from './calculate-dpm' } from './calculate-dpm'
import { calculateFixedPayout } from './calculate-fixed-payouts' 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' return contract.mechanism === 'cpmm-1'
? getCpmmProbability(contract.pool, contract.p) ? getCpmmProbability(contract.pool, contract.p)
: getDpmProbability(contract.totalShares) : getDpmProbability(contract.totalShares)
} }
export function getInitialProbability(contract: BinaryContract) { export function getInitialProbability(
contract: BinaryContract | PseudoNumericContract
) {
if (contract.initialProbability) return contract.initialProbability if (contract.initialProbability) return contract.initialProbability
if (contract.mechanism === 'dpm-2' || (contract as any).totalShares) if (contract.mechanism === 'dpm-2' || (contract as any).totalShares)
@ -65,7 +74,9 @@ export function calculateShares(
} }
export function calculateSaleAmount(contract: Contract, bet: Bet) { 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 ? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
: calculateDpmSaleAmount(contract, bet) : calculateDpmSaleAmount(contract, bet)
} }
@ -87,7 +98,9 @@ export function getProbabilityAfterSale(
} }
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) { 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) ? calculateFixedPayout(contract, bet, outcome)
: calculateDpmPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome)
} }
@ -96,7 +109,9 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
const outcome = contract.resolution const outcome = contract.resolution
if (!outcome) throw new Error('Contract not resolved') 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) ? calculateFixedPayout(contract, bet, outcome)
: calculateDpmPayout(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 profit = payout + saleValue + redeemed - totalInvested
const profitPercent = (profit / totalInvested) * 100 const profitPercent = (profit / totalInvested) * 100
const hasShares = Object.values(totalShares).some( const hasShares = Object.values(totalShares).some((shares) => shares > 0)
(shares) => shares > 0
)
return { return {
invested: Math.max(0, currentInvested), invested: Math.max(0, currentInvested),

View File

@ -2,9 +2,10 @@ import { Answer } from './answer'
import { Fees } from './fees' import { Fees } from './fees'
export type AnyMechanism = DPM | CPMM export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = Binary | FreeResponse | Numeric export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
export type AnyContractType = export type AnyContractType =
| (CPMM & Binary) | (CPMM & Binary)
| (CPMM & PseudoNumeric)
| (DPM & Binary) | (DPM & Binary)
| (DPM & FreeResponse) | (DPM & FreeResponse)
| (DPM & Numeric) | (DPM & Numeric)
@ -33,7 +34,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
isResolved: boolean isResolved: boolean
resolutionTime?: number // When the contract creator resolved the market resolutionTime?: number // When the contract creator resolved the market
resolution?: string resolution?: string
resolutionProbability?: number, resolutionProbability?: number
closeEmailsSent?: number closeEmailsSent?: number
@ -44,7 +45,8 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
collectedFees: Fees collectedFees: Fees
} & T } & T
export type BinaryContract = Contract & Binary export type BinaryContract = Contract & Binary
export type PseudoNumericContract = Contract & PseudoNumeric
export type NumericContract = Contract & Numeric export type NumericContract = Contract & Numeric
export type FreeResponseContract = Contract & FreeResponse export type FreeResponseContract = Contract & FreeResponse
export type DPMContract = Contract & DPM export type DPMContract = Contract & DPM
@ -75,6 +77,18 @@ export type Binary = {
resolution?: resolution 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 = { export type FreeResponse = {
outcomeType: 'FREE_RESPONSE' outcomeType: 'FREE_RESPONSE'
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'. answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
@ -94,7 +108,7 @@ export type Numeric = {
export type outcomeType = AnyOutcomeType['outcomeType'] export type outcomeType = AnyOutcomeType['outcomeType']
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL' export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const 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_QUESTION_LENGTH = 480
export const MAX_DESCRIPTION_LENGTH = 10000 export const MAX_DESCRIPTION_LENGTH = 10000

View File

@ -14,6 +14,7 @@ import {
DPMBinaryContract, DPMBinaryContract,
FreeResponseContract, FreeResponseContract,
NumericContract, NumericContract,
PseudoNumericContract,
} from './contract' } from './contract'
import { noFees } from './fees' import { noFees } from './fees'
import { addObjects } from './util/object' import { addObjects } from './util/object'
@ -32,7 +33,7 @@ export type BetInfo = {
export const getNewBinaryCpmmBetInfo = ( export const getNewBinaryCpmmBetInfo = (
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
amount: number, amount: number,
contract: CPMMBinaryContract, contract: CPMMBinaryContract | PseudoNumericContract,
loanAmount: number loanAmount: number
) => { ) => {
const { shares, newPool, newP, fees } = calculateCpmmPurchase( const { shares, newPool, newP, fees } = calculateCpmmPurchase(

View File

@ -7,6 +7,7 @@ import {
FreeResponse, FreeResponse,
Numeric, Numeric,
outcomeType, outcomeType,
PseudoNumeric,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { parseTags } from './util/parse' import { parseTags } from './util/parse'
@ -27,7 +28,8 @@ export function getNewContract(
// used for numeric markets // used for numeric markets
bucketCount: number, bucketCount: number,
min: number, min: number,
max: number max: number,
isLogScale: boolean
) { ) {
const tags = parseTags( const tags = parseTags(
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
@ -37,6 +39,8 @@ export function getNewContract(
const propsByOutcomeType = const propsByOutcomeType =
outcomeType === 'BINARY' outcomeType === 'BINARY'
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante) ? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
: outcomeType === 'PSEUDO_NUMERIC'
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
: outcomeType === 'NUMERIC' : outcomeType === 'NUMERIC'
? getNumericProps(ante, bucketCount, min, max) ? getNumericProps(ante, bucketCount, min, max)
: getFreeAnswerProps(ante) : getFreeAnswerProps(ante)
@ -111,6 +115,24 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
return system 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 getFreeAnswerProps = (ante: number) => {
const system: DPM & FreeResponse = { const system: DPM & FreeResponse = {
mechanism: 'dpm-2', mechanism: 'dpm-2',

View File

@ -1,7 +1,12 @@
import { sumBy, groupBy, mapValues } from 'lodash' import { sumBy, groupBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet' import { Bet, NumericBet } from './bet'
import { Contract, CPMMBinaryContract, DPMContract } from './contract' import {
Contract,
CPMMBinaryContract,
DPMContract,
PseudoNumericContract,
} from './contract'
import { Fees } from './fees' import { Fees } from './fees'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { import {
@ -56,7 +61,11 @@ export const getPayouts = (
}, },
resolutionProbability?: number resolutionProbability?: number
): PayoutInfo => { ): PayoutInfo => {
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') { if (
contract.mechanism === 'cpmm-1' &&
(contract.outcomeType === 'BINARY' ||
contract.outcomeType === 'PSEUDO_NUMERIC')
) {
return getFixedPayouts( return getFixedPayouts(
outcome, outcome,
contract, contract,
@ -76,7 +85,7 @@ export const getPayouts = (
export const getFixedPayouts = ( export const getFixedPayouts = (
outcome: string | undefined, outcome: string | undefined,
contract: CPMMBinaryContract, contract: CPMMBinaryContract | PseudoNumericContract,
bets: Bet[], bets: Bet[],
liquidities: LiquidityProvision[], liquidities: LiquidityProvision[],
resolutionProbability?: number resolutionProbability?: number

45
common/pseudo-numeric.ts Normal file
View 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)
}

View File

@ -28,6 +28,7 @@ import { getNewContract } from '../../common/new-contract'
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
import { User } from '../../common/user' import { User } from '../../common/user'
import { Group, MAX_ID_LENGTH } from '../../common/group' import { Group, MAX_ID_LENGTH } from '../../common/group'
import { getPseudoProbability } from '../../common/pseudo-numeric'
const bodySchema = z.object({ const bodySchema = z.object({
question: z.string().min(1).max(MAX_QUESTION_LENGTH), question: z.string().min(1).max(MAX_QUESTION_LENGTH),
@ -45,19 +46,31 @@ const binarySchema = z.object({
initialProb: z.number().min(1).max(99), 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({ const numericSchema = z.object({
min: z.number(), min: finite(),
max: z.number(), max: finite(),
initialValue: finite(),
isLogScale: z.boolean().optional(),
}) })
export const createmarket = newEndpoint({}, async (req, auth) => { export const createmarket = newEndpoint({}, async (req, auth) => {
const { question, description, tags, closeTime, outcomeType, groupId } = const { question, description, tags, closeTime, outcomeType, groupId } =
validate(bodySchema, req.body) validate(bodySchema, req.body)
let min, max, initialProb let min, max, initialProb, isLogScale
if (outcomeType === 'NUMERIC') {
;({ min, max } = validate(numericSchema, req.body)) if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
if (max - min <= 0.01) throw new APIError(400, 'Invalid range.') 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') { if (outcomeType === 'BINARY') {
;({ initialProb } = validate(binarySchema, req.body)) ;({ initialProb } = validate(binarySchema, req.body))
@ -121,7 +134,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
tags ?? [], tags ?? [],
NUMERIC_BUCKET_COUNT, NUMERIC_BUCKET_COUNT,
min ?? 0, min ?? 0,
max ?? 0 max ?? 0,
isLogScale ?? false
) )
if (ante) await chargeUser(user.id, ante, true) if (ante) await chargeUser(user.id, ante, true)
@ -130,7 +144,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
const providerId = user.id const providerId = user.id
if (outcomeType === 'BINARY') { if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
const liquidityDoc = firestore const liquidityDoc = firestore
.collection(`contracts/${contract.id}/liquidity`) .collection(`contracts/${contract.id}/liquidity`)
.doc() .doc()

View File

@ -6,8 +6,13 @@ import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { DPM_CREATOR_FEE } from '../../common/fees' import { DPM_CREATOR_FEE } from '../../common/fees'
import { PrivateUser, User } from '../../common/user' 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 { getValueFromBucket } from '../../common/calculate-dpm'
import { formatNumericProbability } from '../../common/pseudo-numeric'
import { sendTemplateEmail } from './send-email' import { sendTemplateEmail } from './send-email'
import { getPrivateUser, getUser } from './utils' import { getPrivateUser, getUser } from './utils'
@ -101,6 +106,17 @@ const toDisplayResolution = (
return display || resolution 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 === 'MKT' && resolutions) return 'MULTI'
if (resolution === 'CANCEL') return 'N/A' if (resolution === 'CANCEL') return 'N/A'

View File

@ -70,7 +70,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') { if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
const { outcome } = validate(binarySchema, req.body) const { outcome } = validate(binarySchema, req.body)
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount) 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) const { outcome } = validate(binarySchema, req.body)
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount) return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') { } else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {

View File

@ -16,7 +16,11 @@ export const redeemShares = async (userId: string, contractId: string) => {
return { status: 'error', message: 'Invalid contract' } return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as 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' } return { status: 'success' }
const betsSnap = await transaction.get( const betsSnap = await transaction.get(

View File

@ -27,7 +27,7 @@ const bodySchema = z.object({
const binarySchema = z.object({ const binarySchema = z.object({
outcome: z.enum(RESOLUTIONS), outcome: z.enum(RESOLUTIONS),
probabilityInt: z.number().gte(0).lt(100).optional(), probabilityInt: z.number().gte(0).lte(100).optional(),
}) })
const freeResponseSchema = z.union([ const freeResponseSchema = z.union([
@ -39,7 +39,7 @@ const freeResponseSchema = z.union([
resolutions: z.array( resolutions: z.array(
z.object({ z.object({
answer: z.number().int().nonnegative(), 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(), 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'] } const opts = { secrets: ['MAILGUN_KEY'] }
export const resolvemarket = newEndpoint(opts, async (req, auth) => { export const resolvemarket = newEndpoint(opts, async (req, auth) => {
const { contractId } = validate(bodySchema, req.body) const { contractId } = validate(bodySchema, req.body)
const userId = auth.uid const userId = auth.uid
@ -221,12 +233,18 @@ const sendResolutionEmails = async (
function getResolutionParams(contract: Contract, body: string) { function getResolutionParams(contract: Contract, body: string) {
const { outcomeType } = contract const { outcomeType } = contract
if (outcomeType === 'NUMERIC') { if (outcomeType === 'NUMERIC') {
return { return {
...validate(numericSchema, body), ...validate(numericSchema, body),
resolutions: undefined, resolutions: undefined,
probabilityInt: undefined, probabilityInt: undefined,
} }
} else if (outcomeType === 'PSEUDO_NUMERIC') {
return {
...validate(pseudoNumericSchema, body),
resolutions: undefined,
}
} else if (outcomeType === 'FREE_RESPONSE') { } else if (outcomeType === 'FREE_RESPONSE') {
const freeResponseParams = validate(freeResponseSchema, body) const freeResponseParams = validate(freeResponseSchema, body)
const { outcome } = freeResponseParams const { outcome } = freeResponseParams

View File

@ -3,7 +3,11 @@ import React, { useEffect, useState } from 'react'
import { partition, sumBy } from 'lodash' import { partition, sumBy } from 'lodash'
import { useUser } from 'web/hooks/use-user' 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 { Col } from './layout/col'
import { Row } from './layout/row' import { Row } from './layout/row'
import { Spacer } from './layout/spacer' 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 { sellShares } from 'web/lib/firebase/api-call'
import { AmountInput, BuyAmountInput } from './amount-input' import { AmountInput, BuyAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip' import { InfoTooltip } from './info-tooltip'
import { BinaryOutcomeLabel } from './outcome-label' import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
import { import {
calculatePayoutAfterCorrectBet, calculatePayoutAfterCorrectBet,
calculateShares, calculateShares,
@ -35,6 +39,7 @@ import {
getCpmmProbability, getCpmmProbability,
getCpmmLiquidityFee, getCpmmLiquidityFee,
} from 'common/calculate-cpmm' } from 'common/calculate-cpmm'
import { getFormattedMappedValue } from 'common/pseudo-numeric'
import { SellRow } from './sell-row' import { SellRow } from './sell-row'
import { useSaveShares } from './use-save-shares' import { useSaveShares } from './use-save-shares'
import { SignUpPrompt } from './sign-up-prompt' import { SignUpPrompt } from './sign-up-prompt'
@ -42,7 +47,7 @@ import { isIOS } from 'web/lib/util/device'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
export function BetPanel(props: { export function BetPanel(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
className?: string className?: string
}) { }) {
const { contract, className } = props const { contract, className } = props
@ -81,7 +86,7 @@ export function BetPanel(props: {
} }
export function BetPanelSwitcher(props: { export function BetPanelSwitcher(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
className?: string className?: string
title?: string // Set if BetPanel is on a feed modal title?: string // Set if BetPanel is on a feed modal
selected?: 'YES' | 'NO' selected?: 'YES' | 'NO'
@ -89,7 +94,8 @@ export function BetPanelSwitcher(props: {
}) { }) {
const { contract, className, title, selected, onBetSuccess } = props const { contract, className, title, selected, onBetSuccess } = props
const { mechanism } = contract const { mechanism, outcomeType } = contract
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const user = useUser() const user = useUser()
const userBets = useUserContractBets(user?.id, contract.id) const userBets = useUserContractBets(user?.id, contract.id)
@ -122,7 +128,12 @@ export function BetPanelSwitcher(props: {
<Row className="items-center justify-between gap-2"> <Row className="items-center justify-between gap-2">
<div> <div>
You have {formatWithCommas(floorShares)}{' '} You have {formatWithCommas(floorShares)}{' '}
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares {isPseudoNumeric ? (
<PseudoNumericOutcomeLabel outcome={sharesOutcome} />
) : (
<BinaryOutcomeLabel outcome={sharesOutcome} />
)}{' '}
shares
</div> </div>
{tradeType === 'BUY' && ( {tradeType === 'BUY' && (
@ -190,12 +201,13 @@ export function BetPanelSwitcher(props: {
} }
function BuyPanel(props: { function BuyPanel(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
user: User | null | undefined user: User | null | undefined
selected?: 'YES' | 'NO' selected?: 'YES' | 'NO'
onBuySuccess?: () => void onBuySuccess?: () => void
}) { }) {
const { contract, user, selected, onBuySuccess } = props const { contract, user, selected, onBuySuccess } = props
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
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)
@ -302,6 +314,9 @@ function BuyPanel(props: {
: 0) : 0)
)} ${betChoice ?? 'YES'} shares` )} ${betChoice ?? 'YES'} shares`
: undefined : undefined
const format = getFormattedMappedValue(contract)
return ( return (
<> <>
<YesNoSelector <YesNoSelector
@ -309,6 +324,7 @@ function BuyPanel(props: {
btnClassName="flex-1" btnClassName="flex-1"
selected={betChoice} selected={betChoice}
onSelect={(choice) => onBetChoice(choice)} onSelect={(choice) => onBetChoice(choice)}
isPseudoNumeric={isPseudoNumeric}
/> />
<div className="my-3 text-left text-sm text-gray-500">Amount</div> <div className="my-3 text-left text-sm text-gray-500">Amount</div>
<BuyAmountInput <BuyAmountInput
@ -323,11 +339,13 @@ function BuyPanel(props: {
<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">
<div className="text-gray-500">Probability</div> <div className="text-gray-500">
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
</div>
<div> <div>
{formatPercent(initialProb)} {format(initialProb)}
<span className="mx-2"></span> <span className="mx-2"></span>
{formatPercent(resultProb)} {format(resultProb)}
</div> </div>
</Row> </Row>
@ -340,6 +358,8 @@ function BuyPanel(props: {
<br /> payout if{' '} <br /> payout if{' '}
<BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
</> </>
) : isPseudoNumeric ? (
'Max payout'
) : ( ) : (
<> <>
Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
@ -389,7 +409,7 @@ function BuyPanel(props: {
} }
export function SellPanel(props: { export function SellPanel(props: {
contract: CPMMBinaryContract contract: CPMMBinaryContract | PseudoNumericContract
userBets: Bet[] userBets: Bet[]
shares: number shares: number
sharesOutcome: 'YES' | 'NO' sharesOutcome: 'YES' | 'NO'
@ -488,6 +508,10 @@ export function SellPanel(props: {
} }
} }
const { outcomeType } = contract
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const format = getFormattedMappedValue(contract)
return ( return (
<> <>
<AmountInput <AmountInput
@ -511,11 +535,13 @@ export function SellPanel(props: {
<span className="text-neutral">{formatMoney(saleValue)}</span> <span className="text-neutral">{formatMoney(saleValue)}</span>
</Row> </Row>
<Row className="items-center justify-between"> <Row className="items-center justify-between">
<div className="text-gray-500">Probability</div> <div className="text-gray-500">
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
</div>
<div> <div>
{formatPercent(initialProb)} {format(initialProb)}
<span className="mx-2"></span> <span className="mx-2"></span>
{formatPercent(resultProb)} {format(resultProb)}
</div> </div>
</Row> </Row>
</Col> </Col>

View File

@ -3,7 +3,7 @@ import clsx from 'clsx'
import { BetPanelSwitcher } from './bet-panel' import { BetPanelSwitcher } from './bet-panel'
import { YesNoSelector } from './yes-no-selector' import { YesNoSelector } from './yes-no-selector'
import { BinaryContract } from 'common/contract' import { BinaryContract, PseudoNumericContract } from 'common/contract'
import { Modal } from './layout/modal' import { Modal } from './layout/modal'
import { SellButton } from './sell-button' import { SellButton } from './sell-button'
import { useUser } from 'web/hooks/use-user' 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. // Inline version of a bet panel. Opens BetPanel in a new modal.
export default function BetRow(props: { export default function BetRow(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
className?: string className?: string
btnClassName?: string btnClassName?: string
betPanelClassName?: string betPanelClassName?: string
@ -32,6 +32,7 @@ export default function BetRow(props: {
return ( return (
<> <>
<YesNoSelector <YesNoSelector
isPseudoNumeric={contract.outcomeType === 'PSEUDO_NUMERIC'}
className={clsx('justify-end', className)} className={clsx('justify-end', className)}
btnClassName={clsx('btn-sm w-24', btnClassName)} btnClassName={clsx('btn-sm w-24', btnClassName)}
onSelect={(choice) => { onSelect={(choice) => {

View File

@ -8,6 +8,7 @@ import { useUserBets } from 'web/hooks/use-user-bets'
import { Bet } from 'web/lib/firebase/bets' import { Bet } from 'web/lib/firebase/bets'
import { User } from 'web/lib/firebase/users' import { User } from 'web/lib/firebase/users'
import { import {
formatLargeNumber,
formatMoney, formatMoney,
formatPercent, formatPercent,
formatWithCommas, formatWithCommas,
@ -40,6 +41,7 @@ import {
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render' import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
import { trackLatency } from 'web/lib/firebase/tracking' import { trackLatency } from 'web/lib/firebase/tracking'
import { NumericContract } from 'common/contract' import { NumericContract } from 'common/contract'
import { formatNumericProbability } from 'common/pseudo-numeric'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { SellSharesModal } from './sell-modal' import { SellSharesModal } from './sell-modal'
@ -366,6 +368,7 @@ export function BetsSummary(props: {
const { contract, isYourBets, className } = props const { contract, isYourBets, className } = props
const { resolution, closeTime, outcomeType, mechanism } = contract const { resolution, closeTime, outcomeType, mechanism } = contract
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const isCpmm = mechanism === 'cpmm-1' const isCpmm = mechanism === 'cpmm-1'
const isClosed = closeTime && Date.now() > closeTime const isClosed = closeTime && Date.now() > closeTime
@ -427,6 +430,25 @@ export function BetsSummary(props: {
</div> </div>
</Col> </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> <Col>
<div className="whitespace-nowrap text-sm text-gray-500"> <div className="whitespace-nowrap text-sm text-gray-500">
@ -507,13 +529,15 @@ export function ContractBetsTable(props: {
const { isResolved, mechanism, outcomeType } = contract const { isResolved, mechanism, outcomeType } = contract
const isCPMM = mechanism === 'cpmm-1' const isCPMM = mechanism === 'cpmm-1'
const isNumeric = outcomeType === 'NUMERIC' const isNumeric = outcomeType === 'NUMERIC'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
return ( return (
<div className={clsx('overflow-x-auto', className)}> <div className={clsx('overflow-x-auto', className)}>
{amountRedeemed > 0 && ( {amountRedeemed > 0 && (
<> <>
<div className="pl-2 text-sm text-gray-500"> <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)}. automatically redeemed for {formatMoney(amountRedeemed)}.
</div> </div>
<Spacer h={4} /> <Spacer h={4} />
@ -541,7 +565,7 @@ export function ContractBetsTable(props: {
)} )}
{!isCPMM && !isResolved && <th>Payout if chosen</th>} {!isCPMM && !isResolved && <th>Payout if chosen</th>}
<th>Shares</th> <th>Shares</th>
<th>Probability</th> {!isPseudoNumeric && <th>Probability</th>}
<th>Date</th> <th>Date</th>
</tr> </tr>
</thead> </thead>
@ -585,6 +609,7 @@ function BetRow(props: {
const isCPMM = mechanism === 'cpmm-1' const isCPMM = mechanism === 'cpmm-1'
const isNumeric = outcomeType === 'NUMERIC' const isNumeric = outcomeType === 'NUMERIC'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const saleAmount = saleBet?.sale?.amount const saleAmount = saleBet?.sale?.amount
@ -628,14 +653,18 @@ function BetRow(props: {
truncate="short" truncate="short"
/> />
)} )}
{isPseudoNumeric &&
' than ' + formatNumericProbability(bet.probAfter, contract)}
</td> </td>
<td>{formatMoney(Math.abs(amount))}</td> <td>{formatMoney(Math.abs(amount))}</td>
{!isCPMM && !isNumeric && <td>{saleDisplay}</td>} {!isCPMM && !isNumeric && <td>{saleDisplay}</td>}
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>} {!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
<td>{formatWithCommas(Math.abs(shares))}</td> <td>{formatWithCommas(Math.abs(shares))}</td>
<td> {!isPseudoNumeric && (
{formatPercent(probBefore)} {formatPercent(probAfter)} <td>
</td> {formatPercent(probBefore)} {formatPercent(probAfter)}
</td>
)}
<td>{dayjs(createdTime).format('MMM D, h:mma')}</td> <td>{dayjs(createdTime).format('MMM D, h:mma')}</td>
</tr> </tr>
) )

View File

@ -9,6 +9,7 @@ import {
BinaryContract, BinaryContract,
FreeResponseContract, FreeResponseContract,
NumericContract, NumericContract,
PseudoNumericContract,
} from 'common/contract' } from 'common/contract'
import { import {
AnswerLabel, AnswerLabel,
@ -16,7 +17,11 @@ import {
CancelLabel, CancelLabel,
FreeResponseOutcomeLabel, FreeResponseOutcomeLabel,
} from '../outcome-label' } from '../outcome-label'
import { getOutcomeProbability, getTopAnswer } from 'common/calculate' import {
getOutcomeProbability,
getProbability,
getTopAnswer,
} from 'common/calculate'
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details' import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm' import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
import { QuickBet, ProbBar, getColor } from './quick-bet' 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 { useUser } from 'web/hooks/use-user'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
import { trackCallback } from 'web/lib/service/analytics' import { trackCallback } from 'web/lib/service/analytics'
import { formatNumericProbability } from 'common/pseudo-numeric'
export function ContractCard(props: { export function ContractCard(props: {
contract: Contract contract: Contract
@ -131,6 +137,13 @@ export function ContractCard(props: {
/> />
)} )}
{outcomeType === 'PSEUDO_NUMERIC' && (
<PseudoNumericResolutionOrExpectation
className="items-center"
contract={contract}
/>
)}
{outcomeType === 'NUMERIC' && ( {outcomeType === 'NUMERIC' && (
<NumericResolutionOrExpectation <NumericResolutionOrExpectation
className="items-center" className="items-center"
@ -270,7 +283,9 @@ export function NumericResolutionOrExpectation(props: {
{resolution === 'CANCEL' ? ( {resolution === 'CANCEL' ? (
<CancelLabel /> <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> </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>
)
}

View File

@ -11,6 +11,7 @@ import {
FreeResponseResolutionOrChance, FreeResponseResolutionOrChance,
BinaryResolutionOrChance, BinaryResolutionOrChance,
NumericResolutionOrExpectation, NumericResolutionOrExpectation,
PseudoNumericResolutionOrExpectation,
} from './contract-card' } from './contract-card'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import BetRow from '../bet-row' import BetRow from '../bet-row'
@ -32,6 +33,7 @@ export const ContractOverview = (props: {
const user = useUser() const user = useUser()
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
return ( return (
<Col className={clsx('mb-6', className)}> <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' && ( {outcomeType === 'NUMERIC' && (
<NumericResolutionOrExpectation <NumericResolutionOrExpectation
contract={contract} contract={contract}
@ -61,6 +70,11 @@ export const ContractOverview = (props: {
<Row className="items-center justify-between gap-4 xl:hidden"> <Row className="items-center justify-between gap-4 xl:hidden">
<BinaryResolutionOrChance contract={contract} /> <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} />} {tradingAllowed(contract) && <BetRow contract={contract} />}
</Row> </Row>
) : ( ) : (
@ -86,7 +100,9 @@ export const ContractOverview = (props: {
/> />
</Col> </Col>
<Spacer h={4} /> <Spacer h={4} />
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '} {(isBinary || isPseudoNumeric) && (
<ContractProbGraph contract={contract} bets={bets} />
)}{' '}
{outcomeType === 'FREE_RESPONSE' && ( {outcomeType === 'FREE_RESPONSE' && (
<AnswersGraph contract={contract} bets={bets} /> <AnswersGraph contract={contract} bets={bets} />
)} )}

View File

@ -5,16 +5,20 @@ import dayjs from 'dayjs'
import { memo } from 'react' import { memo } from 'react'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { getInitialProbability } from 'common/calculate' 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 { 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: { export const ContractProbGraph = memo(function ContractProbGraph(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
bets: Bet[] bets: Bet[]
height?: number height?: number
}) { }) {
const { contract, height } = props 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) const bets = props.bets.filter((bet) => !bet.isAnte && !bet.isRedemption)
@ -24,7 +28,10 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
contract.createdTime, contract.createdTime,
...bets.map((bet) => bet.createdTime), ...bets.map((bet) => bet.createdTime),
].map((time) => new Date(time)) ].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 isClosed = !!closeTime && Date.now() > closeTime
const latestTime = dayjs( const latestTime = dayjs(
@ -39,7 +46,11 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
times.push(latestTime.toDate()) times.push(latestTime.toDate())
probs.push(probs[probs.length - 1]) 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() const { width } = useWindowSize()
@ -55,9 +66,13 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
const totalPoints = width ? (width > 800 ? 300 : 50) : 1 const totalPoints = width ? (width > 800 ? 300 : 50) : 1
const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints
const points: { x: Date; y: number }[] = [] 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++) { 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( const numPoints: number = Math.floor(
dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep 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]) x: dayjs(times[i])
.add(thisTimeStep * n, 'ms') .add(thisTimeStep * n, 'ms')
.toDate(), .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 multiYear = !dayjs(startDate).isSame(latestTime, 'year')
const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime) const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime)
const formatter = isBinary
? formatPercent
: (x: DatumValue) => formatLargeNumber(+x.valueOf())
return ( return (
<div <div
className="w-full overflow-visible" className="w-full overflow-visible"
@ -87,12 +108,20 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
> >
<ResponsiveLine <ResponsiveLine
data={data} data={data}
yScale={{ min: 0, max: 100, type: 'linear' }} yScale={
yFormat={formatPercent} isBinary
? { min: 0, max: 100, type: 'linear' }
: {
min: contract.min + c,
max: contract.max + c,
type: contract.isLogScale ? 'log' : 'linear',
}
}
yFormat={formatter}
gridYValues={yTickValues} gridYValues={yTickValues}
axisLeft={{ axisLeft={{
tickValues: yTickValues, tickValues: yTickValues,
format: formatPercent, format: formatter,
}} }}
xScale={{ xScale={{
type: 'time', type: 'time',

View File

@ -2,6 +2,7 @@ import clsx from 'clsx'
import { import {
getOutcomeProbability, getOutcomeProbability,
getOutcomeProbabilityAfterBet, getOutcomeProbabilityAfterBet,
getProbability,
getTopAnswer, getTopAnswer,
} from 'common/calculate' } from 'common/calculate'
import { getExpectedValue } from 'common/calculate-dpm' 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 { sellShares } from 'web/lib/firebase/api-call'
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { formatNumericProbability } from 'common/pseudo-numeric'
const BET_SIZE = 10 const BET_SIZE = 10
export function QuickBet(props: { contract: Contract; user: User }) { export function QuickBet(props: { contract: Contract; user: User }) {
const { contract, user } = props 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 userBets = useUserContractBets(user.id, contract.id)
const topAnswer = const topAnswer =
contract.outcomeType === 'FREE_RESPONSE' outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
? getTopAnswer(contract)
: undefined
// TODO: yes/no from useSaveShares doesn't work on numeric contracts // TODO: yes/no from useSaveShares doesn't work on numeric contracts
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
@ -45,9 +46,9 @@ export function QuickBet(props: { contract: Contract; user: User }) {
topAnswer?.number.toString() || undefined topAnswer?.number.toString() || undefined
) )
const hasUpShares = const hasUpShares =
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC') yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC')
const hasDownShares = const hasDownShares =
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC' noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC'
const [upHover, setUpHover] = useState(false) const [upHover, setUpHover] = useState(false)
const [downHover, setDownHover] = 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 ( return (
<Col <Col
className={clsx( className={clsx(
@ -173,14 +155,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
<TriangleFillIcon <TriangleFillIcon
className={clsx( className={clsx(
'mx-auto h-5 w-5', 'mx-auto h-5 w-5',
upHover ? textColor : 'text-gray-400' upHover ? 'text-green-500' : 'text-gray-400'
)} )}
/> />
) : ( ) : (
<TriangleFillIcon <TriangleFillIcon
className={clsx( className={clsx(
'mx-auto h-5 w-5', '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} /> <QuickOutcomeView contract={contract} previewProb={previewProb} />
{/* Down bet triangle */} {/* Down bet triangle */}
{contract.outcomeType !== 'BINARY' ? ( {outcomeType !== 'BINARY' && outcomeType !== 'PSEUDO_NUMERIC' ? (
<div> <div>
<div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div> <div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div>
<TriangleDownFillIcon <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: { function QuickOutcomeView(props: {
contract: Contract contract: Contract
previewProb?: number previewProb?: number
@ -261,9 +262,16 @@ function QuickOutcomeView(props: {
}) { }) {
const { contract, previewProb, caption } = props const { contract, previewProb, caption } = props
const { outcomeType } = contract const { outcomeType } = contract
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
// If there's a preview prob, display that instead of the current prob // If there's a preview prob, display that instead of the current prob
const override = const override =
previewProb === undefined ? undefined : formatPercent(previewProb) previewProb === undefined
? undefined
: isPseudoNumeric
? formatNumericProbability(previewProb, contract)
: formatPercent(previewProb)
const textColor = `text-${getColor(contract)}` const textColor = `text-${getColor(contract)}`
let display: string | undefined let display: string | undefined
@ -271,6 +279,9 @@ function QuickOutcomeView(props: {
case 'BINARY': case 'BINARY':
display = getBinaryProbPercent(contract) display = getBinaryProbPercent(contract)
break break
case 'PSEUDO_NUMERIC':
display = formatNumericProbability(getProbability(contract), contract)
break
case 'NUMERIC': case 'NUMERIC':
display = formatLargeNumber(getExpectedValue(contract)) display = formatLargeNumber(getExpectedValue(contract))
break break
@ -295,11 +306,15 @@ function QuickOutcomeView(props: {
// Return a number from 0 to 1 for this contract // Return a number from 0 to 1 for this contract
// Resolved contracts are set to 1, for coloring purposes (even if NO) // Resolved contracts are set to 1, for coloring purposes (even if NO)
function getProb(contract: Contract) { function getProb(contract: Contract) {
const { outcomeType, resolution } = contract const { outcomeType, resolution, resolutionProbability } = contract
return resolution return resolutionProbability
? resolutionProbability
: resolution
? 1 ? 1
: outcomeType === 'BINARY' : outcomeType === 'BINARY'
? getBinaryProb(contract) ? getBinaryProb(contract)
: outcomeType === 'PSEUDO_NUMERIC'
? getProbability(contract)
: outcomeType === 'FREE_RESPONSE' : outcomeType === 'FREE_RESPONSE'
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '') ? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
: outcomeType === 'NUMERIC' : outcomeType === 'NUMERIC'
@ -316,7 +331,8 @@ function getNumericScale(contract: NumericContract) {
export function getColor(contract: Contract) { export function getColor(contract: Contract) {
// TODO: Try injecting a gradient here // TODO: Try injecting a gradient here
// return 'primary' // return 'primary'
const { resolution } = contract const { resolution, outcomeType } = contract
if (resolution) { if (resolution) {
return ( return (
OUTCOME_TO_COLOR[resolution as resolution] ?? 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()) { if ((contract.closeTime ?? Infinity) < Date.now()) {
return 'gray-400' return 'gray-400'
} }

View File

@ -7,13 +7,14 @@ import { Row } from 'web/components/layout/row'
import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Avatar, EmptyAvatar } from 'web/components/avatar'
import clsx from 'clsx' import clsx from 'clsx'
import { UsersIcon } from '@heroicons/react/solid' 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 { OutcomeLabel } from 'web/components/outcome-label'
import { RelativeTimestamp } from 'web/components/relative-timestamp' import { RelativeTimestamp } from 'web/components/relative-timestamp'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { uniqBy, partition, sumBy, groupBy } from 'lodash' import { uniqBy, partition, sumBy, groupBy } from 'lodash'
import { JoinSpans } from 'web/components/join-spans' import { JoinSpans } from 'web/components/join-spans'
import { UserLink } from '../user-page' import { UserLink } from '../user-page'
import { formatNumericProbability } from 'common/pseudo-numeric'
export function FeedBet(props: { export function FeedBet(props: {
contract: Contract contract: Contract
@ -75,6 +76,8 @@ export function BetStatusText(props: {
hideOutcome?: boolean hideOutcome?: boolean
}) { }) {
const { bet, contract, bettor, isSelf, hideOutcome } = props const { bet, contract, bettor, isSelf, hideOutcome } = props
const { outcomeType } = contract
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const { amount, outcome, createdTime } = bet const { amount, outcome, createdTime } = bet
const bought = amount >= 0 ? 'bought' : 'sold' const bought = amount >= 0 ? 'bought' : 'sold'
@ -97,7 +100,10 @@ export function BetStatusText(props: {
value={(bet as any).value} value={(bet as any).value}
contract={contract} contract={contract}
truncate="short" truncate="short"
/> />{' '}
{isPseudoNumeric
? ' than ' + formatNumericProbability(bet.probAfter, contract)
: ' at ' + formatPercent(bet.probAfter)}
</> </>
)} )}
<RelativeTimestamp time={createdTime} /> <RelativeTimestamp time={createdTime} />

View File

@ -6,13 +6,14 @@ import { User } from 'web/lib/firebase/users'
import { NumberCancelSelector } from './yes-no-selector' import { NumberCancelSelector } from './yes-no-selector'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
import { ResolveConfirmationButton } from './confirmation-button' import { ResolveConfirmationButton } from './confirmation-button'
import { NumericContract, PseudoNumericContract } from 'common/contract'
import { APIError, resolveMarket } from 'web/lib/firebase/api-call' import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
import { NumericContract } from 'common/contract'
import { BucketInput } from './bucket-input' import { BucketInput } from './bucket-input'
import { getPseudoProbability } from 'common/pseudo-numeric'
export function NumericResolutionPanel(props: { export function NumericResolutionPanel(props: {
creator: User creator: User
contract: NumericContract contract: NumericContract | PseudoNumericContract
className?: string className?: string
}) { }) {
useEffect(() => { useEffect(() => {
@ -21,6 +22,7 @@ export function NumericResolutionPanel(props: {
}, []) }, [])
const { contract, className } = props const { contract, className } = props
const { min, max, outcomeType } = contract
const [outcomeMode, setOutcomeMode] = useState< const [outcomeMode, setOutcomeMode] = useState<
'NUMBER' | 'CANCEL' | undefined 'NUMBER' | 'CANCEL' | undefined
@ -32,15 +34,32 @@ export function NumericResolutionPanel(props: {
const [error, setError] = useState<string | undefined>(undefined) const [error, setError] = useState<string | undefined>(undefined)
const resolve = async () => { 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 if (outcomeMode === undefined || finalOutcome === undefined) return
setIsSubmitting(true) 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 { try {
const result = await resolveMarket({ const result = await resolveMarket({
outcome: finalOutcome, outcome: finalOutcome,
value, value,
probabilityInt,
contractId: contract.id, contractId: contract.id,
}) })
console.log('resolved', outcome, 'result:', result) console.log('resolved', outcome, 'result:', result)
@ -77,7 +96,7 @@ export function NumericResolutionPanel(props: {
{outcomeMode === 'NUMBER' && ( {outcomeMode === 'NUMBER' && (
<BucketInput <BucketInput
contract={contract} contract={contract as any}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onBucketChange={(v, o) => (setValue(v), setOutcome(o))} onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
/> />

View File

@ -19,11 +19,15 @@ export function OutcomeLabel(props: {
value?: number value?: number
}) { }) {
const { outcome, contract, truncate, value } = props 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} /> return <BinaryOutcomeLabel outcome={outcome as any} />
if (contract.outcomeType === 'NUMERIC') if (outcomeType === 'NUMERIC')
return ( return (
<span className="text-blue-500"> <span className="text-blue-500">
{value ?? getValueFromBucket(outcome, contract)} {value ?? getValueFromBucket(outcome, contract)}
@ -49,6 +53,15 @@ export function BinaryOutcomeLabel(props: { outcome: resolution }) {
return <CancelLabel /> 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: { export function BinaryContractOutcomeLabel(props: {
contract: BinaryContract contract: BinaryContract
resolution: resolution resolution: resolution
@ -98,6 +111,14 @@ export function YesLabel() {
return <span className="text-primary">YES</span> 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() { export function NoLabel() {
return <span className="text-red-400">NO</span> return <span className="text-red-400">NO</span>
} }

View File

@ -1,4 +1,4 @@
import { BinaryContract } from 'common/contract' import { BinaryContract, PseudoNumericContract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { useUserContractBets } from 'web/hooks/use-user-bets' import { useUserContractBets } from 'web/hooks/use-user-bets'
import { useState } from 'react' import { useState } from 'react'
@ -7,7 +7,7 @@ import clsx from 'clsx'
import { SellSharesModal } from './sell-modal' import { SellSharesModal } from './sell-modal'
export function SellButton(props: { export function SellButton(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
user: User | null | undefined user: User | null | undefined
sharesOutcome: 'YES' | 'NO' | undefined sharesOutcome: 'YES' | 'NO' | undefined
shares: number shares: number
@ -16,7 +16,8 @@ export function SellButton(props: {
const { contract, user, sharesOutcome, shares, panelClassName } = props const { contract, user, sharesOutcome, shares, panelClassName } = props
const userBets = useUserContractBets(user?.id, contract.id) const userBets = useUserContractBets(user?.id, contract.id)
const [showSellModal, setShowSellModal] = useState(false) const [showSellModal, setShowSellModal] = useState(false)
const { mechanism } = contract const { mechanism, outcomeType } = contract
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
if (sharesOutcome && user && mechanism === 'cpmm-1') { if (sharesOutcome && user && mechanism === 'cpmm-1') {
return ( return (
@ -32,7 +33,10 @@ export function SellButton(props: {
)} )}
onClick={() => setShowSellModal(true)} onClick={() => setShowSellModal(true)}
> >
{'Sell ' + sharesOutcome} Sell{' '}
{isPseudoNumeric
? { YES: 'HIGH', NO: 'LOW' }[sharesOutcome]
: sharesOutcome}
</button> </button>
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}> <div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
{'(' + Math.floor(shares) + ' shares)'} {'(' + Math.floor(shares) + ' shares)'}

View File

@ -1,4 +1,4 @@
import { CPMMBinaryContract } from 'common/contract' import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { User } from 'common/user' import { User } from 'common/user'
import { Modal } from './layout/modal' import { Modal } from './layout/modal'
@ -11,7 +11,7 @@ import clsx from 'clsx'
export function SellSharesModal(props: { export function SellSharesModal(props: {
className?: string className?: string
contract: CPMMBinaryContract contract: CPMMBinaryContract | PseudoNumericContract
userBets: Bet[] userBets: Bet[]
shares: number shares: number
sharesOutcome: 'YES' | 'NO' sharesOutcome: 'YES' | 'NO'

View File

@ -1,4 +1,4 @@
import { BinaryContract } from 'common/contract' import { BinaryContract, PseudoNumericContract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { useState } from 'react' import { useState } from 'react'
import { Col } from './layout/col' import { Col } from './layout/col'
@ -10,7 +10,7 @@ import { useSaveShares } from './use-save-shares'
import { SellSharesModal } from './sell-modal' import { SellSharesModal } from './sell-modal'
export function SellRow(props: { export function SellRow(props: {
contract: BinaryContract contract: BinaryContract | PseudoNumericContract
user: User | null | undefined user: User | null | undefined
className?: string className?: string
}) { }) {

View File

@ -12,6 +12,7 @@ export function YesNoSelector(props: {
btnClassName?: string btnClassName?: string
replaceYesButton?: React.ReactNode replaceYesButton?: React.ReactNode
replaceNoButton?: React.ReactNode replaceNoButton?: React.ReactNode
isPseudoNumeric?: boolean
}) { }) {
const { const {
selected, selected,
@ -20,6 +21,7 @@ export function YesNoSelector(props: {
btnClassName, btnClassName,
replaceNoButton, replaceNoButton,
replaceYesButton, replaceYesButton,
isPseudoNumeric,
} = props } = props
const commonClassNames = const commonClassNames =
@ -41,7 +43,7 @@ export function YesNoSelector(props: {
)} )}
onClick={() => onSelect('YES')} onClick={() => onSelect('YES')}
> >
Bet YES {isPseudoNumeric ? 'HIGHER' : 'Bet YES'}
</button> </button>
)} )}
{replaceNoButton ? ( {replaceNoButton ? (
@ -58,7 +60,7 @@ export function YesNoSelector(props: {
)} )}
onClick={() => onSelect('NO')} onClick={() => onSelect('NO')}
> >
Bet NO {isPseudoNumeric ? 'LOWER' : 'Bet NO'}
</button> </button>
)} )}
</Row> </Row>

View File

@ -144,10 +144,12 @@ export function ContractPageContent(
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const isNumeric = outcomeType === 'NUMERIC' const isNumeric = outcomeType === 'NUMERIC'
const allowTrade = tradingAllowed(contract) const allowTrade = tradingAllowed(contract)
const allowResolve = !isResolved && isCreator && !!user const allowResolve = !isResolved && isCreator && !!user
const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve) const hasSidePanel =
(isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
const ogCardProps = getOpenGraphProps(contract) const ogCardProps = getOpenGraphProps(contract)
@ -170,7 +172,7 @@ export function ContractPageContent(
<BetPanel className="hidden xl:flex" contract={contract} /> <BetPanel className="hidden xl:flex" contract={contract} />
))} ))}
{allowResolve && {allowResolve &&
(isNumeric ? ( (isNumeric || isPseudoNumeric ? (
<NumericResolutionPanel creator={user} contract={contract} /> <NumericResolutionPanel creator={user} contract={contract} />
) : ( ) : (
<ResolutionPanel creator={user} contract={contract} /> <ResolutionPanel creator={user} contract={contract} />
@ -210,10 +212,11 @@ export function ContractPageContent(
)} )}
<ContractOverview contract={contract} bets={bets} /> <ContractOverview contract={contract} bets={bets} />
{isNumeric && ( {isNumeric && (
<AlertBox <AlertBox
title="Warning" 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."
/> />
)} )}

View File

@ -85,8 +85,12 @@ export function NewContract(props: {
const { creator, question, groupId } = props const { creator, question, groupId } = props
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY') const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
const [initialProb] = useState(50) const [initialProb] = useState(50)
const [minString, setMinString] = useState('') const [minString, setMinString] = useState('')
const [maxString, setMaxString] = useState('') const [maxString, setMaxString] = useState('')
const [isLogScale, setIsLogScale] = useState(false)
const [initialValueString, setInitialValueString] = useState('')
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
// const [tagText, setTagText] = useState<string>(tag ?? '') // const [tagText, setTagText] = useState<string>(tag ?? '')
// const tags = parseWordsAsTags(tagText) // const tags = parseWordsAsTags(tagText)
@ -129,6 +133,18 @@ export function NewContract(props: {
const min = minString ? parseFloat(minString) : undefined const min = minString ? parseFloat(minString) : undefined
const max = maxString ? parseFloat(maxString) : 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: // get days from today until the end of this year:
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day') const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
@ -145,13 +161,16 @@ export function NewContract(props: {
// closeTime must be in the future // closeTime must be in the future
closeTime && closeTime &&
closeTime > Date.now() && closeTime > Date.now() &&
(outcomeType !== 'NUMERIC' || (outcomeType !== 'PSEUDO_NUMERIC' ||
(min !== undefined && (min !== undefined &&
max !== undefined && max !== undefined &&
initialValue !== undefined &&
isFinite(min) && isFinite(min) &&
isFinite(max) && isFinite(max) &&
min < max && min < max &&
max - min > 0.01)) max - min > 0.01 &&
min < initialValue &&
initialValue < max))
function setCloseDateInDays(days: number) { function setCloseDateInDays(days: number) {
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
@ -175,6 +194,8 @@ export function NewContract(props: {
closeTime, closeTime,
min, min,
max, max,
initialValue,
isLogScale: (min ?? 0) < 0 ? false : isLogScale,
groupId: selectedGroup?.id, groupId: selectedGroup?.id,
tags: category ? [category] : undefined, tags: category ? [category] : undefined,
}) })
@ -220,6 +241,7 @@ export function NewContract(props: {
choicesMap={{ choicesMap={{
'Yes / No': 'BINARY', 'Yes / No': 'BINARY',
'Free response': 'FREE_RESPONSE', 'Free response': 'FREE_RESPONSE',
Numeric: 'PSEUDO_NUMERIC',
}} }}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
className={'col-span-4'} className={'col-span-4'}
@ -232,38 +254,89 @@ export function NewContract(props: {
<Spacer h={6} /> <Spacer h={6} />
{outcomeType === 'NUMERIC' && ( {outcomeType === 'PSEUDO_NUMERIC' && (
<div className="form-control items-start"> <>
<label className="label gap-2"> <div className="form-control mb-2 items-start">
<span className="mb-1">Range</span> <label className="label gap-2">
<InfoTooltip text="The minimum and maximum numbers across the numeric range." /> <span className="mb-1">Range</span>
</label> <InfoTooltip text="The minimum and maximum numbers across the numeric range." />
</label>
<Row className="gap-2"> <Row className="gap-2">
<input <input
type="number" type="number"
className="input input-bordered" className="input input-bordered"
placeholder="MIN" placeholder="MIN"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onChange={(e) => setMinString(e.target.value)} onChange={(e) => setMinString(e.target.value)}
min={Number.MIN_SAFE_INTEGER} onBlur={adjustIsLog}
max={Number.MAX_SAFE_INTEGER} min={Number.MIN_SAFE_INTEGER}
disabled={isSubmitting} max={Number.MAX_SAFE_INTEGER}
value={minString ?? ''} disabled={isSubmitting}
/> value={minString ?? ''}
<input />
type="number" <input
className="input input-bordered" type="number"
placeholder="MAX" className="input input-bordered"
onClick={(e) => e.stopPropagation()} placeholder="MAX"
onChange={(e) => setMaxString(e.target.value)} onClick={(e) => e.stopPropagation()}
min={Number.MIN_SAFE_INTEGER} onChange={(e) => setMaxString(e.target.value)}
max={Number.MAX_SAFE_INTEGER} onBlur={adjustIsLog}
disabled={isSubmitting} min={Number.MIN_SAFE_INTEGER}
value={maxString} max={Number.MAX_SAFE_INTEGER}
/> disabled={isSubmitting}
</Row> value={maxString}
</div> />
</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"> <div className="form-control max-w-[265px] items-start">

View File

@ -7,6 +7,7 @@ import {
BinaryResolutionOrChance, BinaryResolutionOrChance,
FreeResponseResolutionOrChance, FreeResponseResolutionOrChance,
NumericResolutionOrExpectation, NumericResolutionOrExpectation,
PseudoNumericResolutionOrExpectation,
} from 'web/components/contract/contract-card' } from 'web/components/contract/contract-card'
import { ContractDetails } from 'web/components/contract/contract-details' import { ContractDetails } from 'web/components/contract/contract-details'
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph' 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 { question, outcomeType } = contract
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const href = `https://${DOMAIN}${contractPath(contract)}` const href = `https://${DOMAIN}${contractPath(contract)}`
@ -110,13 +112,18 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
{isBinary && ( {isBinary && (
<Row className="items-center gap-4"> <Row className="items-center gap-4">
{/* this fails typechecking, but it doesn't explode because we will <BetRow contract={contract} betPanelClassName="scale-75" />
never */}
<BetRow contract={contract as any} betPanelClassName="scale-75" />
<BinaryResolutionOrChance contract={contract} /> <BinaryResolutionOrChance contract={contract} />
</Row> </Row>
)} )}
{isPseudoNumeric && (
<Row className="items-center gap-4">
<BetRow contract={contract} betPanelClassName="scale-75" />
<PseudoNumericResolutionOrExpectation contract={contract} />
</Row>
)}
{outcomeType === 'FREE_RESPONSE' && ( {outcomeType === 'FREE_RESPONSE' && (
<FreeResponseResolutionOrChance <FreeResponseResolutionOrChance
contract={contract} contract={contract}
@ -133,7 +140,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
</div> </div>
<div className="mx-1" style={{ paddingBottom }}> <div className="mx-1" style={{ paddingBottom }}>
{isBinary && ( {(isBinary || isPseudoNumeric) && (
<ContractProbGraph <ContractProbGraph
contract={contract} contract={contract}
bets={bets} bets={bets}