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, |   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), | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  | @ -45,6 +46,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { | ||||||
| } & 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 | ||||||
|  |  | ||||||
|  | @ -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( | ||||||
|  |  | ||||||
|  | @ -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', | ||||||
|  |  | ||||||
|  | @ -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
									
								
							
							
						
						
									
										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 { 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() | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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') { | ||||||
|  |  | ||||||
|  | @ -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( | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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) => { | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |       {!isPseudoNumeric && ( | ||||||
|         <td> |         <td> | ||||||
|           {formatPercent(probBefore)} → {formatPercent(probAfter)} |           {formatPercent(probBefore)} → {formatPercent(probAfter)} | ||||||
|         </td> |         </td> | ||||||
|  |       )} | ||||||
|       <td>{dayjs(createdTime).format('MMM D, h:mma')}</td> |       <td>{dayjs(createdTime).format('MMM D, h:mma')}</td> | ||||||
|     </tr> |     </tr> | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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} /> | ||||||
|       )} |       )} | ||||||
|  |  | ||||||
|  | @ -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', | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -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} /> | ||||||
|  |  | ||||||
|  | @ -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))} | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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)'} | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| }) { | }) { | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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." | ||||||
|           /> |           /> | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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,8 +254,9 @@ export function NewContract(props: { | ||||||
| 
 | 
 | ||||||
|       <Spacer h={6} /> |       <Spacer h={6} /> | ||||||
| 
 | 
 | ||||||
|       {outcomeType === 'NUMERIC' && ( |       {outcomeType === 'PSEUDO_NUMERIC' && ( | ||||||
|         <div className="form-control items-start"> |         <> | ||||||
|  |           <div className="form-control mb-2 items-start"> | ||||||
|             <label className="label gap-2"> |             <label className="label gap-2"> | ||||||
|               <span className="mb-1">Range</span> |               <span className="mb-1">Range</span> | ||||||
|               <InfoTooltip text="The minimum and maximum numbers across the numeric range." /> |               <InfoTooltip text="The minimum and maximum numbers across the numeric range." /> | ||||||
|  | @ -246,6 +269,7 @@ export function NewContract(props: { | ||||||
|                 placeholder="MIN" |                 placeholder="MIN" | ||||||
|                 onClick={(e) => e.stopPropagation()} |                 onClick={(e) => e.stopPropagation()} | ||||||
|                 onChange={(e) => setMinString(e.target.value)} |                 onChange={(e) => setMinString(e.target.value)} | ||||||
|  |                 onBlur={adjustIsLog} | ||||||
|                 min={Number.MIN_SAFE_INTEGER} |                 min={Number.MIN_SAFE_INTEGER} | ||||||
|                 max={Number.MAX_SAFE_INTEGER} |                 max={Number.MAX_SAFE_INTEGER} | ||||||
|                 disabled={isSubmitting} |                 disabled={isSubmitting} | ||||||
|  | @ -257,14 +281,63 @@ export function NewContract(props: { | ||||||
|                 placeholder="MAX" |                 placeholder="MAX" | ||||||
|                 onClick={(e) => e.stopPropagation()} |                 onClick={(e) => e.stopPropagation()} | ||||||
|                 onChange={(e) => setMaxString(e.target.value)} |                 onChange={(e) => setMaxString(e.target.value)} | ||||||
|  |                 onBlur={adjustIsLog} | ||||||
|                 min={Number.MIN_SAFE_INTEGER} |                 min={Number.MIN_SAFE_INTEGER} | ||||||
|                 max={Number.MAX_SAFE_INTEGER} |                 max={Number.MAX_SAFE_INTEGER} | ||||||
|                 disabled={isSubmitting} |                 disabled={isSubmitting} | ||||||
|                 value={maxString} |                 value={maxString} | ||||||
|               /> |               /> | ||||||
|             </Row> |             </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> | ||||||
|  |           <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"> | ||||||
|         <label className="label gap-2"> |         <label className="label gap-2"> | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user