create pseudo-numeric contracts
This commit is contained in:
parent
1e904f567a
commit
a1bdf552c0
|
@ -18,15 +18,24 @@ import {
|
|||
getDpmProbabilityAfterSale,
|
||||
} from './calculate-dpm'
|
||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
||||
import { Contract, BinaryContract, FreeResponseContract } from './contract'
|
||||
import {
|
||||
Contract,
|
||||
BinaryContract,
|
||||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
|
||||
export function getProbability(contract: BinaryContract) {
|
||||
export function getProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbability(contract.pool, contract.p)
|
||||
: getDpmProbability(contract.totalShares)
|
||||
}
|
||||
|
||||
export function getInitialProbability(contract: BinaryContract) {
|
||||
export function getInitialProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
if (contract.initialProbability) return contract.initialProbability
|
||||
|
||||
if (contract.mechanism === 'dpm-2' || (contract as any).totalShares)
|
||||
|
@ -65,7 +74,9 @@ export function calculateShares(
|
|||
}
|
||||
|
||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
@ -87,7 +98,9 @@ export function getProbabilityAfterSale(
|
|||
}
|
||||
|
||||
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
@ -96,7 +109,9 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
|||
const outcome = contract.resolution
|
||||
if (!outcome) throw new Error('Contract not resolved')
|
||||
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
@ -142,9 +157,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
const profit = payout + saleValue + redeemed - totalInvested
|
||||
const profitPercent = (profit / totalInvested) * 100
|
||||
|
||||
const hasShares = Object.values(totalShares).some(
|
||||
(shares) => shares > 0
|
||||
)
|
||||
const hasShares = Object.values(totalShares).some((shares) => shares > 0)
|
||||
|
||||
return {
|
||||
invested: Math.max(0, currentInvested),
|
||||
|
|
|
@ -2,9 +2,10 @@ import { Answer } from './answer'
|
|||
import { Fees } from './fees'
|
||||
|
||||
export type AnyMechanism = DPM | CPMM
|
||||
export type AnyOutcomeType = Binary | FreeResponse | Numeric
|
||||
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
| (DPM & Binary)
|
||||
| (DPM & FreeResponse)
|
||||
| (DPM & Numeric)
|
||||
|
@ -33,7 +34,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
isResolved: boolean
|
||||
resolutionTime?: number // When the contract creator resolved the market
|
||||
resolution?: string
|
||||
resolutionProbability?: number,
|
||||
resolutionProbability?: number
|
||||
|
||||
closeEmailsSent?: number
|
||||
|
||||
|
@ -44,7 +45,8 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
collectedFees: Fees
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
export type BinaryContract = Contract & Binary
|
||||
export type PseudoNumericContract = Contract & PseudoNumeric
|
||||
export type NumericContract = Contract & Numeric
|
||||
export type FreeResponseContract = Contract & FreeResponse
|
||||
export type DPMContract = Contract & DPM
|
||||
|
@ -75,6 +77,17 @@ export type Binary = {
|
|||
resolution?: resolution
|
||||
}
|
||||
|
||||
export type PseudoNumeric = {
|
||||
outcomeType: 'PSEUDO_NUMERIC'
|
||||
min: number
|
||||
max: number
|
||||
isLogScale: boolean
|
||||
|
||||
// same as binary market; map to everything to probability
|
||||
initialProbability: number
|
||||
resolutionProbability?: number
|
||||
}
|
||||
|
||||
export type FreeResponse = {
|
||||
outcomeType: 'FREE_RESPONSE'
|
||||
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
|
||||
|
@ -94,7 +107,7 @@ export type Numeric = {
|
|||
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const
|
||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC'] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
FreeResponse,
|
||||
Numeric,
|
||||
outcomeType,
|
||||
PseudoNumeric,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { parseTags } from './util/parse'
|
||||
|
@ -37,6 +38,8 @@ export function getNewContract(
|
|||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, false)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
@ -111,6 +114,24 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
|||
return system
|
||||
}
|
||||
|
||||
const getPseudoNumericCpmmProps = (
|
||||
initialProb: number,
|
||||
ante: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale: boolean
|
||||
) => {
|
||||
const system: CPMM & PseudoNumeric = {
|
||||
...getBinaryCpmmProps(initialProb, ante),
|
||||
outcomeType: 'PSEUDO_NUMERIC',
|
||||
min,
|
||||
max,
|
||||
isLogScale,
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
const getFreeAnswerProps = (ante: number) => {
|
||||
const system: DPM & FreeResponse = {
|
||||
mechanism: 'dpm-2',
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { sumBy, groupBy, mapValues } from 'lodash'
|
||||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { Contract, CPMMBinaryContract, DPMContract } from './contract'
|
||||
import {
|
||||
Contract,
|
||||
CPMMBinaryContract,
|
||||
DPMContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { Fees } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import {
|
||||
|
@ -56,7 +61,11 @@ export const getPayouts = (
|
|||
liquidities: LiquidityProvision[],
|
||||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
||||
if (
|
||||
contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
) {
|
||||
return getFixedPayouts(
|
||||
outcome,
|
||||
contract,
|
||||
|
@ -76,7 +85,7 @@ export const getPayouts = (
|
|||
|
||||
export const getFixedPayouts = (
|
||||
outcome: string | undefined,
|
||||
contract: CPMMBinaryContract,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutionProbability?: number
|
||||
|
|
|
@ -48,6 +48,7 @@ const binarySchema = z.object({
|
|||
const numericSchema = z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
initialValue: z.number(),
|
||||
})
|
||||
|
||||
export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
||||
|
@ -55,9 +56,14 @@ export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
|||
validate(bodySchema, req.body)
|
||||
|
||||
let min, max, initialProb
|
||||
if (outcomeType === 'NUMERIC') {
|
||||
;({ min, max } = validate(numericSchema, req.body))
|
||||
if (max - min <= 0.01) throw new APIError(400, 'Invalid range.')
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||
let initialValue
|
||||
;({ min, max, initialValue } = validate(numericSchema, req.body))
|
||||
if (max - min <= 0.01 || initialValue < min || initialValue > max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
|
||||
initialProb = (initialValue - min) / (max - min) * 100
|
||||
}
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, req.body))
|
||||
|
@ -130,7 +136,7 @@ export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
|||
|
||||
const providerId = user.id
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||
const liquidityDoc = firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.doc()
|
||||
|
|
|
@ -88,7 +88,10 @@ const toDisplayResolution = (
|
|||
resolutionProbability?: number,
|
||||
resolutions?: { [outcome: string]: number }
|
||||
) => {
|
||||
if (contract.outcomeType === 'BINARY') {
|
||||
if (
|
||||
contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||
) {
|
||||
const prob = resolutionProbability ?? getProbability(contract)
|
||||
|
||||
const display = {
|
||||
|
|
|
@ -142,7 +142,7 @@ export function ContractPageContent(
|
|||
const { creatorId, isResolved, question, outcomeType } = contract
|
||||
|
||||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isBinary = outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const allowTrade = tradingAllowed(contract)
|
||||
const allowResolve = !isResolved && isCreator && !!user
|
||||
|
|
|
@ -78,6 +78,7 @@ export function NewContract(props: { question: string; groupId?: string }) {
|
|||
const [initialProb] = useState(50)
|
||||
const [minString, setMinString] = useState('')
|
||||
const [maxString, setMaxString] = useState('')
|
||||
const [initialValueString, setInitialValueString] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||
// const tags = parseWordsAsTags(tagText)
|
||||
|
@ -120,6 +121,9 @@ export function NewContract(props: { question: string; groupId?: string }) {
|
|||
|
||||
const min = minString ? parseFloat(minString) : undefined
|
||||
const max = maxString ? parseFloat(maxString) : undefined
|
||||
const initialValue = initialValueString
|
||||
? parseFloat(initialValueString)
|
||||
: undefined
|
||||
// get days from today until the end of this year:
|
||||
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
|
||||
|
||||
|
@ -136,13 +140,16 @@ export function NewContract(props: { question: string; groupId?: string }) {
|
|||
// closeTime must be in the future
|
||||
closeTime &&
|
||||
closeTime > Date.now() &&
|
||||
(outcomeType !== 'NUMERIC' ||
|
||||
(outcomeType !== 'PSEUDO_NUMERIC' ||
|
||||
(min !== undefined &&
|
||||
max !== undefined &&
|
||||
initialValue !== undefined &&
|
||||
isFinite(min) &&
|
||||
isFinite(max) &&
|
||||
min < max &&
|
||||
max - min > 0.01))
|
||||
max - min > 0.01 &&
|
||||
min <= initialValue &&
|
||||
initialValue <= max))
|
||||
|
||||
function setCloseDateInDays(days: number) {
|
||||
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
||||
|
@ -166,6 +173,7 @@ export function NewContract(props: { question: string; groupId?: string }) {
|
|||
closeTime,
|
||||
min,
|
||||
max,
|
||||
initialValue,
|
||||
groupId: selectedGroup?.id,
|
||||
tags: category ? [category] : undefined,
|
||||
})
|
||||
|
@ -213,6 +221,7 @@ export function NewContract(props: { question: string; groupId?: string }) {
|
|||
choicesMap={{
|
||||
'Yes / No': 'BINARY',
|
||||
'Free response': 'FREE_RESPONSE',
|
||||
Numeric: 'PSEUDO_NUMERIC',
|
||||
}}
|
||||
isSubmitting={isSubmitting}
|
||||
className={'col-span-4'}
|
||||
|
@ -225,38 +234,59 @@ export function NewContract(props: { question: string; groupId?: string }) {
|
|||
|
||||
<Spacer h={6} />
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<div className="form-control items-start">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Range</span>
|
||||
<InfoTooltip text="The minimum and maximum numbers across the numeric range." />
|
||||
</label>
|
||||
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||
<>
|
||||
<div className="form-control mb-2 items-start">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Range</span>
|
||||
<InfoTooltip text="The minimum and maximum numbers across the numeric range." />
|
||||
</label>
|
||||
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MIN"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMinString(e.target.value)}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={minString ?? ''}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MAX"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMaxString(e.target.value)}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={maxString}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MIN"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMinString(e.target.value)}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={minString ?? ''}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="MAX"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMaxString(e.target.value)}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
disabled={isSubmitting}
|
||||
value={maxString}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
<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)}
|
||||
maxLength={6}
|
||||
disabled={isSubmitting}
|
||||
value={initialValueString ?? ''}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-control max-w-[265px] items-start">
|
||||
|
|
Loading…
Reference in New Issue
Block a user