create pseudo-numeric contracts

This commit is contained in:
mantikoros 2022-06-27 15:08:30 -05:00
parent 1e904f567a
commit a1bdf552c0
8 changed files with 150 additions and 55 deletions

View File

@ -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),

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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()

View File

@ -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 = {

View File

@ -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

View File

@ -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">