Multiple choice markets (#698)
* multipe choice answers * create multiple choice cloud function * multi choice market page * show outcome '0' * stats: multi choice type * update place bet * answer doc id = outcome * update resolve market * prettier * fix * fix resolution
This commit is contained in:
parent
b1c4f018f9
commit
1aaae93113
|
@ -5,12 +5,14 @@ import {
|
||||||
CPMMBinaryContract,
|
CPMMBinaryContract,
|
||||||
DPMBinaryContract,
|
DPMBinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { LiquidityProvision } from './liquidity-provision'
|
import { LiquidityProvision } from './liquidity-provision'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
import { ENV_CONFIG } from './envs/constants'
|
import { ENV_CONFIG } from './envs/constants'
|
||||||
|
import { Answer } from './answer'
|
||||||
|
|
||||||
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
|
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
|
||||||
|
|
||||||
|
@ -111,6 +113,50 @@ export function getFreeAnswerAnte(
|
||||||
return anteBet
|
return anteBet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMultipleChoiceAntes(
|
||||||
|
creator: User,
|
||||||
|
contract: MultipleChoiceContract,
|
||||||
|
answers: string[],
|
||||||
|
betDocIds: string[]
|
||||||
|
) {
|
||||||
|
const { totalBets, totalShares } = contract
|
||||||
|
const amount = totalBets['0']
|
||||||
|
const shares = totalShares['0']
|
||||||
|
const p = 1 / answers.length
|
||||||
|
|
||||||
|
const { createdTime } = contract
|
||||||
|
|
||||||
|
const bets: Bet[] = answers.map((answer, i) => ({
|
||||||
|
id: betDocIds[i],
|
||||||
|
userId: creator.id,
|
||||||
|
contractId: contract.id,
|
||||||
|
amount,
|
||||||
|
shares,
|
||||||
|
outcome: i.toString(),
|
||||||
|
probBefore: p,
|
||||||
|
probAfter: p,
|
||||||
|
createdTime,
|
||||||
|
isAnte: true,
|
||||||
|
fees: noFees,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { username, name, avatarUrl } = creator
|
||||||
|
|
||||||
|
const answerObjects: Answer[] = answers.map((answer, i) => ({
|
||||||
|
id: i.toString(),
|
||||||
|
number: i,
|
||||||
|
contractId: contract.id,
|
||||||
|
createdTime,
|
||||||
|
userId: creator.id,
|
||||||
|
username,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
text: answer,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { bets, answerObjects }
|
||||||
|
}
|
||||||
|
|
||||||
export function getNumericAnte(
|
export function getNumericAnte(
|
||||||
anteBettorId: string,
|
anteBettorId: string,
|
||||||
contract: NumericContract,
|
contract: NumericContract,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { floatingEqual } from './util/math'
|
import { floatingEqual } from './util/math'
|
||||||
|
|
||||||
|
@ -200,7 +201,9 @@ export function getContractBetNullMetrics() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTopAnswer(contract: FreeResponseContract) {
|
export function getTopAnswer(
|
||||||
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
|
) {
|
||||||
const { answers } = contract
|
const { answers } = contract
|
||||||
const top = maxBy(
|
const top = maxBy(
|
||||||
answers?.map((answer) => ({
|
answers?.map((answer) => ({
|
||||||
|
|
|
@ -4,13 +4,19 @@ import { JSONContent } from '@tiptap/core'
|
||||||
import { GroupLink } from 'common/group'
|
import { GroupLink } from 'common/group'
|
||||||
|
|
||||||
export type AnyMechanism = DPM | CPMM
|
export type AnyMechanism = DPM | CPMM
|
||||||
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
|
export type AnyOutcomeType =
|
||||||
|
| Binary
|
||||||
|
| MultipleChoice
|
||||||
|
| PseudoNumeric
|
||||||
|
| FreeResponse
|
||||||
|
| Numeric
|
||||||
export type AnyContractType =
|
export type AnyContractType =
|
||||||
| (CPMM & Binary)
|
| (CPMM & Binary)
|
||||||
| (CPMM & PseudoNumeric)
|
| (CPMM & PseudoNumeric)
|
||||||
| (DPM & Binary)
|
| (DPM & Binary)
|
||||||
| (DPM & FreeResponse)
|
| (DPM & FreeResponse)
|
||||||
| (DPM & Numeric)
|
| (DPM & Numeric)
|
||||||
|
| (DPM & MultipleChoice)
|
||||||
|
|
||||||
export type Contract<T extends AnyContractType = AnyContractType> = {
|
export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -57,6 +63,7 @@ export type BinaryContract = Contract & Binary
|
||||||
export type PseudoNumericContract = Contract & PseudoNumeric
|
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 MultipleChoiceContract = Contract & MultipleChoice
|
||||||
export type DPMContract = Contract & DPM
|
export type DPMContract = Contract & DPM
|
||||||
export type CPMMContract = Contract & CPMM
|
export type CPMMContract = Contract & CPMM
|
||||||
export type DPMBinaryContract = BinaryContract & DPM
|
export type DPMBinaryContract = BinaryContract & DPM
|
||||||
|
@ -104,6 +111,13 @@ export type FreeResponse = {
|
||||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MultipleChoice = {
|
||||||
|
outcomeType: 'MULTIPLE_CHOICE'
|
||||||
|
answers: Answer[]
|
||||||
|
resolution?: string | 'MKT' | 'CANCEL'
|
||||||
|
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||||
|
}
|
||||||
|
|
||||||
export type Numeric = {
|
export type Numeric = {
|
||||||
outcomeType: 'NUMERIC'
|
outcomeType: 'NUMERIC'
|
||||||
bucketCount: number
|
bucketCount: number
|
||||||
|
@ -118,6 +132,7 @@ 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 = [
|
export const OUTCOME_TYPES = [
|
||||||
'BINARY',
|
'BINARY',
|
||||||
|
'MULTIPLE_CHOICE',
|
||||||
'FREE_RESPONSE',
|
'FREE_RESPONSE',
|
||||||
'PSEUDO_NUMERIC',
|
'PSEUDO_NUMERIC',
|
||||||
'NUMERIC',
|
'NUMERIC',
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
CPMMBinaryContract,
|
CPMMBinaryContract,
|
||||||
DPMBinaryContract,
|
DPMBinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
|
@ -322,7 +323,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
export const getNewMultiBetInfo = (
|
export const getNewMultiBetInfo = (
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FreeResponseContract,
|
contract: FreeResponseContract | MultipleChoiceContract,
|
||||||
loanAmount: number
|
loanAmount: number
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
CPMM,
|
CPMM,
|
||||||
DPM,
|
DPM,
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
|
MultipleChoice,
|
||||||
Numeric,
|
Numeric,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
PseudoNumeric,
|
PseudoNumeric,
|
||||||
|
@ -30,7 +31,10 @@ export function getNewContract(
|
||||||
bucketCount: number,
|
bucketCount: number,
|
||||||
min: number,
|
min: number,
|
||||||
max: number,
|
max: number,
|
||||||
isLogScale: boolean
|
isLogScale: boolean,
|
||||||
|
|
||||||
|
// for multiple choice
|
||||||
|
answers: string[]
|
||||||
) {
|
) {
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
[
|
[
|
||||||
|
@ -48,6 +52,8 @@ export function getNewContract(
|
||||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||||
: outcomeType === 'NUMERIC'
|
: outcomeType === 'NUMERIC'
|
||||||
? getNumericProps(ante, bucketCount, min, max)
|
? getNumericProps(ante, bucketCount, min, max)
|
||||||
|
: outcomeType === 'MULTIPLE_CHOICE'
|
||||||
|
? getMultipleChoiceProps(ante, answers)
|
||||||
: getFreeAnswerProps(ante)
|
: getFreeAnswerProps(ante)
|
||||||
|
|
||||||
const contract: Contract = removeUndefinedProps({
|
const contract: Contract = removeUndefinedProps({
|
||||||
|
@ -151,6 +157,26 @@ const getFreeAnswerProps = (ante: number) => {
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
|
||||||
|
const numAnswers = answers.length
|
||||||
|
const betAnte = ante / numAnswers
|
||||||
|
const betShares = Math.sqrt(ante ** 2 / numAnswers)
|
||||||
|
|
||||||
|
const defaultValues = (x: any) =>
|
||||||
|
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
|
||||||
|
|
||||||
|
const system: DPM & MultipleChoice = {
|
||||||
|
mechanism: 'dpm-2',
|
||||||
|
outcomeType: 'MULTIPLE_CHOICE',
|
||||||
|
pool: defaultValues(betAnte),
|
||||||
|
totalShares: defaultValues(betShares),
|
||||||
|
totalBets: defaultValues(betAnte),
|
||||||
|
answers: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return system
|
||||||
|
}
|
||||||
|
|
||||||
const getNumericProps = (
|
const getNumericProps = (
|
||||||
ante: number,
|
ante: number,
|
||||||
bucketCount: number,
|
bucketCount: number,
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
||||||
|
|
||||||
import { Bet, NumericBet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||||
import { DPMContract, FreeResponseContract } from './contract'
|
import {
|
||||||
|
DPMContract,
|
||||||
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
|
} from './contract'
|
||||||
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
|
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects } from './util/object'
|
||||||
|
|
||||||
|
@ -180,7 +184,7 @@ export const getDpmMktPayouts = (
|
||||||
|
|
||||||
export const getPayoutsMultiOutcome = (
|
export const getPayoutsMultiOutcome = (
|
||||||
resolutions: { [outcome: string]: number },
|
resolutions: { [outcome: string]: number },
|
||||||
contract: FreeResponseContract,
|
contract: FreeResponseContract | MultipleChoiceContract,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const poolTotal = sum(Object.values(contract.pool))
|
const poolTotal = sum(Object.values(contract.pool))
|
||||||
|
|
|
@ -117,6 +117,7 @@ export const getDpmPayouts = (
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
): PayoutInfo => {
|
): PayoutInfo => {
|
||||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
const { outcomeType } = contract
|
||||||
|
|
||||||
switch (outcome) {
|
switch (outcome) {
|
||||||
case 'YES':
|
case 'YES':
|
||||||
|
@ -124,7 +125,8 @@ export const getDpmPayouts = (
|
||||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||||
|
|
||||||
case 'MKT':
|
case 'MKT':
|
||||||
return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
return outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
|
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
|
||||||
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
||||||
case 'CANCEL':
|
case 'CANCEL':
|
||||||
|
@ -132,7 +134,7 @@ export const getDpmPayouts = (
|
||||||
return getDpmCancelPayouts(contract, openBets)
|
return getDpmCancelPayouts(contract, openBets)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (contract.outcomeType === 'NUMERIC')
|
if (outcomeType === 'NUMERIC')
|
||||||
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||||
|
|
||||||
// Outcome is a free response answer id.
|
// Outcome is a free response answer id.
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MAX_QUESTION_LENGTH,
|
MAX_QUESTION_LENGTH,
|
||||||
MAX_TAG_LENGTH,
|
MAX_TAG_LENGTH,
|
||||||
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
OUTCOME_TYPES,
|
OUTCOME_TYPES,
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
|
@ -20,15 +21,18 @@ import {
|
||||||
FIXED_ANTE,
|
FIXED_ANTE,
|
||||||
getCpmmInitialLiquidity,
|
getCpmmInitialLiquidity,
|
||||||
getFreeAnswerAnte,
|
getFreeAnswerAnte,
|
||||||
|
getMultipleChoiceAntes,
|
||||||
getNumericAnte,
|
getNumericAnte,
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { getNoneAnswer } from '../../common/answer'
|
import { Answer, getNoneAnswer } from '../../common/answer'
|
||||||
import { getNewContract } from '../../common/new-contract'
|
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'
|
import { getPseudoProbability } from '../../common/pseudo-numeric'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
|
import { zip } from 'lodash'
|
||||||
|
import { Bet } from 'common/bet'
|
||||||
|
|
||||||
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
||||||
z.intersection(
|
z.intersection(
|
||||||
|
@ -79,11 +83,15 @@ const numericSchema = z.object({
|
||||||
isLogScale: z.boolean().optional(),
|
isLogScale: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const multipleChoiceSchema = z.object({
|
||||||
|
answers: z.string().trim().min(1).array().min(2),
|
||||||
|
})
|
||||||
|
|
||||||
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, isLogScale
|
let min, max, initialProb, isLogScale, answers
|
||||||
|
|
||||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||||
let initialValue
|
let initialValue
|
||||||
|
@ -104,10 +112,15 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
)
|
)
|
||||||
else throw new APIError(400, 'Invalid initial probability.')
|
else throw new APIError(400, 'Invalid initial probability.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outcomeType === 'BINARY') {
|
if (outcomeType === 'BINARY') {
|
||||||
;({ initialProb } = validate(binarySchema, req.body))
|
;({ initialProb } = validate(binarySchema, req.body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||||
|
;({ answers } = validate(multipleChoiceSchema, req.body))
|
||||||
|
}
|
||||||
|
|
||||||
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||||
if (!userDoc.exists) {
|
if (!userDoc.exists) {
|
||||||
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||||
|
@ -167,7 +180,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
NUMERIC_BUCKET_COUNT,
|
NUMERIC_BUCKET_COUNT,
|
||||||
min ?? 0,
|
min ?? 0,
|
||||||
max ?? 0,
|
max ?? 0,
|
||||||
isLogScale ?? false
|
isLogScale ?? false,
|
||||||
|
answers ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ante) await chargeUser(user.id, ante, true)
|
if (ante) await chargeUser(user.id, ante, true)
|
||||||
|
@ -189,6 +203,31 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
await liquidityDoc.set(lp)
|
await liquidityDoc.set(lp)
|
||||||
|
} else if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||||
|
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
|
||||||
|
const betDocs = (answers ?? []).map(() => betCol.doc())
|
||||||
|
|
||||||
|
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
|
||||||
|
const answerDocs = (answers ?? []).map((_, i) =>
|
||||||
|
answerCol.doc(i.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
const { bets, answerObjects } = getMultipleChoiceAntes(
|
||||||
|
user,
|
||||||
|
contract as MultipleChoiceContract,
|
||||||
|
answers ?? [],
|
||||||
|
betDocs.map((bd) => bd.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet))
|
||||||
|
)
|
||||||
|
await Promise.all(
|
||||||
|
zip(answerObjects, answerDocs).map(([answer, doc]) =>
|
||||||
|
doc?.create(answer as Answer)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await contractRef.update({ answers: answerObjects })
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const noneAnswerDoc = firestore
|
const noneAnswerDoc = firestore
|
||||||
.collection(`contracts/${contract.id}/answers`)
|
.collection(`contracts/${contract.id}/answers`)
|
||||||
|
|
|
@ -96,7 +96,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
limitProb,
|
limitProb,
|
||||||
unfilledBets
|
unfilledBets
|
||||||
)
|
)
|
||||||
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
} else if (
|
||||||
|
(outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||||
|
mechanism == 'dpm-2'
|
||||||
|
) {
|
||||||
const { outcome } = validate(freeResponseSchema, req.body)
|
const { outcome } = validate(freeResponseSchema, req.body)
|
||||||
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
||||||
const answerSnap = await trans.get(answerDoc)
|
const answerSnap = await trans.get(answerDoc)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
RESOLUTIONS,
|
RESOLUTIONS,
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
@ -245,7 +246,10 @@ function getResolutionParams(contract: Contract, body: string) {
|
||||||
...validate(pseudoNumericSchema, body),
|
...validate(pseudoNumericSchema, body),
|
||||||
resolutions: undefined,
|
resolutions: undefined,
|
||||||
}
|
}
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
} else if (
|
||||||
|
outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
outcomeType === 'MULTIPLE_CHOICE'
|
||||||
|
) {
|
||||||
const freeResponseParams = validate(freeResponseSchema, body)
|
const freeResponseParams = validate(freeResponseSchema, body)
|
||||||
const { outcome } = freeResponseParams
|
const { outcome } = freeResponseParams
|
||||||
switch (outcome) {
|
switch (outcome) {
|
||||||
|
@ -292,7 +296,10 @@ function getResolutionParams(contract: Contract, body: string) {
|
||||||
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAnswer(contract: FreeResponseContract, answer: number) {
|
function validateAnswer(
|
||||||
|
contract: FreeResponseContract | MultipleChoiceContract,
|
||||||
|
answer: number
|
||||||
|
) {
|
||||||
const validIds = contract.answers.map((a) => a.id)
|
const validIds = contract.answers.map((a) => a.id)
|
||||||
if (!validIds.includes(answer.toString())) {
|
if (!validIds.includes(answer.toString())) {
|
||||||
throw new APIError(400, `${answer} is not a valid answer ID`)
|
throw new APIError(400, `${answer} is not a valid answer ID`)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||||
import { XIcon } from '@heroicons/react/solid'
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { FreeResponseContract } from 'common/contract'
|
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||||
import { BuyAmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { APIError, placeBet } from 'web/lib/firebase/api'
|
import { APIError, placeBet } from 'web/lib/firebase/api'
|
||||||
|
@ -29,7 +29,7 @@ import { isIOS } from 'web/lib/util/device'
|
||||||
|
|
||||||
export function AnswerBetPanel(props: {
|
export function AnswerBetPanel(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
closePanel: () => void
|
closePanel: () => void
|
||||||
className?: string
|
className?: string
|
||||||
isModal?: boolean
|
isModal?: boolean
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { FreeResponseContract } from 'common/contract'
|
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
|
@ -13,7 +13,7 @@ import { Linkify } from '../linkify'
|
||||||
|
|
||||||
export function AnswerItem(props: {
|
export function AnswerItem(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
showChoice: 'radio' | 'checkbox' | undefined
|
showChoice: 'radio' | 'checkbox' | undefined
|
||||||
chosenProb: number | undefined
|
chosenProb: number | undefined
|
||||||
totalChosenProb?: number
|
totalChosenProb?: number
|
||||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx'
|
||||||
import { sum } from 'lodash'
|
import { sum } from 'lodash'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Contract, FreeResponse } from 'common/contract'
|
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { APIError, resolveMarket } from 'web/lib/firebase/api'
|
import { APIError, resolveMarket } from 'web/lib/firebase/api'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
@ -11,7 +11,7 @@ import { ResolveConfirmationButton } from '../confirmation-button'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
|
||||||
export function AnswerResolvePanel(props: {
|
export function AnswerResolvePanel(props: {
|
||||||
contract: Contract & FreeResponse
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
setResolveOption: (
|
setResolveOption: (
|
||||||
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
|
|
|
@ -5,14 +5,14 @@ import { groupBy, sortBy, sumBy } from 'lodash'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { FreeResponseContract } from 'common/contract'
|
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||||
import { getOutcomeProbability } from 'common/calculate'
|
import { getOutcomeProbability } from 'common/calculate'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
|
||||||
const NUM_LINES = 6
|
const NUM_LINES = 6
|
||||||
|
|
||||||
export const AnswersGraph = memo(function AnswersGraph(props: {
|
export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
height?: number
|
height?: number
|
||||||
}) {
|
}) {
|
||||||
|
@ -178,15 +178,22 @@ function formatTime(
|
||||||
return d.format(format)
|
return d.format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeProbsByOutcome = (bets: Bet[], contract: FreeResponseContract) => {
|
const computeProbsByOutcome = (
|
||||||
const { totalBets } = contract
|
bets: Bet[],
|
||||||
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
|
) => {
|
||||||
|
const { totalBets, outcomeType } = contract
|
||||||
|
|
||||||
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
||||||
const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
||||||
const maxProb = Math.max(
|
const maxProb = Math.max(
|
||||||
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
||||||
)
|
)
|
||||||
return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001
|
return (
|
||||||
|
(outcome !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||||
|
maxProb > 0.02 &&
|
||||||
|
totalBets[outcome] > 0.000000001
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const trackedOutcomes = sortBy(
|
const trackedOutcomes = sortBy(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { sortBy, partition, sum, uniq } from 'lodash'
|
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { FreeResponseContract } from 'common/contract'
|
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||||
|
@ -25,14 +25,19 @@ import { UserLink } from 'web/components/user-page'
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
import { BuyButton } from 'web/components/yes-no-selector'
|
import { BuyButton } from 'web/components/yes-no-selector'
|
||||||
|
|
||||||
export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
export function AnswersPanel(props: {
|
||||||
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
|
}) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { creatorId, resolution, resolutions, totalBets } = contract
|
const { creatorId, resolution, resolutions, totalBets, outcomeType } =
|
||||||
|
contract
|
||||||
|
|
||||||
const answers = useAnswers(contract.id) ?? contract.answers
|
const answers = useAnswers(contract.id) ?? contract.answers
|
||||||
const [winningAnswers, losingAnswers] = partition(
|
const [winningAnswers, losingAnswers] = partition(
|
||||||
answers.filter(
|
answers.filter(
|
||||||
(answer) => answer.id !== '0' && totalBets[answer.id] > 0.000000001
|
(answer) =>
|
||||||
|
(answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||||
|
totalBets[answer.id] > 0.000000001
|
||||||
),
|
),
|
||||||
(answer) =>
|
(answer) =>
|
||||||
answer.id === resolution || (resolutions && resolutions[answer.id])
|
answer.id === resolution || (resolutions && resolutions[answer.id])
|
||||||
|
@ -131,7 +136,8 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
||||||
<div className="pb-4 text-gray-500">No answers yet...</div>
|
<div className="pb-4 text-gray-500">No answers yet...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tradingAllowed(contract) &&
|
{outcomeType === 'FREE_RESPONSE' &&
|
||||||
|
tradingAllowed(contract) &&
|
||||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||||
<CreateAnswerPanel contract={contract} />
|
<CreateAnswerPanel contract={contract} />
|
||||||
)}
|
)}
|
||||||
|
@ -152,7 +158,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAnswerItems(
|
function getAnswerItems(
|
||||||
contract: FreeResponseContract,
|
contract: FreeResponseContract | MultipleChoiceContract,
|
||||||
answers: Answer[],
|
answers: Answer[],
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
) {
|
) {
|
||||||
|
@ -178,7 +184,7 @@ function getAnswerItems(
|
||||||
}
|
}
|
||||||
|
|
||||||
function OpenAnswer(props: {
|
function OpenAnswer(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
answer: Answer
|
answer: Answer
|
||||||
items: ActivityItem[]
|
items: ActivityItem[]
|
||||||
type: string
|
type: string
|
||||||
|
|
65
web/components/answers/multiple-choice-answers.tsx
Normal file
65
web/components/answers/multiple-choice-answers.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import { Row } from '../layout/row'
|
||||||
|
|
||||||
|
export function MultipleChoiceAnswers(props: {
|
||||||
|
setAnswers: (answers: string[]) => void
|
||||||
|
}) {
|
||||||
|
const [answers, setInternalAnswers] = useState(['', '', ''])
|
||||||
|
|
||||||
|
const setAnswer = (i: number, answer: string) => {
|
||||||
|
const newAnswers = setElement(answers, i, answer)
|
||||||
|
setInternalAnswers(newAnswers)
|
||||||
|
props.setAnswers(newAnswers)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAnswer = (i: number) => {
|
||||||
|
const newAnswers = answers.slice(0, i).concat(answers.slice(i + 1))
|
||||||
|
setInternalAnswers(newAnswers)
|
||||||
|
props.setAnswers(newAnswers)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAnswer = () => setAnswer(answers.length, '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
{answers.map((answer, i) => (
|
||||||
|
<Row className="mb-2 items-center align-middle">
|
||||||
|
{i + 1}.{' '}
|
||||||
|
<Textarea
|
||||||
|
value={answer}
|
||||||
|
onChange={(e) => setAnswer(i, e.target.value)}
|
||||||
|
className="textarea textarea-bordered ml-2 w-full resize-none"
|
||||||
|
placeholder="Type your answer..."
|
||||||
|
rows={1}
|
||||||
|
maxLength={MAX_ANSWER_LENGTH}
|
||||||
|
/>
|
||||||
|
{answers.length > 2 && (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-outline ml-2"
|
||||||
|
onClick={() => removeAnswer(i)}
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Row className="justify-end">
|
||||||
|
<button className="btn btn-outline btn-xs" onClick={addAnswer}>
|
||||||
|
Add answer
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setElement = <T,>(array: T[], i: number, elem: T) => {
|
||||||
|
const newArray = array.concat()
|
||||||
|
newArray[i] = elem
|
||||||
|
return newArray
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
Contract,
|
Contract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
|
@ -227,7 +228,7 @@ function FreeResponseTopAnswer(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FreeResponseResolutionOrChance(props: {
|
export function FreeResponseResolutionOrChance(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -41,6 +41,8 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
? 'YES / NO'
|
? 'YES / NO'
|
||||||
: outcomeType === 'FREE_RESPONSE'
|
: outcomeType === 'FREE_RESPONSE'
|
||||||
? 'Free response'
|
? 'Free response'
|
||||||
|
: outcomeType === 'MULTIPLE_CHOICE'
|
||||||
|
? 'Multiple choice'
|
||||||
: 'Numeric'
|
: 'Numeric'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -85,7 +85,8 @@ export const ContractOverview = (props: {
|
||||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||||
</Row>
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
outcomeType === 'FREE_RESPONSE' &&
|
(outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
outcomeType === 'MULTIPLE_CHOICE') &&
|
||||||
resolution && (
|
resolution && (
|
||||||
<FreeResponseResolutionOrChance
|
<FreeResponseResolutionOrChance
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -110,7 +111,8 @@ export const ContractOverview = (props: {
|
||||||
{(isBinary || isPseudoNumeric) && (
|
{(isBinary || isPseudoNumeric) && (
|
||||||
<ContractProbGraph contract={contract} bets={bets} />
|
<ContractProbGraph contract={contract} bets={bets} />
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{(outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||||
<AnswersGraph contract={contract} bets={bets} />
|
<AnswersGraph contract={contract} bets={bets} />
|
||||||
)}
|
)}
|
||||||
{outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />}
|
{outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
Contract,
|
Contract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
resolution,
|
resolution,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
||||||
|
@ -77,7 +78,7 @@ export function BinaryContractOutcomeLabel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FreeResponseOutcomeLabel(props: {
|
export function FreeResponseOutcomeLabel(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
resolution: string | 'CANCEL' | 'MKT'
|
resolution: string | 'CANCEL' | 'MKT'
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
answerClassName?: string
|
answerClassName?: string
|
||||||
|
|
|
@ -217,7 +217,8 @@ export function ContractPageContent(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{(outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||||
<>
|
<>
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
<AnswersPanel contract={contract} />
|
<AnswersPanel contract={contract} />
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { Checkbox } from 'web/components/checkbox'
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
|
|
||||||
|
@ -116,6 +117,8 @@ export function NewContract(props: {
|
||||||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||||
const [initialValueString, setInitialValueString] = useState(initValue)
|
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||||
|
|
||||||
|
const [answers, setAnswers] = useState<string[]>([]) // for multiple choice
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupId && creator)
|
if (groupId && creator)
|
||||||
getGroup(groupId).then((group) => {
|
getGroup(groupId).then((group) => {
|
||||||
|
@ -160,6 +163,10 @@ export function NewContract(props: {
|
||||||
// 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')
|
||||||
|
|
||||||
|
const isValidMultipleChoice = answers.every(
|
||||||
|
(answer) => answer.trim().length > 0
|
||||||
|
)
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
(outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) &&
|
(outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) &&
|
||||||
question.length > 0 &&
|
question.length > 0 &&
|
||||||
|
@ -178,7 +185,8 @@ export function NewContract(props: {
|
||||||
min < max &&
|
min < max &&
|
||||||
max - min > 0.01 &&
|
max - min > 0.01 &&
|
||||||
min < initialValue &&
|
min < initialValue &&
|
||||||
initialValue < max))
|
initialValue < max)) &&
|
||||||
|
(outcomeType !== 'MULTIPLE_CHOICE' || isValidMultipleChoice)
|
||||||
|
|
||||||
const [errorText, setErrorText] = useState<string>('')
|
const [errorText, setErrorText] = useState<string>('')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -221,6 +229,7 @@ export function NewContract(props: {
|
||||||
max,
|
max,
|
||||||
initialValue,
|
initialValue,
|
||||||
isLogScale,
|
isLogScale,
|
||||||
|
answers,
|
||||||
groupId: selectedGroup?.id,
|
groupId: selectedGroup?.id,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -259,10 +268,11 @@ export function NewContract(props: {
|
||||||
'Users can submit their own answers to this market.'
|
'Users can submit their own answers to this market.'
|
||||||
)
|
)
|
||||||
else setMarketInfoText('')
|
else setMarketInfoText('')
|
||||||
setOutcomeType(choice as 'BINARY' | 'FREE_RESPONSE')
|
setOutcomeType(choice as outcomeType)
|
||||||
}}
|
}}
|
||||||
choicesMap={{
|
choicesMap={{
|
||||||
'Yes / No': 'BINARY',
|
'Yes / No': 'BINARY',
|
||||||
|
'Multiple choice': 'MULTIPLE_CHOICE',
|
||||||
'Free response': 'FREE_RESPONSE',
|
'Free response': 'FREE_RESPONSE',
|
||||||
Numeric: 'PSEUDO_NUMERIC',
|
Numeric: 'PSEUDO_NUMERIC',
|
||||||
}}
|
}}
|
||||||
|
@ -277,6 +287,10 @@ export function NewContract(props: {
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
{outcomeType === 'MULTIPLE_CHOICE' && (
|
||||||
|
<MultipleChoiceAnswers setAnswers={setAnswers} />
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'PSEUDO_NUMERIC' && (
|
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||||
<>
|
<>
|
||||||
<div className="form-control mb-2 items-start">
|
<div className="form-control mb-2 items-start">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user